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,33 @@
1
+ /**
2
+ * "Project positioning": the small block every organizing job gets so it knows which project
3
+ * it is in, what the product reference and areas currently are, and which roles exist. It is
4
+ * deliberately short (Spec §3.3: each job takes only the input it needs, never the whole history).
5
+ */
6
+ import type { Project } from '../../model/types.ts';
7
+ import type { ProjectStore } from '../../store/project-store.ts';
8
+
9
+ export function positioningBlock(store: ProjectStore, project: Project): string {
10
+ const lines: string[] = [];
11
+ lines.push(`Project: ${project.name} (root ${project.locations[0]})`);
12
+ if (project.roles.length) lines.push(`Roles recognised in this project: ${project.roles.join(', ')}`);
13
+ const scope = project.scope.filter((i) => i.relation !== 'Excluded' && i.category !== 'Session source');
14
+ lines.push(`In scope: ${scope.map((i) => `${i.category} ${i.path} (${i.relation})`).join('; ')}`);
15
+ const refs = store.reference.all();
16
+ if (refs.length) {
17
+ lines.push('Product reference (id · category · name · validity):');
18
+ for (const r of refs.filter((x) => x.validity === 'Current' || x.validity === 'Proposed').slice(0, 80)) lines.push(`- ${r.id} · ${r.category} · ${r.name}${r.ids.length ? ` [${r.ids.join(', ')}]` : ''} · ${r.validity}${r.basis === 'Inferred' ? ' (Inferred)' : ''}`);
19
+ const replaced = refs.filter((x) => x.validity === 'Replaced' || x.validity === 'Abandoned' || x.validity === 'Deferred');
20
+ if (replaced.length) lines.push(`- (${replaced.length} more items are Replaced, Deferred or Abandoned; read them with pk_read_assets kind=reference if needed)`);
21
+ } else {
22
+ lines.push('Product reference: none yet.');
23
+ }
24
+ const areas = store.reference.filter((r) => r.category === 'Area' && r.validity === 'Current');
25
+ if (areas.length) lines.push(`Areas: ${areas.map((a) => `${a.name} (${a.id})`).join('; ')}`);
26
+ const threads = store.threads.all().filter((t) => t.validity === 'Current' || t.validity === 'Proposed');
27
+ if (threads.length) {
28
+ lines.push(`Work items (${threads.length}; id · project ids · title · progress · serves):`);
29
+ for (const t of threads.slice(0, 120)) lines.push(`- ${t.id} · [${t.ids.join(', ')}] · ${t.title.slice(0, 70)} · ${t.progress} · ${t.serves.map((s) => store.reference.get(s.referenceId)?.name.slice(0, 24) ?? s.referenceId).join(', ') || 'serves nothing yet'}`);
30
+ if (threads.length > 120) lines.push(`- (${threads.length - 120} more; pk_read_assets kind=thread lists them)`);
31
+ }
32
+ return lines.join('\n');
33
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A readiness word is not progress (DB; Spec §2.2; the lane skill: 「"ready" is readiness: don't map it」).
3
+ *
4
+ * A project's table writes a status per row. Some of those words say how far the work is — done, in progress, blocked,
5
+ * deferred. Others say how far the *document* is: that the contract or plan row is drafted, ready to be picked up,
6
+ * approved, accepted, final. The second kind says nothing about the work: a contract that is "ready" may be untouched or
7
+ * long delivered, and only the execution records tell which.
8
+ *
9
+ * On the DeepSeek run (2026-10-04) the main agent's brief gave the plan lane 「statusMap:ready→Planned」; the fill took
10
+ * it, and none of the 27 contracts showed `Done` although the execution plan counted most of them delivered. The six-run
11
+ * read-through had already listed that reading as one no check points at. So the program holds the list, and the fill
12
+ * tool takes no mapping from one of these words to a progress: the word stays the row's written status, and the row's
13
+ * progress is the lane's to judge from the execution records.
14
+ *
15
+ * One list, used by the fill tool and printed in its description; the lane skill names the same words.
16
+ */
17
+
18
+ /** The words, as a status cell opens with them: English as whole words, Chinese as written. */
19
+ export const READINESS_WORDS: readonly string[] = [
20
+ 'ready', 'draft', 'drafted', 'approved', 'accepted', 'final', 'finalized', 'finalised',
21
+ '就绪', '已就绪', '准备就绪', '草案', '草稿', '初稿', '已批准', '批准', '已核准', '已审批', '已接受', '已采纳', '定稿', '已定稿', '终稿', '最终版',
22
+ ];
23
+
24
+ /** The words the rule is stated with, for a sentence that names it. */
25
+ export const READINESS_EXAMPLES = 'ready, draft, approved, accepted, final, 就绪, 草案, 已批准, 定稿';
26
+
27
+ const LATIN = READINESS_WORDS.filter((w) => /^[a-z]+$/.test(w));
28
+ const CJK = READINESS_WORDS.filter((w) => !/^[a-z]+$/.test(w)).sort((a, b) => b.length - a.length);
29
+ /** A status cell as it is compared: no emphasis or code marks, one space, lower case. */
30
+ const plain = (s: string): string => s.replace(/\*\*|__|`/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
31
+
32
+ /**
33
+ * The readiness word a written status opens with, or null: `ready`, `Ready (review after K)`, `draft`, `已批准(D12)`.
34
+ * A status that opens with another word — `not ready`, `done`, `deferred (D79)`, `in progress` — states no readiness here.
35
+ */
36
+ export function readinessWord(status: string): string | null {
37
+ const s = plain(status);
38
+ if (!s) return null;
39
+ const first = /^[a-z]+/.exec(s)?.[0];
40
+ if (first) return LATIN.includes(first) ? first : null;
41
+ return CJK.find((w) => s.startsWith(w)) ?? null;
42
+ }
43
+
44
+ /** The rule in one sentence, for a refusal and for a tool's description. */
45
+ export const READINESS_RULE = `a word for the readiness or approval of the document itself (${READINESS_EXAMPLES}) is not progress: it is kept as the row’s written status and maps to no progress`;
46
+ /** The same, as a sentence of its own. */
47
+ export const READINESS_SENTENCE = `${READINESS_RULE[0]!.toUpperCase()}${READINESS_RULE.slice(1)}`;
@@ -0,0 +1,401 @@
1
+ /**
2
+ * What a deepening's material is and what was read of it (Spec §3.3, §3.7), as the coverage check, the depth question and
3
+ * the views of the rounds from before D99 still use it:
4
+ * - the organizing plan's Read closely, resolved through the ledger (`readCloselyOf`, `readCloselyNow`; Spec §3.7, D62):
5
+ * what the coverage check plans, and what the depth question counts on each path (`closelyPath`);
6
+ * - what a brief names (`namedInBrief`), the directories the project's rules settle (`settlingDirs`);
7
+ * - what a tally of the recorded reads read of each material (`readsOf`): whole, in part, not at all;
8
+ * - a deepening from before D99, read in reading assignments (`RoundReading`): its paths as the Keeper view shows them
9
+ * (`pathReadingOf`, `readingBasisText`).
10
+ * D99 replaced the reading assignments with the main agent's lanes and the coverage check (coverage-tools.ts): the program
11
+ * no longer places, sizes, packs, queues or chases a deepening's reading, so that machinery is gone (git keeps it).
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import { join, sep } from 'node:path';
15
+ import type { ClerkRound, ReadingAssignment, ReadingCategory, ReadingMaterial, RoundReading, RoundReadingPath } from '../../model/k-types.ts';
16
+ import type { KeeperJob, OrganizingPlan, OrganizingPlanContent, PathReading, Project } from '../../model/types.ts';
17
+ import type { ProjectStore } from '../../store/project-store.ts';
18
+ import type { Ledger } from '../../ledger/index.ts';
19
+ import { isWithin, nameForm, pathKey, relativeDisplay } from '../../util/paths.ts';
20
+ import { coversAll, mergeRanges } from '../bounds/reads.ts';
21
+ import { tallyReads, type FileReads, type ReadTally } from './clerk-coverage.ts';
22
+ import { DEEPENING_PATHS, type SweepKind } from './clerk-prompts.ts';
23
+
24
+ // ───────────────────────── what a deepening's material is ─────────────────────────
25
+
26
+ /** The category a kind of question counted by the ledger's totals plans whole (`kindTotals` counts it as its materials). */
27
+ export const KIND_CATEGORY: Readonly<Record<SweepKind, ReadingCategory>> = {
28
+ [DEEPENING_PATHS[0]]: 'sessions', [DEEPENING_PATHS[1]]: 'document versions', [DEEPENING_PATHS[2]]: 'commits', [DEEPENING_PATHS[3]]: 'code files',
29
+ } as Record<SweepKind, ReadingCategory>;
30
+
31
+ /** The directories the project's own material rules settle (§1.15: obsolete, reference only, recovery only). */
32
+ export function settlingDirs(store: ProjectStore): string[] {
33
+ const settling = new Set(['Obsolete', 'Reference only', 'Recovery only']);
34
+ return [...new Set(store.rules.filter((r) => r.group === 'Material rules' && r.validity === 'Current' && settling.has(r.category ?? ''))
35
+ .flatMap((r) => r.appliesTo).map((a) => nameForm(a.trim()).replace(/\\/g, '/')).filter((a) => /^[\w.-]+(\/[\w.-]+)*\/?$/.test(a) && !/\.\w+$/.test(a)))];
36
+ }
37
+ export const underAny = (dirs: readonly string[], path: string): boolean => dirs.some((d) => { const x = d.replace(/\/+$/, ''); return path === x || path.startsWith(`${x}/`); });
38
+
39
+ /** A commit read by a hash that is the planned commit, or a prefix of it long enough to name one. */
40
+ export const sameCommit = (read: string, planned: string): boolean => read.length >= 7 && planned.toLowerCase().startsWith(read.toLowerCase());
41
+
42
+ /**
43
+ * The paths and commits a brief names: `docs/plan/`, `src/app.ts:12`, `a1b2c3d`. Hashes only count when the ledger knows
44
+ * them. `absolute`: the absolute paths it names (`C:\…\sessions`), which can name the sessions' logs — the ledger counts
45
+ * the sessions whose log is there, and a session a hex id names by the start of its native id. The organizing plan's
46
+ * entries are read the same way (`readCloselyOf`, `planRank`).
47
+ */
48
+ export function namedInBrief(markdown: string, fromRoot: ((name: string) => boolean) | null = FROM_ROOT): { paths: string[]; commits: string[]; absolute: string[] } {
49
+ const text = markdown.replace(/https?:\/\/\S+/g, ' ');
50
+ const paths = new Set<string>();
51
+ for (const m of text.matchAll(/(?<![\w./@-])((?:[\w.-]+\/)+[\w.-]*|[\w.-]+\.(?:md|markdown|txt|ts|tsx|js|jsx|mjs|cjs|dart|py|go|rs|java|kt|swift|json|ya?ml|toml|sh))(?![\w/-])/g)) {
52
+ const p = m[1]!.replace(/[.,;:)]+$/, '');
53
+ if (p.length > 1 && !/^\.+\/?$/.test(p)) paths.add(p);
54
+ }
55
+ const commits = new Set<string>();
56
+ for (const m of text.matchAll(/(?<![\w/-])([0-9a-f]{7,40})(?![\w/-])/g)) if (/[a-f]/.test(m[1]!) && /\d/.test(m[1]!)) commits.add(m[1]!);
57
+ const absolute = new Set<string>();
58
+ for (const m of text.matchAll(/(?<![\w])([A-Za-z]:[\\/](?:[\w.$-]+[\\/]?)+)/g)) absolute.add(m[1]!.replace(/[\\/.]+$/, ''));
59
+ // Where paths start at `/` (macOS, Linux): `/Users/sam/.claude/projects/-Users-sam-orchard` is one. A `/` that starts
60
+ // a word, at least two names, the first of them at the file system's root — `/api/users` is a route, not a place.
61
+ if (fromRoot) {
62
+ for (const m of text.matchAll(/(?<![\w.:~$}/-])(\/(?:[\w.$@+-]+\/)+[\w.$@+-]*)/g)) {
63
+ const path = m[1]!.replace(/[/.]+$/, '');
64
+ if (fromRoot(path.split('/')[1] ?? '')) absolute.add(path);
65
+ }
66
+ }
67
+ return { paths: [...paths], commits: [...commits], absolute: [...absolute] };
68
+ }
69
+ /** Whether a name is at the file system's root, on a system whose paths start there; null on Windows, where they start at a drive. */
70
+ const FROM_ROOT: ((name: string) => boolean) | null = sep === '/' ? (name) => name !== '' && existsSync(`/${name}`) : null;
71
+
72
+ // ───────────────────────── the organizing plan: what is read closely, and what first (§3.7, D62) ─────────────────────────
73
+ //
74
+ // The plan orientation's round sets, which the owner sees in Project scope and corrects in conversation (Spec §3.7; D62,
75
+ // the owner: 「之后的整理照它执行」). What it says to read closely is planned by the coverage check and counted by the depth
76
+ // question, under Focused too.
77
+
78
+ /** The organizing plan's id: one plan per project (§3.7). */
79
+ export const ORGANIZING_PLAN_ID = 'organizing-plan';
80
+
81
+ /** The kind of question a category of material is read for when no brief names it: the inverse of `KIND_CATEGORY`. */
82
+ const CATEGORY_KIND = Object.fromEntries(Object.entries(KIND_CATEGORY).map(([kind, category]) => [category, kind])) as Readonly<Record<ReadingCategory, SweepKind>>;
83
+
84
+ /** A deepening's materials as the ledger names them: document versions (with their paths), code files (with their lines), commits, sessions. */
85
+ export interface NamedMaterials {
86
+ readonly versions: readonly { readonly key: string; readonly path: string }[];
87
+ readonly codeFiles: readonly { readonly key: string; readonly lines: number }[];
88
+ readonly commits: readonly string[];
89
+ readonly sessions: readonly string[];
90
+ }
91
+
92
+ /** What the organizing plan says to read closely, as the ledger has it (`readCloselyOf`). */
93
+ export interface ReadClosely extends NamedMaterials {
94
+ /** Each material's entry in the plan, in the plan's words (a commit by `commit:<hash>`): what it is read closely for. */
95
+ readonly what: ReadonlyMap<string, string>;
96
+ /** Targets the ledger has nothing for: the steps still get them, in words, in the plan block of their prompts. */
97
+ readonly unresolved: readonly string[];
98
+ }
99
+
100
+ const NO_MATERIALS: NamedMaterials = { versions: [], codeFiles: [], commits: [], sessions: [] };
101
+ /** A path the plan or a brief names, written one way: with `/`, and in the form names are kept in (a name copied from a listing on a Mac is in another). */
102
+ const slashed = (p: string): string => nameForm(p.trim()).replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
103
+ const isAbsolutePath = (p: string): boolean => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('/') || p.startsWith('\\\\');
104
+ /** A relative path at or under a directory (or the file itself), whatever its slashes and case. */
105
+ const atOrUnder = (path: string, dir: string): boolean => {
106
+ const d = slashed(dir).toLowerCase();
107
+ const p = slashed(path).toLowerCase();
108
+ return d.length > 0 && (p === d || p.startsWith(`${d}/`));
109
+ };
110
+ /** A target as the plan writes it, without the quotes, backticks and trailing punctuation around it. */
111
+ const bareTarget = (t: string): string => t.trim().replace(/^[`'"“”‘’「」]+|[`'"“”‘’「」]+$/g, '').replace(/[.,;:)]+$/, '').trim();
112
+
113
+ /** The folder every repository of the ledger is in — where the plan's relative paths start; null when they share none. */
114
+ function commonRoot(repos: readonly { readonly path: string }[]): string | null {
115
+ const split = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
116
+ if (!repos.length) return null;
117
+ const first = split(repos[0]!.path);
118
+ let n = first.length;
119
+ for (const r of repos.slice(1)) {
120
+ const parts = split(r.path);
121
+ let i = 0;
122
+ while (i < n && i < parts.length && parts[i]!.toLowerCase() === first[i]!.toLowerCase()) i++;
123
+ n = i;
124
+ }
125
+ // A bare drive, or the filesystem's root, is no project's folder.
126
+ const root = first.slice(0, n);
127
+ return root.length >= 2 && !(root.length === 2 && root[0] === '' && root[1] === '') ? root.join('/') : null;
128
+ }
129
+
130
+ /**
131
+ * The paths a target can be inside the ledger's repositories: as the plan writes it (relative to its repository, the way a
132
+ * single-repository project names its files), and — taken relative to the project's folder, or absolute — as the path in
133
+ * the repository that holds it (a nested `app/` repository's `src/…` for `app/src/…`).
134
+ */
135
+ function targetPaths(repos: readonly { readonly path: string }[], root: string | null, target: string): string[] {
136
+ const out = new Set<string>();
137
+ const absolute = isAbsolutePath(target);
138
+ if (!absolute) out.add(slashed(target));
139
+ const abs = absolute ? target : root ? join(root, ...slashed(target).split('/')) : null;
140
+ if (abs) for (const r of repos) if (isWithin(r.path, abs)) { const rel = relativeDisplay(r.path, abs); if (rel) out.add(rel); }
141
+ return [...out].filter(Boolean);
142
+ }
143
+
144
+ /**
145
+ * The commits a branch holds that the trunk did not have — a branch named as the project names it, `feature/x`, or a
146
+ * remote's `origin/feature/x` by what follows its remote: those still only on it, or, merged, those its merge brought in.
147
+ */
148
+ function branchCommits(ledger: Ledger, name: string): string[] {
149
+ let rows: { repo: string; name: string; tip: string }[];
150
+ try {
151
+ rows = (ledger.db.prepare('SELECT repo, name, tip FROM branches').all() as { repo: string; name: string; tip: string }[]).filter((b) => b.name === name || b.name.endsWith(`/${name}`));
152
+ } catch { return []; }
153
+ if (!rows.length) return [];
154
+ const all = ledger.commitsByHash(ledger.allCommits());
155
+ const groupOf = new Map(all.map((c) => [c.hash, c.group]));
156
+ const groups = new Set<string>();
157
+ for (const b of rows) {
158
+ const own = `branch:${b.repo}:${b.name}`;
159
+ if (all.some((c) => c.group === own)) groups.add(own);
160
+ else { const g = groupOf.get(b.tip); if (g?.startsWith('merge:')) groups.add(g); }
161
+ }
162
+ return all.filter((c) => groups.has(c.group)).map((c) => c.hash);
163
+ }
164
+
165
+ /** One target through the ledger: a path's document versions and code files, a commit, a branch's commits, the sessions whose log is there or whose id it begins. */
166
+ function resolveTarget(ledger: Ledger, repos: readonly { readonly path: string }[], root: string | null, target: string): NamedMaterials {
167
+ const t = bareTarget(target);
168
+ if (!t) return NO_MATERIALS;
169
+ const hash = /^[0-9a-f]{7,40}$/i.test(t);
170
+ const named = ledger.named({ paths: hash ? [] : targetPaths(repos, root, t), commits: hash ? [t] : [] });
171
+ let sessions: string[] = [];
172
+ try { sessions = ledger.namedSessions(isAbsolutePath(t) ? [t] : [], /^[\w-]{8,}$/.test(t) ? [t] : []); } catch { sessions = []; }
173
+ const found = named.versions.length + named.codeFiles.length + named.commits.length + sessions.length > 0;
174
+ return { versions: named.versions, codeFiles: named.codeFiles, commits: found || isAbsolutePath(t) ? named.commits : branchCommits(ledger, t), sessions };
175
+ }
176
+
177
+ /**
178
+ * What the organizing plan says to read closely (Spec §3.7, D62), resolved through the ledger to the materials a deepening
179
+ * plans. Each entry's targets — a directory or file as the project names it, a commit, a branch, a session's log or id; an
180
+ * entry with no targets, by the paths and commits its words name. What the project's rules settle is left out (§1.15:
181
+ * judged by the rule, not read closely) — the directories its material rules settle, and what the plan itself says a rule
182
+ * settles — unless the target itself points into it.
183
+ */
184
+ export function readCloselyOf(ledger: Ledger, store: ProjectStore, plan: OrganizingPlanContent | null | undefined = store.plans.get(ORGANIZING_PLAN_ID)): ReadClosely {
185
+ const versions = new Map<string, string>();
186
+ const code = new Map<string, number>();
187
+ const commits = new Set<string>();
188
+ const sessions = new Set<string>();
189
+ const what = new Map<string, string>();
190
+ const unresolved: string[] = [];
191
+ const result = (): ReadClosely => ({ versions: [...versions].map(([key, path]) => ({ key, path })), codeFiles: [...code].map(([key, lines]) => ({ key, lines })), commits: [...commits], sessions: [...sessions], what, unresolved });
192
+ if (!plan?.readClosely.length) return result();
193
+ const repos = ledger.repos();
194
+ const root = commonRoot(repos);
195
+ const repoIds = repos.map((r) => r.id).sort((a, b) => b.length - a.length);
196
+ const settling = [...settlingDirs(store), ...plan.byRule.filter((e) => store.rules.get(e.ruleId)?.validity === 'Current').flatMap((e) => e.targets)];
197
+ for (const entry of plan.readClosely) {
198
+ const inWords = entry.targets.length ? null : namedInBrief(entry.what);
199
+ const targets = inWords ? [...inWords.paths, ...inWords.commits, ...inWords.absolute] : entry.targets;
200
+ // An entry in words alone: the steps have it in the plan block, and orientation names its material in a brief.
201
+ if (!targets.length) { unresolved.push(entry.what); continue; }
202
+ for (const raw of targets) {
203
+ const found = resolveTarget(ledger, repos, root, raw);
204
+ if (!found.versions.length && !found.codeFiles.length && !found.commits.length && !found.sessions.length) { unresolved.push(raw); continue; }
205
+ const own = targetPaths(repos, root, bareTarget(raw));
206
+ const settled = (rel: string) => settling.some((d) => atOrUnder(rel, d) && !own.some((t) => atOrUnder(t, d)));
207
+ const note = (key: string) => { if (!what.has(key)) what.set(key, entry.what); };
208
+ for (const v of found.versions) if (!settled(v.path)) { versions.set(v.key, v.path); note(v.key); }
209
+ for (const f of found.codeFiles) {
210
+ const repo = repoIds.find((id) => f.key.startsWith(`${id}:`));
211
+ if (settled(repo ? f.key.slice(repo.length + 1) : f.key)) continue;
212
+ code.set(f.key, f.lines);
213
+ note(f.key);
214
+ }
215
+ for (const c of found.commits) { commits.add(c); note(`commit:${c}`); }
216
+ for (const s of found.sessions) { sessions.add(s); note(s); }
217
+ }
218
+ }
219
+ return result();
220
+ }
221
+
222
+ /**
223
+ * `readCloselyOf` for the plan as it stands, kept while the ledger connection, the plan and the rules stay the same: the
224
+ * coverage and a deepening's reading ask after every job, and a target the ledger has no path for is looked up among
225
+ * the branches, which reads every commit. A rebuilt ledger is a new connection, so what it adds is found.
226
+ */
227
+ const closelySeen = new WeakMap<Ledger, Map<string, ReadClosely>>();
228
+ export function readCloselyNow(ledger: Ledger, store: ProjectStore, plan: OrganizingPlan | null | undefined = store.plans.get(ORGANIZING_PLAN_ID)): ReadClosely {
229
+ if (!plan?.readClosely.length) return readCloselyOf(ledger, store, plan);
230
+ const rules = store.rules.all().reduce((at, r) => (r.updatedAt > at ? r.updatedAt : at), '');
231
+ // What resolving reads: the entries and what the rules settle.
232
+ const key = `${store.dir}\0${JSON.stringify([plan.readClosely, plan.byRule])}\0${store.rules.size}\0${rules}`;
233
+ let seen = closelySeen.get(ledger);
234
+ if (!seen) { seen = new Map(); closelySeen.set(ledger, seen); }
235
+ let hit = seen.get(key);
236
+ if (!hit) {
237
+ if (seen.size >= 16) seen.clear();
238
+ hit = readCloselyOf(ledger, store, plan);
239
+ seen.set(key, hit);
240
+ }
241
+ return hit;
242
+ }
243
+
244
+ /** A path as what the plan says to read closely is placed on it (`closelyPath`). */
245
+ export interface PathSlot {
246
+ /** Its kinds of question: those it is counted whole for, or those its name begins with. */
247
+ readonly kinds: readonly SweepKind[];
248
+ /** What its brief names, by material key (a commit's `commit:<hash>`). */
249
+ readonly named: ReadonlySet<string>;
250
+ /** The categories it plans whole (a path counted by the ledger's totals for its kinds of question). */
251
+ readonly whole: ReadonlySet<ReadingCategory>;
252
+ /** The groups it plans material of already (a document's versions stay on one path). */
253
+ readonly groups?: ReadonlySet<string>;
254
+ }
255
+
256
+ /**
257
+ * The path a material the plan says to read closely goes on (Spec §3.7: each target in the brief of the right path): the
258
+ * one that plans the rest of its document already; else one that plans it already — its brief names it, or it plans the
259
+ * material's whole category (what the rules settle, `settled`, is outside that); else the first path of its kind of
260
+ * question — a document version the document chain's, a code file the code's, a commit the process and checks', a
261
+ * session the owner's meaning's; else the first path. -1 with no path.
262
+ */
263
+ export function closelyPath(slots: readonly PathSlot[], m: { readonly key: string; readonly category: ReadingCategory; readonly group?: string; readonly settled?: boolean }): number {
264
+ if (m.group) { const i = slots.findIndex((s) => s.groups?.has(m.group!)); if (i >= 0) return i; }
265
+ const plans = slots.findIndex((s) => s.named.has(m.key) || (!m.settled && s.whole.has(m.category)));
266
+ if (plans >= 0) return plans;
267
+ const kind = CATEGORY_KIND[m.category];
268
+ const ofKind = slots.findIndex((s) => s.kinds.includes(kind));
269
+ return ofKind >= 0 ? ofKind : slots.length ? 0 : -1;
270
+ }
271
+
272
+ /** How a Focused deepening from before D99 counted its paths (§3.7): only what the organizing plan says to read closely was planned. */
273
+ export const FOCUSED_READING = 'Focused: only what the organizing plan says to read closely is planned on this path, to be read in full; the sweep answers the rest of its brief at the Focused depth';
274
+ /** A reading planned for a Focused deepening: its paths say so. */
275
+ export const isFocusedReading = (reading: RoundReading | null | undefined): boolean => Boolean(reading?.paths.some((p) => p.basis.startsWith(FOCUSED_READING)));
276
+
277
+ // ───────────────────────── what was read of each material ─────────────────────────
278
+
279
+ /** How far a material was read: `measure` grows with every line, message or commit more that reached a step. */
280
+ export interface MaterialRead {
281
+ readonly outcome: 'whole' | 'part' | 'none';
282
+ readonly measure: number;
283
+ /** A file, a version or a session read in part: what of it was read (lines, or message positions), and of how many. */
284
+ readonly ranges?: readonly [number, number | null][];
285
+ readonly of?: number | null;
286
+ }
287
+
288
+ const NONE: MaterialRead = { outcome: 'none', measure: 0 };
289
+ const WHOLE = 1e12;
290
+
291
+ function mergedReads(list: readonly FileReads[]): FileReads | null {
292
+ if (!list.length) return null;
293
+ return {
294
+ whole: list.some((f) => f.whole), ranges: list.flatMap((f) => f.ranges), lines: list.reduce<number | null>((n, f) => (f.lines === null ? n : Math.max(n ?? 0, f.lines)), null), part: list.some((f) => f.part),
295
+ };
296
+ }
297
+ const coveredCount = (ranges: readonly (readonly [number, number | null])[], total: number | null): number =>
298
+ mergeRanges(ranges).reduce((n, [a, b]) => n + Math.max(0, (b ?? total ?? a) - a + 1), 0);
299
+ const within = (need: readonly (readonly [number, number])[], have: readonly (readonly [number, number | null])[]): boolean => {
300
+ const merged = mergeRanges(have);
301
+ return need.every(([a, b]) => merged.some(([x, y]) => x <= a && (y === null || y >= b)));
302
+ };
303
+
304
+ /**
305
+ * What a tally read of each planned material (the program's count, from the steps' recorded reads). A version older than
306
+ * the next counts as read whole when its own lines were read and the version after it was read whole.
307
+ */
308
+ export function readsOf(materials: readonly ReadingMaterial[], tally: ReadTally): Map<string, MaterialRead> {
309
+ const byKey = new Map(materials.map((m) => [m.key, m]));
310
+ const out = new Map<string, MaterialRead>();
311
+ const fileReads = (m: ReadingMaterial) => (m.file ? tally.files.get(pathKey(m.file)) ?? null : null);
312
+ const historyReads = (m: ReadingMaterial) => (m.file && m.rev ? mergedReads((tally.versions.get(pathKey(m.file)) ?? []).filter((v) => sameCommit(v.rev, m.rev!)).map((v) => v.reads)) : null);
313
+ const of = (m: ReadingMaterial): MaterialRead => {
314
+ const had = out.get(m.key);
315
+ if (had) return had;
316
+ out.set(m.key, NONE); // a cycle of `next` (none is made) reads as nothing
317
+ let r: MaterialRead = NONE;
318
+ if (m.category === 'commits') {
319
+ r = m.rev && [...tally.commits].some((c) => sameCommit(c, m.rev!)) ? { outcome: 'whole', measure: WHOLE } : NONE;
320
+ } else if (m.category === 'sessions') {
321
+ const s = m.session ? tally.sessions.get(m.session) ?? null : null;
322
+ const log = fileReads(m);
323
+ const total = m.messages ?? s?.lines ?? null;
324
+ if (log?.whole || (s && (s.whole || (s.ranges.length > 0 && coversAll(s.ranges, total))))) r = { outcome: 'whole', measure: WHOLE };
325
+ else if (s && (s.ranges.length || s.part)) r = { outcome: 'part', measure: Math.max(1, coveredCount(s.ranges, total)), ranges: mergeRanges(s.ranges), of: total };
326
+ else if (log && (log.ranges.length || log.part)) r = { outcome: 'part', measure: 1 };
327
+ } else {
328
+ const reads = [m.current || m.category === 'code files' ? fileReads(m) : null, m.category === 'document versions' ? historyReads(m) : null].filter((x): x is FileReads => x !== null);
329
+ const all = mergedReads(reads);
330
+ const lines = m.lines ?? all?.lines ?? null;
331
+ const whole = reads.some((f) => f.whole || (f.ranges.length > 0 && coversAll(f.ranges, f.lines ?? m.lines ?? null)));
332
+ const next = m.next ? byKey.get(m.next) : undefined;
333
+ const own = historyReads(m);
334
+ const throughNext = Boolean(m.diff && next && of(next).outcome === 'whole' && (m.diff.length === 0 || (own && (own.whole || within(m.diff, own.ranges)))));
335
+ if (whole || throughNext) r = { outcome: 'whole', measure: WHOLE };
336
+ else if (all && (all.ranges.length || all.part)) r = { outcome: 'part', measure: Math.max(1, coveredCount(all.ranges, lines)), ranges: mergeRanges(all.ranges), of: lines };
337
+ }
338
+ out.set(m.key, r);
339
+ return r;
340
+ };
341
+ for (const m of materials) of(m);
342
+ return out;
343
+ }
344
+
345
+ // ───────────────────────── the jobs whose reads count ─────────────────────────
346
+
347
+ /** A job and every job it sent, and theirs: what a step read includes what it sent others to read. */
348
+ export function withDescendants(store: ProjectStore, jobs: readonly KeeperJob[]): KeeperJob[] {
349
+ const out = [...jobs];
350
+ const seen = new Set(out.map((j) => j.id));
351
+ for (let i = 0; i < out.length; i++) {
352
+ for (const c of store.jobs.filter((j) => j.parentJobId === out[i]!.id)) if (!seen.has(c.id)) { seen.add(c.id); out.push(c); }
353
+ }
354
+ return out;
355
+ }
356
+
357
+ /** The deep-sweep jobs of a path in a round from before D99 (its assignments, whatever became of them). */
358
+ export function pathJobs(store: ProjectStore, roundId: string, path: string): KeeperJob[] {
359
+ return store.jobs.filter((j) => j.step?.roundId === roundId && j.step.kind === 'dig' && (j.step.path ?? j.scope.label) === path);
360
+ }
361
+
362
+ /** A path's basis, when the organizing plan changed while a round from before D99 ran and its reading took it up (`readingBasisText`). */
363
+ const PLAN_CHANGED = ' · while the round ran, ';
364
+
365
+ // ───────────────────────── the path's reading, counted ─────────────────────────
366
+
367
+ /** A path's reading of a deepening from before D99, as the Keeper view shows it: planned, read whole, recorded in part or not read with why, open, its assignments. */
368
+ export function pathReadingOf(store: ProjectStore, project: Project, round: ClerkRound, p: RoundReadingPath, known?: ReadonlyMap<string, MaterialRead>): PathReading {
369
+ const reads = known ?? readsOf(p.materials, tallyReads(withDescendants(store, pathJobs(store, round.id, p.path)), project));
370
+ const accounted = new Map(p.accounted.map((a) => [a.key, a]));
371
+ const labelOf = new Map(p.materials.map((m) => [m.key, m.label]));
372
+ let whole = 0, part = 0, notRead = 0, open = 0;
373
+ for (const m of p.materials) {
374
+ if (reads.get(m.key)?.outcome === 'whole') whole++;
375
+ else if (accounted.get(m.key)?.outcome === 'part') part++;
376
+ else if (accounted.get(m.key)?.outcome === 'none') notRead++;
377
+ else open++;
378
+ }
379
+ const status = (a: ReadingAssignment) => (a.jobId ? store.jobs.get(a.jobId)?.status ?? 'Queued' : 'Queued');
380
+ const count = (pred: (s: string) => boolean) => p.assignments.filter((a) => pred(status(a))).length;
381
+ return {
382
+ planned: p.materials.length, whole, part, notRead, open,
383
+ assignments: {
384
+ total: p.assignments.length, done: count((s) => s === 'Done'), running: count((s) => s === 'Running'), queued: count((s) => s === 'Queued' || s === 'Paused' || s === 'Waiting for quota'),
385
+ failed: count((s) => s === 'Failed' || s === 'Stopped'), followUps: p.assignments.filter((a) => a.followUpOf.length > 0).length,
386
+ },
387
+ accounted: p.accounted.filter((a) => reads.get(a.key)?.outcome !== 'whole').map((a) => ({ key: a.key, label: labelOf.get(a.key) ?? a.key, outcome: a.outcome, why: a.why, by: a.by })),
388
+ };
389
+ }
390
+
391
+ /**
392
+ * The words under the progress table of a deepening from before D99: how its reading was counted, how the assignments were
393
+ * sized, how the organizing plan steered it (§3.7, D62), and what a change of the plan while the round ran added.
394
+ */
395
+ export function readingBasisText(reading: RoundReading): string {
396
+ const planned = isFocusedReading(reading)
397
+ ? `Under Focused only what the organizing plan says to read closely is planned — each on its path, split into reading assignments that run at once on the lanes — and each sweep answers the rest of its brief at the Focused depth; ${reading.budget.basis}.`
398
+ : `Each path's planned material is split into reading assignments that run at once on the lanes; ${reading.budget.basis}.`;
399
+ const changed = reading.paths.filter((p) => p.basis.includes(PLAN_CHANGED)).map((p) => `${p.path}: ${p.basis.split(PLAN_CHANGED).slice(1).join('; ')}`);
400
+ return `${planned} A material counts as read whole when a step's recorded reads show all of it — an older version of a document when its own lines (what it changed against the version after it) were read and that version was read whole. What an assignment did not read goes into a follow-up; a follow-up that reads nothing more of its list ends the chase, and what it left is recorded as read in part or not read, with why. The cross-check starts once every planned material is read whole or recorded so. The reading follows the organizing plan as it stands at each pass: what it says to read closely is planned and read in full — on the path that plans the rest of it, else the one whose brief names it, else the first of its kind of question — and its focus, then its order, decide which assignments are queued first; a change of the plan while the round runs applies to what is not queued yet.${changed.length ? ` Taken up while this round ran — ${changed.join(' · ')}.` : ''}`;
401
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The blocks every job of a round is given (D99): the round and the organizing plan it follows, and the project's rules
3
+ * in force. The planner gives them to the main job; `pk_send_lanes` gives them to each lane with its skill and its brief.
4
+ */
5
+ import type { ClerkRound, LaneKind, SlotKind } from '../../model/k-types.ts';
6
+ import type { Project } from '../../model/types.ts';
7
+ import type { ProjectStore } from '../../store/project-store.ts';
8
+ import { organizingPlanBlock } from './clerk-prompts.ts';
9
+ import { ORGANIZING_PLAN_ID } from './reading.ts';
10
+ import { laneSkill } from './skills.ts';
11
+ import { ownerLinesBlock } from './owner-lines.ts';
12
+ import type { Ledger } from '../../ledger/index.ts';
13
+
14
+ /**
15
+ * The round, and the organizing plan it follows (Spec §3.7; D62, the owner: 「之后的整理照它执行」) as it stands when the
16
+ * job is queued: what the rules settle, what is read closely, the focus, the order, and each of the owner's corrections
17
+ * in their words — those since the last round began marked. A correction made while the round runs so reaches every job
18
+ * queued after it (D97).
19
+ */
20
+ export function roundBlockOf(store: ProjectStore, project: Project, round: ClerkRound): string {
21
+ const previous = store.clerkRounds.filter((r) => r.id !== round.id && r.status === 'Done').sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
22
+ const lines = [
23
+ `This is ${round.kind === 'Follow up' ? 'Follow up' : 'takeover'} round ${round.number} (${round.kind}) of ${project.name}; round id ${round.id}.`,
24
+ ...(round.kind === 'Follow up' && previous ? [`What changed since ${previous.startedAt} is this round's scope.`] : []),
25
+ ...(round.kind === 'Deepen' ? [`Depth chosen by the owner: ${project.takeoverDepth ?? 'Full'}.`] : []),
26
+ `The project's language: ${project.language}. Write what you write in it; fixed workbench words stay in English.`,
27
+ ];
28
+ const plan = store.plans.get(ORGANIZING_PLAN_ID);
29
+ const planBlock = plan ? organizingPlanBlock({ plan, rule: (id) => store.rules.get(id)?.summary ?? null, since: previous?.startedAt ?? null }) : null;
30
+ return [`=== This round\n${lines.join('\n')}`, ...(planBlock ? [planBlock] : [])].join('\n\n');
31
+ }
32
+
33
+ /** The project's rules in force, one line each. */
34
+ export function rulesBlockOf(store: ProjectStore): string {
35
+ return store.rules.filter((r) => r.validity === 'Current')
36
+ .map((r) => `- ${r.id} [${r.group}${r.category ? ` · ${r.category}` : ''} · ${r.basis}${r.ownerConfirmation ? ' · confirmed by the owner' : ''}] ${r.summary}${r.appliesTo.length ? ` (applies to: ${r.appliesTo.join(', ')})` : ''}`)
37
+ .join('\n');
38
+ }
39
+
40
+ /**
41
+ * A lane's task (D99, E148 D-b): its skill in its prompt — GLM may not open a skill file by itself — then its brief, the
42
+ * round and the project's rules. The first line names it, so its job and its Report are told apart from the others'.
43
+ */
44
+ export function lanePrompt(input: { readonly name: string; readonly kind: LaneKind; readonly slots: readonly SlotKind[]; readonly brief: string; readonly briefDocId: string; readonly roundBlock: string; readonly rules: string; readonly blocks?: readonly string[] }): string {
45
+ return [
46
+ `Task: a lane of this round — ${input.name} (${input.kind}). The main agent sent you with the brief below (${input.briefDocId}). Write the slots it gives you — ${input.slots.length ? input.slots.join(', ') : 'none: you report only'} — each with its source, and your Report (pk_write_round_doc kind Report). End with a short summary of what you found and wrote: the main agent reads it first.`,
47
+ laneSkill(input.kind),
48
+ `=== Your brief\n${input.brief}`,
49
+ input.roundBlock,
50
+ ...(input.blocks ?? []),
51
+ `=== The project's rules in force\n${input.rules || 'None recorded yet.'}`,
52
+ ].join('\n\n');
53
+ }
54
+
55
+ /** The slot of the lane that writes the owner's words (CM): it is given the owner's lines in full. */
56
+ export const OWNER_WORDS_SLOT = "reference:Owner's words";
57
+
58
+ /**
59
+ * The program's lists a lane is given with its brief, by its slots (CM, E151): the lane that writes the `Owner's words`
60
+ * items gets every owner's line no position cites yet, verbatim (the main agent gets only their count).
61
+ */
62
+ export function laneBlocksOf(store: ProjectStore, slots: readonly SlotKind[], ledger: Ledger | null, since: string | null): string[] {
63
+ if (!slots.some((s) => s.toLowerCase() === OWNER_WORDS_SLOT.toLowerCase())) return [];
64
+ try { return [ownerLinesBlock(store, ledger, { since })]; } catch (e) { return [`=== The owner's lines: the program could not list them (${(e as Error).message}); read the session drafts (pk_read_assets kind draft).`]; }
65
+ }