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,116 @@
1
+ /**
2
+ * The clerk method's skills (D99; Spec §3.3 "每个阶段一份 skill"): the method is given to the Keeper's agent as one pi
3
+ * SKILL.md per stage of the main agent, per kind of lane, one for the synthesis (D103: a job of its own, whose prompt
4
+ * starts with it) and one for the independent spot-check — not as tasks the program cuts. They live in the install's `skills/clerk/<name>/SKILL.md`, in pi's format (frontmatter `name` and
5
+ * `description`), and reach an agent two ways (E148 D-b): pi loads the folder natively for the clerk round's jobs
6
+ * (runtime.ts `additionalSkillPaths`) and lists them in its system prompt, and the text itself is put in front of the
7
+ * agent — `pk_stage` returns the stage's skill, and a lane's prompt starts with its lane's skill — since GLM does not
8
+ * always open a skill file by itself.
9
+ *
10
+ * The folder is resolved from the running install, relative to this module's own file, never from the project's
11
+ * working directory: ProjectKeeper's own skills folder is also project material when the Keeper organizes its own
12
+ * repository, and the resident install is a separate checkout (build plan §4 "自指"). Reads of this folder are left out of what a
13
+ * round counts as read (`isClerkSkillPath`).
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { isAbsolute, join } from 'node:path';
18
+ import { stripFrontmatter } from '@earendil-works/pi-coding-agent';
19
+ import type { ClerkStage, LaneKind } from '../../model/k-types.ts';
20
+ import { isWithin, normalizePath } from '../../util/paths.ts';
21
+
22
+ /** The skill of each stage of the main agent's session (D103: the synthesis is no stage of it; `SYNTHESIS_SKILL`). */
23
+ export const STAGE_SKILLS: Readonly<Record<Exclude<ClerkStage, 'synthesis'>, string>> = {
24
+ orientation: 'orientation',
25
+ skeleton: 'skeleton-dispatch',
26
+ reconcile: 'reconcile',
27
+ dig: 'deepen-dispatch',
28
+ coverage: 'coverage',
29
+ 'cross-check': 'cross-check',
30
+ };
31
+
32
+ /** The synthesis' skill (D103): the head of the synthesis job's prompt. */
33
+ export const SYNTHESIS_SKILL = 'synthesis';
34
+
35
+ /** The skill of each kind of lane. */
36
+ export const LANE_SKILLS: Readonly<Record<LaneKind, string>> = {
37
+ slot: 'lane-slot',
38
+ plan: 'lane-plan',
39
+ topic: 'lane-topic',
40
+ 'follow-up': 'lane-follow-up',
41
+ };
42
+
43
+ /** The independent spot-check's skill. */
44
+ export const SPOT_CHECK_SKILL = 'spot-check';
45
+
46
+ /** Every clerk skill, by folder name. */
47
+ export const CLERK_SKILL_NAMES: readonly string[] = [...Object.values(STAGE_SKILLS), SYNTHESIS_SKILL, ...Object.values(LANE_SKILLS), SPOT_CHECK_SKILL];
48
+
49
+ /** The folder of the clerk method's skills in the running install (`<install>/skills/clerk`). */
50
+ export function clerkSkillsDir(): string {
51
+ return normalizePath(fileURLToPath(new URL('../../../skills/clerk/', import.meta.url)));
52
+ }
53
+
54
+ /** The SKILL.md of one clerk skill in the running install. */
55
+ export function clerkSkillFile(name: string): string {
56
+ return join(clerkSkillsDir(), name, 'SKILL.md');
57
+ }
58
+
59
+ const bodies = new Map<string, string>();
60
+
61
+ /** A skill's body, without its frontmatter. Read once per process: the install does not change under a running Keeper. */
62
+ function skillBody(name: string): string {
63
+ let body = bodies.get(name);
64
+ if (body === undefined) {
65
+ const file = clerkSkillFile(name);
66
+ let raw: string;
67
+ try { raw = readFileSync(file, 'utf8'); } catch (e) { throw new Error(`the clerk skill "${name}" is missing from the install (${file}): ${(e as Error).message}`); }
68
+ body = stripFrontmatter(raw).trim();
69
+ bodies.set(name, body);
70
+ }
71
+ return body;
72
+ }
73
+
74
+ /** The skill of a stage of the main agent's session, without frontmatter (what `pk_stage` returns). */
75
+ export function stageSkill(stage: ClerkStage): string {
76
+ const name = (STAGE_SKILLS as Readonly<Record<string, string>>)[stage];
77
+ if (!name) throw new Error(`no clerk skill for the stage "${stage}"`);
78
+ return skillBody(name);
79
+ }
80
+
81
+ /** The synthesis' skill, without frontmatter (the head of the synthesis job's prompt, D103). */
82
+ export function synthesisSkill(): string {
83
+ return skillBody(SYNTHESIS_SKILL);
84
+ }
85
+
86
+ /** The skill of a kind of lane, without frontmatter (the head of a lane's prompt). */
87
+ export function laneSkill(kind: LaneKind): string {
88
+ const name = LANE_SKILLS[kind];
89
+ if (!name) throw new Error(`no clerk skill for the lane kind "${kind}"`);
90
+ return skillBody(name);
91
+ }
92
+
93
+ /** The independent spot-check's skill, without frontmatter. */
94
+ export function spotCheckSkill(): string {
95
+ return skillBody(SPOT_CHECK_SKILL);
96
+ }
97
+
98
+ /** The job kinds of a clerk round that pi loads the clerk skills for (D99, D103): the main agent, its lanes, the synthesis, the spot-check. */
99
+ export const SKILLED_STEP_KINDS: ReadonlySet<string> = new Set(['main', 'lane', 'synthesis', 'spot-check']);
100
+
101
+ /** The extra skill paths pi's resource loader gets for a job: the clerk skills folder for a clerk round's jobs, else none. */
102
+ export function clerkSkillPathsFor(stepKind: string | null | undefined): string[] {
103
+ return stepKind && SKILLED_STEP_KINDS.has(stepKind) ? [clerkSkillsDir()] : [];
104
+ }
105
+
106
+ /**
107
+ * Whether a path is in the install's clerk skills folder — an agent reading its own method, not the project's material,
108
+ * so a round's read tally and coverage leave it out (W5, W8). A relative path is taken against `cwd` when given, else
109
+ * it is not the skills folder (the install is never addressed relative to a project).
110
+ */
111
+ export function isClerkSkillPath(path: string, cwd?: string): boolean {
112
+ const p = path.trim();
113
+ if (!p) return false;
114
+ if (!isAbsolute(p) && !cwd) return false;
115
+ try { return isWithin(clerkSkillsDir(), isAbsolute(p) ? p : join(cwd!, p)); } catch { return false; }
116
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The workbench's slots, and the ones no lane holds (DB; Spec §3.3 "工作台是一道填空题").
3
+ *
4
+ * The program knows the slots and which lane holds which. When a first usable round's skeleton lanes are sent
5
+ * (`pk_send_lanes`, lane-tools.ts), a slot no lane holds is listed back before anything is sent: the main agent gives it
6
+ * to a lane, or says in one line why the project has nothing for it. The reason is kept on the round (`emptySlots`) and
7
+ * shown to the next spot-check; the program refuses nothing more and does not judge the reason. On the DeepSeek run
8
+ * (2026-10-04) no lane was given the Boundary slot, and the workbench held no boundary although the PRD had a section on
9
+ * what the product does not include — the two runs before it held 18 and 34.
10
+ */
11
+ import type { ClerkRound, EmptySlot, SlotKind } from '../../model/k-types.ts';
12
+ import type { ProjectStore } from '../../store/project-store.ts';
13
+ import { REFERENCE_CATEGORY } from '../../model/vocab.ts';
14
+
15
+ /** What each reference category holds, in words a main agent can match against the project's documents. */
16
+ const REFERENCE_HOLDS: Readonly<Record<(typeof REFERENCE_CATEGORY)[number], string>> = {
17
+ "Owner's words": 'the owner’s own lines, verbatim',
18
+ Product: 'what the product is',
19
+ Goal: 'the goals the product serves',
20
+ Area: 'the modules or areas of the product',
21
+ Requirement: 'the requirements: what the product must do',
22
+ Design: 'the designs: how it is built, section by section',
23
+ Decision: 'the decisions and their records',
24
+ Plan: 'the plans: increments, phases, batches, milestones',
25
+ Boundary: 'what the product does not include: non-goals, out of scope, limits it states',
26
+ };
27
+
28
+ /**
29
+ * The slots a first usable round's skeleton fills by lane, each with what it holds: every reference category, the work
30
+ * items, their process links, the code territories, the earlier generations. `patches` and `relations` are not among
31
+ * them: reconcile drafts the supersession lines no lane drafted, and relations tie the items of the other slots — neither
32
+ * is a position left empty because no lane held it.
33
+ */
34
+ export const SKELETON_SLOTS: readonly { readonly slot: SlotKind; readonly holds: string }[] = [
35
+ ...REFERENCE_CATEGORY.map((c) => ({ slot: `reference:${c}` as SlotKind, holds: REFERENCE_HOLDS[c] })),
36
+ { slot: 'threads', holds: 'the work items: contracts, tickets, tasks, issues' },
37
+ { slot: 'links', holds: 'what ties work to its delivery: dispatches, deliveries, merges, checks' },
38
+ { slot: 'territories', holds: 'the code as it stands, by area' },
39
+ { slot: 'generations', holds: 'earlier generations of the plan: archived or superseded plan, contract and module sets' },
40
+ ];
41
+
42
+ /** A slot as it is compared: the apostrophe as typed, any case. */
43
+ export const slotKey = (s: string): string => s.replace(/[’‘`]/g, "'").trim().toLowerCase();
44
+
45
+ /** The skeleton's slot a name stands for, or undefined. */
46
+ export const skeletonSlot = (name: string) => SKELETON_SLOTS.find((s) => slotKey(s.slot) === slotKey(name));
47
+
48
+ /** The slots of the skeleton that neither a lane holds nor a recorded reason accounts for. */
49
+ export function slotsUnheld(held: readonly string[], reasons: readonly Pick<EmptySlot, 'slot'>[]): { readonly slot: SlotKind; readonly holds: string }[] {
50
+ const taken = new Set([...held, ...reasons.map((r) => r.slot)].map(slotKey));
51
+ return SKELETON_SLOTS.filter((s) => !taken.has(slotKey(s.slot)));
52
+ }
53
+
54
+ /** Whether the workbench holds anything in a slot now. */
55
+ export function slotHolds(store: ProjectStore, slot: string): boolean {
56
+ const key = slotKey(slot);
57
+ if (key.startsWith('reference:')) return store.reference.find((r) => `reference:${r.category}`.toLowerCase() === key && r.validity !== 'Removed') !== undefined;
58
+ if (key === 'threads') return store.threads.size > 0;
59
+ if (key === 'links') return store.links.size > 0;
60
+ if (key === 'territories') return store.territories.size > 0;
61
+ if (key === 'generations') return store.generations.size > 0;
62
+ return true;
63
+ }
64
+
65
+ /** Every recorded reason of the project, oldest round first, each with its round and whether the slot holds anything now. */
66
+ export function emptySlotsOf(store: ProjectStore): (EmptySlot & { readonly round: string; readonly holdsNow: boolean })[] {
67
+ return store.clerkRounds.all().sort((a, b) => a.startedAt.localeCompare(b.startedAt))
68
+ .flatMap((r) => (r.emptySlots ?? []).map((e) => ({ ...e, round: `${r.kind} round ${r.number}`, holdsNow: slotHolds(store, e.slot) })));
69
+ }
70
+
71
+ /**
72
+ * What a round's spot-check is shown of the slots left to no lane: the reasons recorded since the last spot-check of an
73
+ * earlier round began, whichever round recorded them — a first usable round has no spot-check of its own — for the slots
74
+ * that still hold nothing. Null when there is none.
75
+ */
76
+ export function emptySlotsBlock(store: ProjectStore, round: Pick<ClerkRound, 'id'>): string | null {
77
+ const lastCheck = store.jobs.filter((j) => j.step?.kind === 'spot-check' && j.step.roundId !== round.id).map((j) => j.queuedAt).sort().at(-1) ?? '';
78
+ const standing = emptySlotsOf(store).filter((e) => e.at > lastCheck && !e.holdsNow);
79
+ if (!standing.length) return null;
80
+ return [
81
+ `=== Slots no lane held (${standing.length}; the main agent’s reasons, which the program does not check)`,
82
+ 'The skeleton gave these workbench slots to no lane, and the main agent said why the project has nothing for them; the workbench still holds nothing there. Check each reason against the project’s documents: a section or a table that states such material makes it wrong. Say in your Spot check document which reasons hold and which do not, each wrong one with where the material is (path and line), so the next round gives that slot to a lane.',
83
+ ...standing.map((e) => `- ${e.slot} (${skeletonSlot(e.slot)?.holds ?? 'a workbench slot'}) · ${e.round}: “${e.why}”`),
84
+ ].join('\n');
85
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * What the main agent and its lanes may write, decided at call time by the job's effective stage (D99; E148 D-a).
3
+ *
4
+ * pi cannot change a session's tools halfway through, so the main job is offered every stage's writers at once
5
+ * (`STAGE_WRITERS`, clerk-steps.ts) and each writer refuses outside the stages that list it: the main job's effective
6
+ * stage is the stage its round is in (`ClerkRound.stage`). A lane is offered the writers of the slots it was given
7
+ * (`SLOT_WRITERS`) and its Report; its effective stage is `lane`, and `reference:<category>` is checked by category too.
8
+ * So one matter is still written once, by the one that owns it (Spec §3.3 "谁写什么"), without new sessions per stage.
9
+ *
10
+ * D103: once the main agent has handed the round over to the synthesis (`handedOver`), its session writes nothing more;
11
+ * the synthesis job writes by its own step's table (roles.ts `STEP_WRITES.synthesis`), not by a stage.
12
+ */
13
+ import type { ClerkRound, ClerkStage, RoundDocKind, RoundStepKind, SlotKind } from '../../model/k-types.ts';
14
+ import type { KeeperJob } from '../../model/types.ts';
15
+ import type { ProjectStore } from '../../store/project-store.ts';
16
+ import { DOCS_BY_STAGE, LANE_ALWAYS, LANE_DOCS, MAIN_ALWAYS, ROUND_STAGES, SLOT_WRITERS, STAGE_WRITERS } from '../clerk-steps.ts';
17
+
18
+ type Step = NonNullable<KeeperJob['step']>;
19
+
20
+ /**
21
+ * The judgements only a Follow up round's cross-check makes (Spec §5.5; roles.ts `FOLLOW_UP_WRITES`): the main agent of a
22
+ * Follow up round is offered them and writes them in its cross-check stage.
23
+ */
24
+ export const MAIN_FOLLOW_UP_WRITES: readonly string[] = ['pk_write_change', 'pk_judge_object', 'pk_set_propagation', 'pk_set_propagations', 'pk_write_modification_request', 'pk_mark_request_handled', 'pk_round_pending'];
25
+
26
+ /** The stage the main agent is in: the round's, or its first stage before it has moved (a round started before it said). */
27
+ export function stageOfRound(round: Pick<ClerkRound, 'kind' | 'stage'>): ClerkStage {
28
+ return round.stage ?? ROUND_STAGES[round.kind][0]!;
29
+ }
30
+
31
+ /**
32
+ * Whether the main agent handed the round over to the synthesis (D103): its handover is recorded — or, on a round
33
+ * recorded before D103, it had moved into the synthesis stage, which the synthesis job now holds.
34
+ */
35
+ export function handedOver(round: Pick<ClerkRound, 'stage' | 'handover'>): boolean {
36
+ return !!round.handover || round.stage === 'synthesis';
37
+ }
38
+
39
+ /** The main agent's last stage in a round of this kind: the one it hands over from. */
40
+ export function lastStageOf(kind: ClerkRound['kind']): ClerkStage {
41
+ const order = ROUND_STAGES[kind];
42
+ return order[order.length - 1]!;
43
+ }
44
+
45
+ const HANDED_OVER = `the main agent handed this round over to the synthesis, which runs in a session of its own: the main agent's work is done, and its session writes nothing more`;
46
+
47
+ export type EffectiveStage =
48
+ | { readonly kind: 'main'; readonly stage: ClerkStage; readonly round: ClerkRound }
49
+ | { readonly kind: 'lane'; readonly name: string; readonly slots: readonly SlotKind[]; readonly round: ClerkRound | null };
50
+
51
+ /** Who a job writes as (D99): the main job in its round's stage, a lane with its slots; null for any other job. */
52
+ export function effectiveStage(store: ProjectStore, step: Step | null | undefined): EffectiveStage | null {
53
+ if (!step) return null;
54
+ const round = store.clerkRounds.get(step.roundId) ?? null;
55
+ if (step.kind === 'main') return round ? { kind: 'main', stage: stageOfRound(round), round } : null;
56
+ if (step.kind === 'lane') return { kind: 'lane', name: step.path ?? '', slots: step.lane?.slots ?? [], round };
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * The step kind whose rules a stage-dependent writer applies (a patch is a draft before the cross-check, a link is
62
+ * confirmed and a send-back limited in the cross-check): the main job's stage by the step it replaces, `lane` for a lane,
63
+ * the job's own step kind otherwise.
64
+ */
65
+ export function stepKindAs(store: ProjectStore, step: Step | null | undefined): RoundStepKind | 'lane' | null {
66
+ if (!step) return null;
67
+ const eff = effectiveStage(store, step);
68
+ if (!eff) return step.kind;
69
+ if (eff.kind === 'lane') return 'lane';
70
+ const as: Readonly<Record<ClerkStage, RoundStepKind>> = { orientation: 'orientation', skeleton: 'skeleton', reconcile: 'skeleton', dig: 'dig', coverage: 'dig', 'cross-check': 'cross-check', synthesis: 'synthesis' };
71
+ return as[eff.stage];
72
+ }
73
+
74
+ const either = (list: readonly string[]): string => (list.length <= 1 ? list.join('') : `${list.slice(0, -1).join(', ')} or ${list[list.length - 1]}`);
75
+ const slotBase = (slot: string): string => (slot.startsWith('reference:') ? 'reference' : slot.split(':')[0]!);
76
+ const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
77
+
78
+ /** The stages whose writers list a tool. */
79
+ export function stagesWriting(tool: string): ClerkStage[] {
80
+ return (Object.keys(STAGE_WRITERS) as ClerkStage[]).filter((s) => STAGE_WRITERS[s].includes(tool));
81
+ }
82
+
83
+ /** The slot kinds whose writers list a tool (`reference` standing for every `reference:<category>`). */
84
+ export function slotsWriting(tool: string): string[] {
85
+ return Object.entries(SLOT_WRITERS).filter(([, tools]) => tools.includes(tool)).map(([slot]) => (slot === 'reference' ? 'reference:<category>' : slot));
86
+ }
87
+
88
+ /** The reference category a call writes, when it writes reference items: given, or the one of the item it updates. */
89
+ function referenceCategory(store: ProjectStore, tool: string, args: Record<string, unknown> | null): { readonly writes: 'reference' | 'threads' | null; readonly category: string | null } {
90
+ if (!args) return { writes: null, category: null };
91
+ if (tool === 'pk_write_reference') {
92
+ const given = text(args.category);
93
+ const id = text(args.id);
94
+ return { writes: 'reference', category: given || (id ? store.reference.get(id)?.category ?? null : null) };
95
+ }
96
+ // DA: judging the owner's lines is the Owner's words slot's, like the items it writes.
97
+ if (tool === 'pk_judge_owner_lines') return { writes: 'reference', category: "Owner's words" };
98
+ if (tool === 'pk_fill_from_table') return text(args.into) === 'threads' ? { writes: 'threads', category: null } : { writes: 'reference', category: text(args.category) || null };
99
+ if (tool === 'pk_fill_from_headings' || tool === 'pk_fill_from_bold') return { writes: 'reference', category: text(args.category) || null };
100
+ return { writes: null, category: null };
101
+ }
102
+
103
+ /**
104
+ * Why the main job or a lane may not make this call now, or null (D99): the main job writes only what its stage lists (and
105
+ * in a Follow up's cross-check the round's judgements), a lane only what its slots and every lane may write. Any other job
106
+ * is not decided here.
107
+ */
108
+ export function writeRefusal(store: ProjectStore, step: Step | null | undefined, tool: string, args: Record<string, unknown> | null = null): string | null {
109
+ const eff = effectiveStage(store, step);
110
+ if (!eff) return null;
111
+ if (eff.kind === 'main') {
112
+ if ((MAIN_ALWAYS as readonly string[]).includes(tool)) return null;
113
+ if (handedOver(eff.round)) return `${tool}: ${HANDED_OVER}. Nothing was written. End with a short summary of what you handed over.`;
114
+ if (STAGE_WRITERS[eff.stage].includes(tool)) return null;
115
+ if (eff.round.kind === 'Follow up' && MAIN_FOLLOW_UP_WRITES.includes(tool)) {
116
+ return eff.stage === 'cross-check' ? null : `${tool} judges into the Follow up round's record, which the main agent does in its cross-check stage; you are in the ${eff.stage} stage, so nothing was written.`;
117
+ }
118
+ const stages = stagesWriting(tool);
119
+ if (stages.length) return `${tool} is written in the ${either(stages)} stage of the main agent's session (each stage writes its own positions); this round is in the ${eff.stage} stage, so nothing was written. Move to that stage with pk_stage when this one is done, or give the write to a lane whose slot it is.`;
120
+ const slots = slotsWriting(tool);
121
+ return `${tool} is not the main agent's to write in any stage${slots.length ? `: a lane with the slot ${either(slots)} writes it (send one with pk_send_lanes)` : ''}, so nothing was written.`;
122
+ }
123
+ if ((LANE_ALWAYS as readonly string[]).includes(tool)) return null;
124
+ const own = eff.slots.filter((s) => (SLOT_WRITERS[slotBase(s) as keyof typeof SLOT_WRITERS] ?? []).includes(tool));
125
+ const given = eff.slots.length ? eff.slots.join(', ') : 'none';
126
+ if (!own.length) {
127
+ const slots = slotsWriting(tool);
128
+ return `${tool} writes ${slots.length ? `the slot ${either(slots)}` : 'a position no lane writes'}; this lane (${eff.name}) writes ${given} (a lane writes only the slots its brief gives it), so nothing was written. Put what you found in your Report for the main agent.`;
129
+ }
130
+ const ref = referenceCategory(store, tool, args);
131
+ if (ref.writes === 'threads') return eff.slots.includes('threads') ? null : `${tool} into threads writes work items, the slot threads; this lane (${eff.name}) writes ${given}, so nothing was written.`;
132
+ if (ref.writes === 'reference') {
133
+ if (!ref.category) return `${tool} writes reference items, and a lane writes those only in the categories its slots name (reference:<category>): give the category. Nothing was written.`;
134
+ const want = `reference:${ref.category}`.toLowerCase();
135
+ if (eff.slots.some((s) => s.toLowerCase() === want)) return null;
136
+ const cats = eff.slots.filter((s) => s.startsWith('reference:'));
137
+ return `${tool} would write a ${ref.category} reference item, the slot reference:${ref.category}; this lane (${eff.name}) writes ${cats.length ? cats.join(', ') : 'no reference category'}, so nothing was written. Put it in your Report for the main agent.`;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /** Why the main job or a lane may not write a round document of this kind now, or null (D99; `DOCS_BY_STAGE`, `LANE_DOCS`). */
143
+ export function roundDocRefusal(store: ProjectStore, step: Step | null | undefined, kind: RoundDocKind): string | null {
144
+ const eff = effectiveStage(store, step);
145
+ if (!eff) return null;
146
+ if (eff.kind === 'lane') return LANE_DOCS.includes(kind) ? null : `A lane writes its own ${either([...LANE_DOCS])}; a ${kind} is the main agent's. Nothing was written.`;
147
+ if (handedOver(eff.round)) return `A ${kind}: ${HANDED_OVER}. Nothing was written.`;
148
+ if (kind === 'Handover') return `The handover is written as you hand the round over: pk_stage({ to: "synthesis", handover: { settled, open, first } }) once the ${lastStageOf(eff.round.kind)} stage is done. Nothing was written.`;
149
+ const allowed = DOCS_BY_STAGE[eff.stage];
150
+ if (allowed.includes(kind)) return null;
151
+ const stages = (Object.keys(DOCS_BY_STAGE) as ClerkStage[]).filter((s) => DOCS_BY_STAGE[s].includes(kind));
152
+ const where = stages.length ? `in the ${either(stages)} stage` : LANE_DOCS.includes(kind) ? 'by a lane, as its own report' : kind === 'Result' ? 'by the synthesis, in a session of its own' : 'by the spot-check';
153
+ return `In the ${eff.stage} stage the main agent writes ${allowed.length ? either([...allowed]) : 'no round document'}; a ${kind} is written ${where}. Nothing was written.`;
154
+ }
155
+
156
+ type Result = { content: { type: 'text'; text: string }[]; details: Record<string, never>; isError?: boolean };
157
+ type Executable = { readonly name: string; execute?: (...args: never[]) => unknown };
158
+
159
+ /**
160
+ * The same tool, refusing at call time what `refusal` refuses (the main job's stage, a lane's slots). The refusal comes
161
+ * back the way a writer's own refusal does, so the model reads it as a refused call and nothing is written.
162
+ */
163
+ export function gatedTool<T extends Executable>(tool: T, refusal: (tool: string, args: Record<string, unknown> | null) => string | null): T {
164
+ const run = tool.execute as ((...args: unknown[]) => Promise<unknown>) | undefined;
165
+ if (typeof run !== 'function') return tool;
166
+ return {
167
+ ...tool,
168
+ execute: async (...args: unknown[]) => {
169
+ const params = args[1] && typeof args[1] === 'object' ? args[1] as Record<string, unknown> : null;
170
+ const why = refusal(tool.name, params);
171
+ if (why) return { content: [{ type: 'text', text: `ERROR: ${why}` }], details: {}, isError: true } satisfies Result;
172
+ return run.apply(tool, args);
173
+ },
174
+ } as T;
175
+ }