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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/README.zh-CN.md +285 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/bin/pk.mjs +6 -0
- package/bin/postinstall.mjs +5 -0
- package/bin/strip-types.mjs +28 -0
- package/package.json +68 -4
- package/patches/README.md +111 -0
- package/patches/pi-ai-0.85.0.ts +122 -0
- package/patches/pi-ai-0.87.1.ts +122 -0
- package/scripts/apply-pi-streaming-patch.ts +86 -0
- package/skills/clerk/coverage/SKILL.md +35 -0
- package/skills/clerk/cross-check/SKILL.md +118 -0
- package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
- package/skills/clerk/lane-follow-up/SKILL.md +94 -0
- package/skills/clerk/lane-plan/SKILL.md +107 -0
- package/skills/clerk/lane-slot/SKILL.md +113 -0
- package/skills/clerk/lane-topic/SKILL.md +116 -0
- package/skills/clerk/orientation/SKILL.md +160 -0
- package/skills/clerk/reconcile/SKILL.md +77 -0
- package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
- package/skills/clerk/spot-check/SKILL.md +52 -0
- package/skills/clerk/synthesis/SKILL.md +65 -0
- package/skills/table-follow-up/SKILL.md +42 -0
- package/src/cli.ts +319 -0
- package/src/codemap/README.md +13 -0
- package/src/codemap/engines.ts +102 -0
- package/src/codemap/facts.ts +504 -0
- package/src/context/agent-entry.ts +232 -0
- package/src/context/ask.ts +97 -0
- package/src/context/assemble.ts +1058 -0
- package/src/context/code-source.ts +103 -0
- package/src/context/k-briefs.ts +114 -0
- package/src/context/lineage.ts +100 -0
- package/src/context/object-brief.ts +300 -0
- package/src/context/owner-words.ts +191 -0
- package/src/context/pack-fixture.ts +342 -0
- package/src/context/project-rules.ts +160 -0
- package/src/intake/intake.ts +392 -0
- package/src/intake/material-rules.ts +166 -0
- package/src/intake/removal.ts +316 -0
- package/src/intake/skipped.ts +72 -0
- package/src/keeper/adjustment.ts +565 -0
- package/src/keeper/bounds/boundary.ts +509 -0
- package/src/keeper/bounds/command.ts +1665 -0
- package/src/keeper/bounds/paths.ts +164 -0
- package/src/keeper/bounds/reads.ts +718 -0
- package/src/keeper/bounds/shell-write-guard.ts +521 -0
- package/src/keeper/capabilities.ts +136 -0
- package/src/keeper/clerk-steps.ts +151 -0
- package/src/keeper/clerk-tools.ts +1424 -0
- package/src/keeper/conversation.ts +253 -0
- package/src/keeper/evidence.ts +662 -0
- package/src/keeper/fake-provider.ts +132 -0
- package/src/keeper/held.ts +15 -0
- package/src/keeper/history-tools.ts +163 -0
- package/src/keeper/jobs/answering.ts +53 -0
- package/src/keeper/key-check.ts +105 -0
- package/src/keeper/ledger-tools.ts +295 -0
- package/src/keeper/link-check.ts +185 -0
- package/src/keeper/merge.ts +260 -0
- package/src/keeper/note-origin.ts +56 -0
- package/src/keeper/numbers-check.ts +102 -0
- package/src/keeper/open-in-pi.ts +57 -0
- package/src/keeper/organize/absence.ts +266 -0
- package/src/keeper/organize/carried-on.ts +214 -0
- package/src/keeper/organize/clerk-coverage.ts +293 -0
- package/src/keeper/organize/clerk-prompts.ts +132 -0
- package/src/keeper/organize/clerk.ts +1395 -0
- package/src/keeper/organize/coverage-gate.ts +19 -0
- package/src/keeper/organize/coverage-tools.ts +765 -0
- package/src/keeper/organize/entries.ts +153 -0
- package/src/keeper/organize/entry-gate.ts +289 -0
- package/src/keeper/organize/fill-tools.ts +718 -0
- package/src/keeper/organize/follow-up.ts +280 -0
- package/src/keeper/organize/generation-check.ts +504 -0
- package/src/keeper/organize/graph.ts +230 -0
- package/src/keeper/organize/lane-tools.ts +289 -0
- package/src/keeper/organize/looked-tools.ts +54 -0
- package/src/keeper/organize/materials.ts +243 -0
- package/src/keeper/organize/owner-lines.ts +493 -0
- package/src/keeper/organize/place-tools.ts +216 -0
- package/src/keeper/organize/placement-inference.ts +1045 -0
- package/src/keeper/organize/placement-time.ts +320 -0
- package/src/keeper/organize/placing.ts +821 -0
- package/src/keeper/organize/positioning.ts +33 -0
- package/src/keeper/organize/readiness.ts +47 -0
- package/src/keeper/organize/reading.ts +401 -0
- package/src/keeper/organize/round-blocks.ts +65 -0
- package/src/keeper/organize/round-news.ts +164 -0
- package/src/keeper/organize/round-open.ts +586 -0
- package/src/keeper/organize/schedule-state.ts +39 -0
- package/src/keeper/organize/schedule.ts +137 -0
- package/src/keeper/organize/service.ts +409 -0
- package/src/keeper/organize/skills.ts +116 -0
- package/src/keeper/organize/slots.ts +85 -0
- package/src/keeper/organize/stage-gate.ts +175 -0
- package/src/keeper/organize/stage-tools.ts +471 -0
- package/src/keeper/organize/standing-notes.ts +141 -0
- package/src/keeper/organize/table-follow-up.ts +330 -0
- package/src/keeper/organize/takeover-clerk.ts +832 -0
- package/src/keeper/organize/takeover-state.ts +103 -0
- package/src/keeper/organize/takeover.ts +216 -0
- package/src/keeper/organize/update-pending.ts +535 -0
- package/src/keeper/organize/workbench-placement.ts +92 -0
- package/src/keeper/own-keys.ts +114 -0
- package/src/keeper/owner-text.ts +165 -0
- package/src/keeper/project-folder.ts +334 -0
- package/src/keeper/prompts.ts +101 -0
- package/src/keeper/references.ts +177 -0
- package/src/keeper/relook/compose.ts +120 -0
- package/src/keeper/relook/prompts.ts +36 -0
- package/src/keeper/roles.ts +259 -0
- package/src/keeper/round-tools.ts +227 -0
- package/src/keeper/route.ts +148 -0
- package/src/keeper/rules.ts +97 -0
- package/src/keeper/runtime.ts +1858 -0
- package/src/keeper/scope-tools.ts +194 -0
- package/src/keeper/sendback-text.ts +83 -0
- package/src/keeper/step-timing.ts +207 -0
- package/src/keeper/testing/clerk-script.ts +212 -0
- package/src/keeper/testing/streaming-arguments.ts +57 -0
- package/src/keeper/tools.ts +2340 -0
- package/src/keeper/utterance-tools.ts +64 -0
- package/src/keeper/verbatim.ts +397 -0
- package/src/ledger/adapters.ts +186 -0
- package/src/ledger/arrangements.ts +257 -0
- package/src/ledger/blame.ts +58 -0
- package/src/ledger/code-engine-child.ts +69 -0
- package/src/ledger/code-engine.ts +548 -0
- package/src/ledger/code.ts +607 -0
- package/src/ledger/docs.ts +294 -0
- package/src/ledger/git-read.ts +321 -0
- package/src/ledger/index.ts +1850 -0
- package/src/ledger/item-range.ts +16 -0
- package/src/ledger/lines.ts +328 -0
- package/src/ledger/numbering.ts +269 -0
- package/src/ledger/ranges.ts +74 -0
- package/src/ledger/rebuild-worker.ts +15 -0
- package/src/ledger/rebuild.ts +387 -0
- package/src/ledger/repo-scan.ts +181 -0
- package/src/ledger/schema.ts +450 -0
- package/src/ledger/sessions.ts +134 -0
- package/src/ledger/text-scan.ts +466 -0
- package/src/ledger/time.ts +185 -0
- package/src/ledger/ts-symbols.ts +180 -0
- package/src/ledger/views.ts +377 -0
- package/src/mcp/server.ts +127 -0
- package/src/mcp/tools.ts +187 -0
- package/src/model/ids.ts +23 -0
- package/src/model/k-types.ts +912 -0
- package/src/model/time.ts +85 -0
- package/src/model/types.ts +1382 -0
- package/src/model/views-k.ts +707 -0
- package/src/model/vocab.ts +269 -0
- package/src/process/analysis.ts +79 -0
- package/src/process/breakpoint-candidates.ts +383 -0
- package/src/process/breakpoints.ts +746 -0
- package/src/process/claim-check.ts +295 -0
- package/src/process/delivery-links.ts +52 -0
- package/src/process/facts.ts +487 -0
- package/src/process/generations.ts +138 -0
- package/src/process/index.ts +243 -0
- package/src/process/integration.ts +128 -0
- package/src/process/placement.ts +107 -0
- package/src/process/plan.ts +276 -0
- package/src/process/sendbacks.ts +244 -0
- package/src/process/text.ts +185 -0
- package/src/process/units.ts +543 -0
- package/src/process/unnumbered.ts +431 -0
- package/src/process/work.ts +884 -0
- package/src/scope/candidates.ts +86 -0
- package/src/scope/decisions.ts +229 -0
- package/src/scope/discover.ts +741 -0
- package/src/scope/ignore.ts +126 -0
- package/src/scope/ignored-place.ts +93 -0
- package/src/scope/skip.ts +161 -0
- package/src/scope/toolchain.ts +180 -0
- package/src/scope/worktree.ts +187 -0
- package/src/server/api.ts +549 -0
- package/src/server/app.ts +595 -0
- package/src/server/drafts-api.ts +170 -0
- package/src/server/folders-api.ts +243 -0
- package/src/server/graph-view.ts +639 -0
- package/src/server/http.ts +224 -0
- package/src/server/k-agent-api.ts +56 -0
- package/src/server/k-rounds-d99.ts +91 -0
- package/src/server/k-views.ts +405 -0
- package/src/server/keeper-page.ts +446 -0
- package/src/server/keys-api.ts +239 -0
- package/src/server/project-folder-api.ts +104 -0
- package/src/server/vendor.ts +14 -0
- package/src/server/workbench-content.ts +561 -0
- package/src/sources/anchor.ts +129 -0
- package/src/sources/files.ts +267 -0
- package/src/sources/gitobs.ts +41 -0
- package/src/sources/history.ts +231 -0
- package/src/sources/scan-worker.ts +10 -0
- package/src/sources/sessions/locate.ts +292 -0
- package/src/sources/sessions/read.ts +346 -0
- package/src/sources/sessions/scope.ts +42 -0
- package/src/sources/sessions/utterances.ts +223 -0
- package/src/sources/watch.ts +215 -0
- package/src/store/json-file.ts +50 -0
- package/src/store/paths.ts +39 -0
- package/src/store/project-store.ts +295 -0
- package/src/store/versions.ts +172 -0
- package/src/store/workspace.ts +216 -0
- package/src/table/check.ts +333 -0
- package/src/table/follow-up-tools.ts +30 -0
- package/src/table/follow-up.ts +344 -0
- package/src/table/import.ts +610 -0
- package/src/table/project-home.ts +61 -0
- package/src/table/sessions.ts +168 -0
- package/src/table/takeover.ts +483 -0
- package/src/util/git-check.ts +29 -0
- package/src/util/git.ts +411 -0
- package/src/util/own-ts.ts +15 -0
- package/src/util/paths.ts +225 -0
- package/template/FILLING-IN.md +148 -0
- package/template/examples/papertrail.json +747 -0
- package/template/projectkeeper.schema.json +1001 -0
- package/ui/app.js +1135 -0
- package/ui/failures.js +82 -0
- package/ui/flyout.js +120 -0
- package/ui/folder-chooser.js +264 -0
- package/ui/glossary.js +156 -0
- package/ui/graph-fit.js +157 -0
- package/ui/graph-material.js +61 -0
- package/ui/graph-palette.js +109 -0
- package/ui/graph-tools.js +62 -0
- package/ui/graph.js +1712 -0
- package/ui/index.html +31 -0
- package/ui/k/code-view.js +230 -0
- package/ui/k/draft-view.js +93 -0
- package/ui/k/k-views.css +224 -0
- package/ui/k/round-tree.js +450 -0
- package/ui/k/scope-k.js +159 -0
- package/ui/k-data.js +118 -0
- package/ui/k-fold.js +106 -0
- package/ui/k-process.css +302 -0
- package/ui/k-process.js +934 -0
- package/ui/keeper-activity.js +175 -0
- package/ui/keeper-view.js +801 -0
- package/ui/keeper.css +101 -0
- package/ui/key-order.js +62 -0
- package/ui/list-fold.js +67 -0
- package/ui/markdown.d.ts +21 -0
- package/ui/markdown.js +472 -0
- package/ui/placement.d.ts +92 -0
- package/ui/placement.js +445 -0
- package/ui/popover-place.d.ts +16 -0
- package/ui/popover-place.js +89 -0
- package/ui/popover.js +198 -0
- package/ui/project-folder.js +37 -0
- package/ui/shell-state.js +40 -0
- package/ui/strip-rows.js +171 -0
- package/ui/styles.css +492 -0
- package/ui/theme-pick.js +36 -0
- package/ui/theme.js +74 -0
- package/ui/themes/LICENSE.md +13 -0
- package/ui/themes/README.md +44 -0
- package/ui/themes/TOKENS.md +291 -0
- package/ui/themes/a1/board.css +1557 -0
- package/ui/themes/a1/pk.css +392 -0
- package/ui/themes/b1/board.css +1163 -0
- package/ui/themes/b1/pk.css +218 -0
- package/ui/themes/d2/board.css +518 -0
- package/ui/themes/d2/pk.css +243 -0
- package/ui/themes/guard.css +73 -0
- package/ui/themes/i1/board.css +776 -0
- package/ui/themes/i1/pk.css +223 -0
- package/ui/themes/manifest.json +13 -0
- package/ui/views.js +2796 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which pk_* tools a Keeper job is offered (Spec §3.3).
|
|
3
|
+
*
|
|
4
|
+
* A step of a clerk-method round gets its step's tools: pi's built-ins, every read, every ledger query, and the writes
|
|
5
|
+
* its step owns (`stepToolsFor`; "跨对象的记录由哪一步写"), so one matter is written once, by the step that owns it. Any
|
|
6
|
+
* other job is either a subagent — an investigation another job sent — which reads, and writes only what concerns its own
|
|
7
|
+
* material, or other work — the owner's conversation, an answer, a request, a re-look — which is offered every tool except
|
|
8
|
+
* those only a round's step has a round for (`toolsFor`).
|
|
9
|
+
*
|
|
10
|
+
* This is kept by the tools a job is given, not by asking the model: a subagent is never offered a tool that writes across
|
|
11
|
+
* objects. When several kinds of independent job wrote the work items at once, the same work came out several times,
|
|
12
|
+
* because nothing held the round (Spec §3.3, D62 background).
|
|
13
|
+
*
|
|
14
|
+
* The D59 method — a round claimed by one main job, whose role and round kind gated its tools — was replaced by the clerk
|
|
15
|
+
* method (Spec v3.0 §3.3); its gating is gone. `roundKindOf` still recognises such a job for the two readers outside this
|
|
16
|
+
* file that ask (see there).
|
|
17
|
+
*/
|
|
18
|
+
import type { KeeperJob } from '../model/types.ts';
|
|
19
|
+
import type { ClerkRound, RoundStepKind } from '../model/k-types.ts';
|
|
20
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
21
|
+
import { clerkWritersOf, LANE_ALWAYS, MAIN_ALWAYS, SLOT_WRITERS, STAGE_WRITERS, SYNTHESIS_ALWAYS, SYNTHESIS_WRITERS } from './clerk-steps.ts';
|
|
22
|
+
import { gatedTool, MAIN_FOLLOW_UP_WRITES, stageOfRound } from './organize/stage-gate.ts';
|
|
23
|
+
|
|
24
|
+
/** The kinds of round of the D59 method: the takeover's first round, a deepening round, a Follow up. */
|
|
25
|
+
export type RoundKind = 'frame' | 'deepen' | 'follow-up';
|
|
26
|
+
export type JobRole = 'subagent' | 'other';
|
|
27
|
+
|
|
28
|
+
interface RoundExtra { readonly kind?: string; readonly round?: string; readonly roundId?: string }
|
|
29
|
+
const extraOf = (job: Pick<KeeperJob, 'task'> | null | undefined): RoundExtra | null => ((job?.task as { extra?: RoundExtra } | null)?.extra ?? null);
|
|
30
|
+
|
|
31
|
+
/** The rounds of the D59 method, by the kind their jobs carry (`task.extra.kind`). */
|
|
32
|
+
export const REPLACED_ROUND_KINDS: ReadonlySet<string> = new Set(['takeover', 'round', 'materials']);
|
|
33
|
+
/** A job of a D59 round: the clerk method replaced them, and their prompts name tools that no longer exist. */
|
|
34
|
+
export const isReplacedRound = (job: Pick<KeeperJob, 'task' | 'step'>): boolean => !job.step && REPLACED_ROUND_KINDS.has(extraOf(job)?.kind ?? '');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The kind of D59 round a job is the main job of, or null. No code creates such a job any more: the clerk method replaced
|
|
38
|
+
* them, a waiting one is stopped when the Keeper starts, and a stopped or failed one is not continued (runtime.ts
|
|
39
|
+
* `stopReplacedRounds`, `restartJob`). What still asks, every path of it dead in the running program:
|
|
40
|
+
* - organize/service.ts `relookAfterRequest` (a re-look waits while such a round runs) and round-tools.ts
|
|
41
|
+
* `pk_ask_owner_about_rules` (a D59 main job's rules note), through `isRoundMain`;
|
|
42
|
+
* - tools.ts `roundOf` (a D59 Follow up main job judging and writing `pk_write_round_result`), and this file's
|
|
43
|
+
* `judgesFollowUp` and `takeoverRoundOf`. The judgement tests still judge through such a job (adjustment-rules,
|
|
44
|
+
* propagation-rules, round-owner, takeover-judgement, tools-validation, replaced-rounds, frame-subagent, method-tools),
|
|
45
|
+
* so these stay until those tests judge through a clerk round's cross-check.
|
|
46
|
+
*/
|
|
47
|
+
export function roundKindOf(job: Pick<KeeperJob, 'task' | 'parentJobId'> | null | undefined): RoundKind | null {
|
|
48
|
+
if (!job || job.parentJobId) return null;
|
|
49
|
+
const x = extraOf(job);
|
|
50
|
+
if (x?.kind === 'round') return 'follow-up';
|
|
51
|
+
if (x?.kind === 'takeover') return x.round === 'deepen' ? 'deepen' : 'frame';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const isRoundMain = (job: Pick<KeeperJob, 'task' | 'parentJobId'> | null | undefined): boolean => roundKindOf(job) !== null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The clerk-method round a job belongs to (Spec v3.0 §3.3): a step's own round, or the round whose root job this is.
|
|
59
|
+
* Null for work outside the clerk method.
|
|
60
|
+
*/
|
|
61
|
+
export function clerkRoundOfJob(store: ProjectStore, job: Pick<KeeperJob, 'task' | 'step'> | null | undefined): ClerkRound | null {
|
|
62
|
+
const x = extraOf(job);
|
|
63
|
+
const id = job?.step?.roundId ?? (x?.kind === 'clerk-round' || x?.kind === 'clerk-step' ? x.roundId : undefined);
|
|
64
|
+
return id ? store.clerkRounds.get(id) ?? null : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The Follow up round record (`store.rounds`) a job judges in by the clerk method (Spec v3.0 §3.3, §5.5): a Follow up
|
|
69
|
+
* round opens its record when it begins, and its cross-check records each piece of work's net change and judges each
|
|
70
|
+
* object the changes reached, once, into that record. The round's one result is counted when the round closes. Null
|
|
71
|
+
* for every other step and for work outside the clerk method.
|
|
72
|
+
*/
|
|
73
|
+
export function clerkFollowUpRecordOf(store: ProjectStore, job: Pick<KeeperJob, 'task' | 'step'> | null | undefined): string | null {
|
|
74
|
+
const round = clerkRoundOfJob(store, job);
|
|
75
|
+
// D99: the main job judges in its cross-check stage (its effective stage).
|
|
76
|
+
const crossCheck = job?.step?.kind === 'cross-check' || (job?.step?.kind === 'main' && !!round && stageOfRound(round) === 'cross-check');
|
|
77
|
+
return round?.kind === 'Follow up' && crossCheck ? round.followUpRoundId : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether a job judges in a Follow up round: the round's cross-check (§5.5). A D59 Follow up main job still counts here,
|
|
82
|
+
* for tools.ts's round and request paths — no such job runs any more; the tests that judge through one are listed in
|
|
83
|
+
* `roundKindOf`'s note.
|
|
84
|
+
*/
|
|
85
|
+
export function judgesFollowUp(store: ProjectStore, job: Pick<KeeperJob, 'task' | 'parentJobId' | 'step'> | null | undefined): boolean {
|
|
86
|
+
return roundKindOf(job) === 'follow-up' || clerkFollowUpRecordOf(store, job) !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A job with a parent is a subagent of the work that sent it, whatever it was sent to do. A step of a round has its
|
|
90
|
+
* round's root as parent, and gets its step's tools instead (`stepToolsFor`). */
|
|
91
|
+
export function jobRole(job: Pick<KeeperJob, 'task' | 'parentJobId'>): JobRole {
|
|
92
|
+
return job.parentJobId ? 'subagent' : 'other';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a job is part of taking the project over: a step of the first usable round or of a deepening, or anything under
|
|
97
|
+
* one. Such a job never opens or joins a Follow up round (§5.5: propagation is judged in the Follow up after the takeover).
|
|
98
|
+
*/
|
|
99
|
+
export function isTakeoverWork(store: ProjectStore, jobId: string | null | undefined): boolean {
|
|
100
|
+
return takeoverRoundOf(store, jobId) !== null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The takeover round a job belongs to — the clerk round of the first usable picture or of a deepening (or, before the
|
|
105
|
+
* clerk method, a takeover round's main job) — or null for work outside the takeover. What such work writes names this
|
|
106
|
+
* round, never a Follow up round.
|
|
107
|
+
*/
|
|
108
|
+
export function takeoverRoundOf(store: ProjectStore, jobId: string | null | undefined): string | null {
|
|
109
|
+
let job = jobId ? store.jobs.get(jobId) : undefined;
|
|
110
|
+
for (let depth = 0; job && depth < 8; depth++) {
|
|
111
|
+
const clerk = clerkRoundOfJob(store, job);
|
|
112
|
+
if (clerk) return clerk.kind === 'Follow up' ? null : clerk.id;
|
|
113
|
+
if (job.scope.kind === 'takeover' || extraOf(job)?.kind === 'takeover') return job.id;
|
|
114
|
+
job = job.parentJobId ? store.jobs.get(job.parentJobId) : undefined;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What a subagent may use: reading the assets and the sources, recording how a source is used, and the fact records
|
|
121
|
+
* of its own material. An allowlist on purpose: a tool added later that writes across objects is kept from subagents
|
|
122
|
+
* until someone decides otherwise. A subagent sends no subagents of its own: subagents do not wait on one another
|
|
123
|
+
* (§3.3), and what else needs reading is for the job that sent it to hand out.
|
|
124
|
+
*/
|
|
125
|
+
export const SUBAGENT_TOOLS: ReadonlySet<string> = new Set([
|
|
126
|
+
'pk_project_overview', 'pk_list_sources', 'pk_read_source', 'pk_read_assets', 'pk_find_references', 'pk_set_used_as', 'pk_write_fact_record',
|
|
127
|
+
// Version history is read like any material; what it records is a History only source of that reading alone.
|
|
128
|
+
'pk_history_log', 'pk_history_read', 'pk_history_deleted',
|
|
129
|
+
// A claim is checked against the code by the subagent that read the report, and the check is recorded next to the
|
|
130
|
+
// claim in that report's fact record (Spec §2.4; CKC-06 AC-23).
|
|
131
|
+
'pk_record_code_check',
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* What only a Follow up round's cross-check does (§2.10, §3.3, §5.5; CKC-11 AC-25): one state per object per round, and
|
|
135
|
+
* what its own changes reached. The owner's conversation, an answer, a request or a re-look has no round of its own: a
|
|
136
|
+
* judgement from one of them used to open a round, judge in it and close it with a result beside the round. An owner's
|
|
137
|
+
* correction is recorded on the object itself and judged by the next Follow up round; the owner who wants that now
|
|
138
|
+
* presses Follow up (§3.8). `pk_write_round_result` is offered to no job at all: the clerk method counts the round's one
|
|
139
|
+
* result when the round closes (organize/clerk.ts); the tool stays for the tests that judge through a D59 main job.
|
|
140
|
+
*/
|
|
141
|
+
export const FOLLOW_UP_TOOLS: ReadonlySet<string> = new Set(['pk_judge_object', 'pk_set_propagation', 'pk_set_propagations', 'pk_round_pending', 'pk_write_round_result']);
|
|
142
|
+
/** Putting the rules the round inferred to the owner in the round's one note (§3.9): only a round's synthesis has a
|
|
143
|
+
* round to write that note for. */
|
|
144
|
+
export const ROUND_SYNTHESIS_TOOLS: ReadonlySet<string> = new Set(['pk_ask_owner_about_rules']);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The tools a job that is not a step of a round is offered. pi's own tools (read, grep, bash …) are not pk_* tools and
|
|
148
|
+
* pass through (§3.1). A subagent reads, and writes only its own material's fact records; other work gets every pk_* tool
|
|
149
|
+
* except those only a round's step has a round for.
|
|
150
|
+
*/
|
|
151
|
+
export function toolsFor<T extends { readonly name: string }>(tools: readonly T[], role: JobRole): T[] {
|
|
152
|
+
return tools.filter((t) => {
|
|
153
|
+
if (!t.name.startsWith('pk_')) return true;
|
|
154
|
+
// The ledger's tools only read (§1.16): a subagent asks them as any step does (CKC-03 AC-26).
|
|
155
|
+
if (role === 'subagent') return SUBAGENT_TOOLS.has(t.name) || t.name.startsWith('pk_ledger_');
|
|
156
|
+
return !FOLLOW_UP_TOOLS.has(t.name) && !ROUND_SYNTHESIS_TOOLS.has(t.name);
|
|
157
|
+
}).map((t) => (t.name === OWNER_WORDS_TOOL ? countedOwnerWords(t) : t));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ───────────────────────── how much of the owner's words a job read (Spec §3.7; D37; CKC-13 AC-25) ─────────────────────────
|
|
161
|
+
|
|
162
|
+
const OWNER_WORDS_TOOL = 'pk_owner_utterances';
|
|
163
|
+
|
|
164
|
+
type ToolResult = { content?: { type: string; text?: string }[] } & Record<string, unknown>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* pk_owner_utterances with how much it read leading its answer: "Read 50 of the owner’s 60 messages (9000 of 11000
|
|
168
|
+
* characters)". The first round's owner's-words step is evidence for D37 — how long it took and how much the owner
|
|
169
|
+
* said — and the job's steps are where the runtime keeps each call, so the count is put where the step keeps it. The
|
|
170
|
+
* answer itself follows unchanged.
|
|
171
|
+
*/
|
|
172
|
+
function countedOwnerWords<T extends { readonly name: string }>(tool: T): T {
|
|
173
|
+
const run = (tool as unknown as { execute?: (...args: unknown[]) => Promise<ToolResult> }).execute;
|
|
174
|
+
if (typeof run !== 'function') return tool;
|
|
175
|
+
return {
|
|
176
|
+
...tool,
|
|
177
|
+
execute: async (...args: unknown[]) => {
|
|
178
|
+
const result = await run.apply(tool, args);
|
|
179
|
+
const first = result?.content?.[0];
|
|
180
|
+
if (!first || typeof first.text !== 'string') return result;
|
|
181
|
+
let page: { utterances?: { chars?: number; text?: string }[]; total?: number; totalChars?: number };
|
|
182
|
+
try { page = JSON.parse(first.text) as typeof page; } catch { return result; }
|
|
183
|
+
if (!Array.isArray(page.utterances)) return result;
|
|
184
|
+
const chars = page.utterances.reduce((n, u) => n + (typeof u.chars === 'number' ? u.chars : (u.text ?? '').length), 0);
|
|
185
|
+
const line = `Read ${page.utterances.length} of the owner’s ${page.total ?? page.utterances.length} messages (${chars} of ${page.totalChars ?? chars} characters)`;
|
|
186
|
+
return { ...result, content: [{ ...first, text: `${line} ${first.text}` }, ...(result.content ?? []).slice(1)] };
|
|
187
|
+
},
|
|
188
|
+
} as T;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ───────────────────────── the clerk method: what each step may write (Spec v3.0 §3.3) ─────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Reading is open to every step (CKC-23 AC-5): every pk_* read tool and every ledger tool (`pk_ledger_*`), besides pi's
|
|
195
|
+
* built-ins, which are never filtered here. What a step may write follows §3.3 "跨对象的记录由哪一步写": orientation the
|
|
196
|
+
* layers, rules, generations and the round's questions and briefs; the skeleton the trunk, numbers and patch drafts; a
|
|
197
|
+
* deep sweep only its report; the cross-check the positions it has checked; the synthesis the notes, send-backs, the
|
|
198
|
+
* six things, area understanding and the round's Result (D103: in a job of its own, after the main agent's handover);
|
|
199
|
+
* the spot-check its corrections and its record. So one matter is written once, by the step that owns it.
|
|
200
|
+
*/
|
|
201
|
+
const PK_READ_TOOLS: ReadonlySet<string> = new Set([
|
|
202
|
+
'pk_project_overview', 'pk_list_sources', 'pk_read_source', 'pk_read_assets', 'pk_find_references', 'pk_owner_utterances',
|
|
203
|
+
'pk_history_log', 'pk_history_read', 'pk_history_deleted', 'pk_scope_items',
|
|
204
|
+
]);
|
|
205
|
+
const TRUNK_WRITES = ['pk_write_reference', 'pk_write_thread', 'pk_write_area', 'pk_relate', 'pk_write_mark', 'pk_merge_work_items', 'pk_record_carry_out', 'pk_check_decisions', 'pk_set_used_as'];
|
|
206
|
+
const FOLLOW_UP_WRITES = ['pk_write_change', 'pk_judge_object', 'pk_set_propagation', 'pk_set_propagations', 'pk_write_modification_request', 'pk_mark_request_handled', 'pk_round_pending'];
|
|
207
|
+
/** The writers from before the clerk method, by step; the clerk method's own writers come from their one table. */
|
|
208
|
+
const EARLIER_WRITES: Readonly<Record<RoundStepKind, readonly string[]>> = {
|
|
209
|
+
ledger: [],
|
|
210
|
+
// D99 (W1+W2): the main agent is offered every stage's writers, gated by its stage; a lane by its slots.
|
|
211
|
+
main: [],
|
|
212
|
+
lane: [],
|
|
213
|
+
'session-drafts': [],
|
|
214
|
+
orientation: ['pk_write_rule', 'pk_classify_scope', 'pk_set_used_as', 'pk_write_organizing_plan'],
|
|
215
|
+
skeleton: TRUNK_WRITES,
|
|
216
|
+
dig: [],
|
|
217
|
+
'cross-check': [...TRUNK_WRITES, 'pk_write_rule'],
|
|
218
|
+
process: [],
|
|
219
|
+
// The round's result is its Result document; a Follow up's counted result is closed with the round (clerk.ts).
|
|
220
|
+
// D103: the synthesis is a job of its own again; what it is offered is one table (`SYNTHESIS_WRITERS`), with where the
|
|
221
|
+
// round stands (`SYNTHESIS_ALWAYS`).
|
|
222
|
+
synthesis: [...SYNTHESIS_WRITERS, ...SYNTHESIS_ALWAYS],
|
|
223
|
+
'spot-check': [...TRUNK_WRITES, 'pk_write_note'],
|
|
224
|
+
};
|
|
225
|
+
export const STEP_WRITES: Readonly<Record<string, ReadonlySet<string>>> = Object.fromEntries(
|
|
226
|
+
(Object.entries(EARLIER_WRITES) as [RoundStepKind, readonly string[]][]).map(([step, earlier]) => [step, new Set([...earlier, ...clerkWritersOf(step)])]),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* What the main job (D99) and a lane write, offered whole to their one session (E148 D-a): the main job every stage's
|
|
231
|
+
* writers, and in a Follow up round the round's judgements; a lane its slots' writers and its Report.
|
|
232
|
+
*/
|
|
233
|
+
// D103: the main agent is no longer offered what only the synthesis writes — `STAGE_WRITERS.synthesis` is empty.
|
|
234
|
+
function d99Writes(step: string, round: 'First usable' | 'Deepen' | 'Follow up', slots: readonly string[]): Set<string> {
|
|
235
|
+
if (step === 'main') return new Set([...MAIN_ALWAYS, ...Object.values(STAGE_WRITERS).flat(), ...(round === 'Follow up' ? MAIN_FOLLOW_UP_WRITES : [])]);
|
|
236
|
+
const base = (s: string) => (s.startsWith('reference:') ? 'reference' : s.split(':')[0]!);
|
|
237
|
+
return new Set([...LANE_ALWAYS, ...slots.flatMap((s) => SLOT_WRITERS[base(s) as keyof typeof SLOT_WRITERS] ?? [])]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The tools a step of a clerk-method round is offered: pi's built-ins, every read, and the writes its step owns. The main
|
|
242
|
+
* job and a lane (D99) are offered what they write in any stage or slot; `gate` then refuses at call time what the job's
|
|
243
|
+
* effective stage does not allow (stage-gate.ts `writeRefusal`), so each writer still writes only where it belongs.
|
|
244
|
+
*/
|
|
245
|
+
export function stepToolsFor<T extends { readonly name: string }>(tools: readonly T[], step: string, round: 'First usable' | 'Deepen' | 'Follow up',
|
|
246
|
+
lane: { readonly slots: readonly string[] } | null = null, gate: ((tool: string, args: Record<string, unknown> | null) => string | null) | null = null): T[] {
|
|
247
|
+
const d99 = step === 'main' || step === 'lane';
|
|
248
|
+
const writes = d99 ? d99Writes(step, round, lane?.slots ?? []) : STEP_WRITES[step] ?? new Set<string>();
|
|
249
|
+
const followUp = round === 'Follow up' && step === 'cross-check';
|
|
250
|
+
return tools.filter((t) => {
|
|
251
|
+
if (!t.name.startsWith('pk_')) return true;
|
|
252
|
+
if (t.name.startsWith('pk_ledger_') || PK_READ_TOOLS.has(t.name)) return true;
|
|
253
|
+
return writes.has(t.name) || (followUp && FOLLOW_UP_WRITES.includes(t.name));
|
|
254
|
+
}).map((t) => {
|
|
255
|
+
const counted = t.name === OWNER_WORDS_TOOL ? countedOwnerWords(t) : t;
|
|
256
|
+
const reads = !t.name.startsWith('pk_') || t.name.startsWith('pk_ledger_') || PK_READ_TOOLS.has(t.name);
|
|
257
|
+
return d99 && gate && !reads ? gatedTool(counted as T & { execute?: (...args: never[]) => unknown }, gate) : counted;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round tools besides the pk_* asset tools (Spec §3.3): what a Follow up round still has to judge (pk_round_pending), a
|
|
3
|
+
* claim checked against the code (pk_record_code_check), and the rules a round inferred, put to the owner
|
|
4
|
+
* (pk_ask_owner_about_rules). Which job is offered which tool is decided in roles.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { Type } from 'typebox';
|
|
7
|
+
import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
import type { CodeCheck, JudgementRecord, Note, NoteVersion, Source, Statement } from '../model/types.ts';
|
|
9
|
+
import { newId } from '../model/ids.ts';
|
|
10
|
+
import type { ToolContext } from './tools.ts';
|
|
11
|
+
import { clerkRoundOfJob, isRoundMain } from './roles.ts';
|
|
12
|
+
import { cameFromFor } from './note-origin.ts';
|
|
13
|
+
import { laneNamesOf, ownerTextRefusal } from './owner-text.ts';
|
|
14
|
+
import { deriveGraph } from './organize/graph.ts';
|
|
15
|
+
import { pendingForRound } from './organize/follow-up.ts';
|
|
16
|
+
import { projectRelPath } from './organize/materials.ts';
|
|
17
|
+
import { isDocumentPath } from '../scope/skip.ts';
|
|
18
|
+
|
|
19
|
+
/** The order claims about the code are checked in (Spec §2.4, D63; CKC-06 AC-23) — an order, not a limit. */
|
|
20
|
+
export const CHECK_TIER = ["Owner's words or red line", 'Done or verified', 'Other'] as const;
|
|
21
|
+
/** What a check of a claim against the code found. */
|
|
22
|
+
export const CHECK_RESULT = ['Consistent', 'Inconsistent', 'Partly consistent'] as const;
|
|
23
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|kts|swift|c|cc|cpp|cxx|h|hh|hpp|cs|rb|php|scala|sh|bash|ps1|psm1|sql|vue|svelte|dart|lua|m|mm|r|jl|ex|exs|erl|hs|clj|fs|ml|zig|nim|sol|proto|gradle|json|jsonc|ya?ml|toml|ini|cfg|conf|xml|html?|css|scss|less)$/i;
|
|
24
|
+
/** Code, tests and the configuration they read: what a claim about the code's behaviour is checked against. */
|
|
25
|
+
function isCode(s: Source): boolean {
|
|
26
|
+
if (s.usedAs === 'Code' || s.usedAs === 'Test') return true;
|
|
27
|
+
const path = s.anchor.kind === 'file' || s.anchor.kind === 'revision' ? s.anchor.path : null;
|
|
28
|
+
return path !== null && CODE_EXT.test(path) && !isDocumentPath(path);
|
|
29
|
+
}
|
|
30
|
+
const squash = (s: string) => s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
31
|
+
|
|
32
|
+
const text = (v: unknown, fallback = ''): string => (typeof v === 'string' ? v : fallback);
|
|
33
|
+
const arr = <T>(v: unknown): T[] => (Array.isArray(v) ? (v as T[]) : []);
|
|
34
|
+
const ok = (value: unknown) => ({ content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value, null, 1) }], details: {} });
|
|
35
|
+
const fail = (message: string) => ({ content: [{ type: 'text' as const, text: `ERROR: ${message}` }], details: {}, isError: true });
|
|
36
|
+
|
|
37
|
+
export function roundTools(ctx: ToolContext): ToolDefinition[] {
|
|
38
|
+
return [
|
|
39
|
+
defineTool({
|
|
40
|
+
name: 'pk_round_pending', label: 'What this round still has to judge',
|
|
41
|
+
description: 'After recording the changes of this round\'s material (pk_write_change), the objects they reached are found by rule. This lists every object the round still has to judge — the ones given with the round and the ones your changes reached — each with the items that reached it and what it still lacks. Objects already judged in this round are not listed again.',
|
|
42
|
+
parameters: Type.Object({}),
|
|
43
|
+
execute: async () => {
|
|
44
|
+
// The downstream of a change is seeded when the graph is derived, which otherwise happens only after a job
|
|
45
|
+
// ends: without this, what the round's own changes reached would wait for the next round.
|
|
46
|
+
deriveGraph(ctx.store, ctx.project);
|
|
47
|
+
const pending = pendingForRound(ctx.store);
|
|
48
|
+
return ok(pending.objects ? `${pending.objects} object${pending.objects === 1 ? '' : 's'} still to judge in this round:\n${pending.text}` : 'Nothing is left to judge in this round.');
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
codeCheckTool(ctx),
|
|
52
|
+
rulesToOwnerTool(ctx),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `pk_record_code_check` (Spec §2.4, D63; CKC-06 AC-23): a claim about what the code does, checked by reading the code,
|
|
58
|
+
* recorded next to the claim in the report's fact record — an Observed statement anchored to the code (repository,
|
|
59
|
+
* commit, file, lines), with which claim it checks, in which order and what it found. The claim itself stays as it
|
|
60
|
+
* was reported. How many claims were checked, in which order and how many did not hold is counted from these.
|
|
61
|
+
* Checking the same claim again replaces the earlier check.
|
|
62
|
+
*
|
|
63
|
+
* Where a check hangs is not settled by the PA yet (asked); this is the most conservative reading: on the fact record
|
|
64
|
+
* that holds the claim, beside it, changing nothing else. What spans objects — the contradicts relation, the Suspected
|
|
65
|
+
* stale mark on the status that rests on the claim — is the main job's to write, and the result tells it so.
|
|
66
|
+
*/
|
|
67
|
+
function codeCheckTool(ctx: ToolContext): ToolDefinition {
|
|
68
|
+
const { store } = ctx;
|
|
69
|
+
return defineTool({
|
|
70
|
+
name: 'pk_record_code_check', label: 'Record a claim checked against the code',
|
|
71
|
+
description: `Record the check of one claim against the code, next to the claim, in the fact record that holds it. Read the code only — never run tests, re-do QC or grade anyone. Give the fact record, the claim (its statement id, or its text as recorded), its tier in the order claims are checked (${CHECK_TIER.join(' → ')}), the result (${CHECK_RESULT.join(', ')}), the code sources you read (codeSourceIds), the lines, and what the code does, in one or two sentences. The commit is the one the source was read at unless you name another. It is written as an Observed statement anchored to the code; the claim stays Claimed. Checking the same claim again replaces the earlier check.`,
|
|
72
|
+
parameters: Type.Object({
|
|
73
|
+
factRecordId: Type.String({ description: 'the fact record that holds the claim (the report’s)' }),
|
|
74
|
+
claim: Type.String({ description: 'the claim: its statement id, or its text as the fact record has it' }),
|
|
75
|
+
tier: Type.String({ description: CHECK_TIER.join(' | ') }),
|
|
76
|
+
result: Type.String({ description: CHECK_RESULT.join(' | ') }),
|
|
77
|
+
codeSourceIds: Type.Array(Type.String(), { description: 'the sources of the code you read' }),
|
|
78
|
+
lines: Type.String({ description: 'the lines you read, e.g. 40-58' }),
|
|
79
|
+
observed: Type.String({ description: 'what the code does, in one or two sentences' }),
|
|
80
|
+
file: Type.Optional(Type.String({ description: 'the file, when it is not the first code source’s own' })),
|
|
81
|
+
commit: Type.Optional(Type.String({ description: 'the commit you read the code at, when it is not the one the source was read at' })),
|
|
82
|
+
}),
|
|
83
|
+
execute: async (_id, p) => {
|
|
84
|
+
const record = store.facts.get(text(p.factRecordId).trim());
|
|
85
|
+
if (!record) return fail(`${text(p.factRecordId)} is not a fact record; give the id of the fact record that holds the claim (the report’s).`);
|
|
86
|
+
const given = text(p.claim).trim();
|
|
87
|
+
const claims = record.statements.filter((s) => s.type === 'Claimed');
|
|
88
|
+
const claim = claims.find((s) => s.id === given) ?? claims.find((s) => squash(s.text) === squash(given));
|
|
89
|
+
if (!claim) {
|
|
90
|
+
return fail(`No claim in ${record.id} reads “${given.slice(0, 160)}”. Name one of its Claimed statements by id or by its text as recorded${claims.length ? `: ${claims.slice(0, 12).map((s) => `${s.id} “${s.text.slice(0, 80)}”`).join('; ')}` : ' — it has none: what a report says it did is written there as Claimed first (pk_write_fact_record)'}.`);
|
|
91
|
+
}
|
|
92
|
+
if (!(CHECK_TIER as readonly string[]).includes(text(p.tier))) return fail(`tier must be one of ${CHECK_TIER.join(', ')} — the order claims are checked in: first what bears on the owner’s words and red lines, then what a Done or verified status rests on, then the rest.`);
|
|
93
|
+
if (!(CHECK_RESULT as readonly string[]).includes(text(p.result))) return fail(`result must be one of ${CHECK_RESULT.join(', ')}.`);
|
|
94
|
+
const ids = [...new Set(arr<string>(p.codeSourceIds).map((id) => text(id).trim()).filter(Boolean))];
|
|
95
|
+
if (ids.length === 0) return fail('Name the code you read (codeSourceIds): the source ids of the code files the check rests on.');
|
|
96
|
+
const sources: Source[] = [];
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
const s = store.sources.get(id);
|
|
99
|
+
if (!s) return fail(`codeSourceIds: unknown source ${id}`);
|
|
100
|
+
if (!isCode(s)) return fail(`${id} (${s.title}) is not code: a claim about what the code does is checked against the code itself; a document that describes it is another claim.`);
|
|
101
|
+
sources.push(s);
|
|
102
|
+
}
|
|
103
|
+
const lines = text(p.lines).trim();
|
|
104
|
+
if (!lines) return fail('Say which lines you read (lines, e.g. 40-58): the check is anchored to commit, file and lines.');
|
|
105
|
+
const observed = text(p.observed).trim();
|
|
106
|
+
if (!observed) return fail('Say what the code does (observed), in one or two sentences: that is the observation the check records.');
|
|
107
|
+
const first = sources[0]!;
|
|
108
|
+
const a = first.anchor;
|
|
109
|
+
const repo = a.kind === 'revision' ? a.repo : ctx.project.scope.find((i) => i.id === first.scopeItemId && (i.category === 'Repository' || i.category === 'Worktree'))?.path ?? ctx.project.locations[0] ?? null;
|
|
110
|
+
const file = text(p.file).trim() || (a.kind === 'revision' ? a.path : a.kind === 'file' ? projectRelPath(ctx.project, a.path).split('\\').join('/') : first.title);
|
|
111
|
+
const commit = text(p.commit).trim() || (a.kind === 'revision' ? a.commit : first.version.commit) || null;
|
|
112
|
+
const check: CodeCheck = { of: claim.id, tier: text(p.tier), result: text(p.result), anchor: { repo, commit, file, lines } };
|
|
113
|
+
const earlier = record.statements.findIndex((s) => s.check?.of === claim.id);
|
|
114
|
+
const statement: Statement = {
|
|
115
|
+
id: earlier >= 0 ? record.statements[earlier]!.id : newId('st'), type: 'Observed',
|
|
116
|
+
text: `${observed} (${file}:${lines}${commit ? ` at ${commit.slice(0, 10)}` : ''}; checks claim ${claim.id}: ${check.result})`,
|
|
117
|
+
sourceIds: ids, check,
|
|
118
|
+
};
|
|
119
|
+
const statements = earlier >= 0 ? record.statements.map((s, i) => (i === earlier ? statement : s)) : [...record.statements, statement];
|
|
120
|
+
store.facts.put({ ...record, statements, updatedAt: new Date().toISOString() }, { jobId: ctx.jobId, basisSourceIds: [...ids, ...claim.sourceIds], summary: `Code check of claim ${claim.id} (${check.tier}): ${check.result}` });
|
|
121
|
+
ctx.onSaved?.('facts', record.id, `Code check: ${check.result}`);
|
|
122
|
+
const held = check.result === 'Consistent';
|
|
123
|
+
return ok({
|
|
124
|
+
factRecordId: record.id, statementId: statement.id, claim: claim.id, tier: check.tier, result: check.result, anchor: check.anchor, replacedEarlierCheck: earlier >= 0,
|
|
125
|
+
next: held
|
|
126
|
+
? 'The claim holds as far as this code shows: the check is its observation, and the claim stays as reported.'
|
|
127
|
+
: `The claim does not hold${check.result === 'Partly consistent' ? ' in full' : ''}: the main job writes contradicts from the code source to the report’s source (the claim quoted in the relation’s claim), puts Suspected stale on the status that rests on it — its progress stays as reported — and adds the claim and this check to that work item’s executionFacts. Say it in your report.`,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `pk_ask_owner_about_rules` (Spec §1.15, §3.9; CKC-21 AC-5): the rules a round inferred from what the project does
|
|
135
|
+
* that change how an agent works, put to the owner in the round's one `For your decision` note — each with the records
|
|
136
|
+
* it rests on and what it changes. A later call in the same round adds to that note; a rule the project writes down,
|
|
137
|
+
* one the owner confirmed, or one an earlier round inferred is refused with the reason.
|
|
138
|
+
*/
|
|
139
|
+
function rulesToOwnerTool(ctx: ToolContext): ToolDefinition {
|
|
140
|
+
const { store } = ctx;
|
|
141
|
+
return defineTool({
|
|
142
|
+
name: 'pk_ask_owner_about_rules', label: 'Put the rules this round inferred to the owner',
|
|
143
|
+
description: 'The rules you inferred in this round from what the project does (basis Inferred) that change how an agent works on the project, put to the owner in the round’s one For your decision note: each rule with the records it rests on and what it changes for an agent. Call it at the end of the round with all of them; a later call in the same round adds to the same note. Until the owner confirms a rule it stays marked as your inference.',
|
|
144
|
+
parameters: Type.Object({
|
|
145
|
+
rules: Type.Array(Type.Object({ ruleId: Type.String(), changes: Type.String({ description: 'what the rule changes in how an agent works on the project, in one sentence' }) })),
|
|
146
|
+
}),
|
|
147
|
+
execute: async (_id, p) => {
|
|
148
|
+
const job = store.jobs.get(ctx.jobId);
|
|
149
|
+
// By the clerk method the synthesis puts the round's inferred rules to the owner: the rules its orientation and
|
|
150
|
+
// cross-check wrote are this round's (Spec v3.0 §3.3, §3.9). Before it, a round's main job did, with its own.
|
|
151
|
+
const synthesis = job?.step?.kind === 'synthesis' || (job?.step?.kind === 'main' && clerkRoundOfJob(store, job)?.stage === 'synthesis');
|
|
152
|
+
const clerk = synthesis ? clerkRoundOfJob(store, job) : null;
|
|
153
|
+
if (!job || (!isRoundMain(job) && !clerk)) return fail('Only a round’s synthesis (or, before the clerk method, a round’s main job) puts the rules the round inferred to the owner, in the round’s one note; outside a round there is no round note to put them in. Name them in your reply instead.');
|
|
154
|
+
const roundJobs = clerk ? new Set(store.jobs.filter((j) => j.step?.roundId === clerk.id).map((j) => j.id)) : new Set([ctx.jobId]);
|
|
155
|
+
const asked = arr<Record<string, unknown>>(p.rules).map((r) => ({ ruleId: text(r.ruleId).trim(), changes: text(r.changes).trim() }));
|
|
156
|
+
if (asked.length === 0) return fail('Name the rules you inferred in this round: rules: [{ ruleId, changes }].');
|
|
157
|
+
for (const q of asked) {
|
|
158
|
+
const rule = store.rules.get(q.ruleId);
|
|
159
|
+
if (!rule) return fail(`${q.ruleId} is not one of the project’s rules; use the id pk_write_rule returned.`);
|
|
160
|
+
if (rule.ownerConfirmation) return fail(`${rule.id} is already confirmed by the owner: there is nothing to ask.`);
|
|
161
|
+
if (rule.basis === 'Explicit') return fail(`${rule.id} is written in the project (basis Explicit): the project’s own rule needs no confirmation. Only the rules you inferred from what the project does are put to the owner.`);
|
|
162
|
+
if (rule.validity !== 'Current') return fail(`${rule.id} is ${rule.validity}: only a rule in force is put to the owner.`);
|
|
163
|
+
if (!roundJobs.has(rule.jobId ?? '')) return fail(`${rule.id} was not inferred in this round: a round puts to the owner the rules it inferred itself; an earlier round’s rules went into that round’s note.`);
|
|
164
|
+
if (!q.changes) return fail(`Say for ${rule.id} what it changes for an agent working on the project (changes).`);
|
|
165
|
+
}
|
|
166
|
+
const at = new Date().toISOString();
|
|
167
|
+
const noteId = `note_rules-${clerk ? clerk.id : ctx.jobId}`;
|
|
168
|
+
const previous = store.notes.get(noteId);
|
|
169
|
+
// The owner reads this note (§4.2, §6.13; D105): what a rule changes is said without the store's ids.
|
|
170
|
+
const unreadable = ownerTextRefusal('The note that puts the inferred rules to the owner', Object.fromEntries(asked.map((q, i) => [`rules[${i}].changes`, q.changes])), laneNamesOf(store));
|
|
171
|
+
if (unreadable) return fail(unreadable);
|
|
172
|
+
// One fact per rule: a rule asked about again takes the new wording, the others stay as they were. Which rule a
|
|
173
|
+
// fact is about is its link (`ruleId`), not a part of its sentence; a note written before D105 named the rule's id
|
|
174
|
+
// in the sentence, which is read once more here and left out of what is written.
|
|
175
|
+
const factOf = (ruleId: string, changes: string) => {
|
|
176
|
+
const r = store.rules.get(ruleId)!;
|
|
177
|
+
return { text: `${r.summary} — for an agent this means: ${changes} (applies to ${r.appliesTo.join('; ')}; inferred from the records cited)`, sourceIds: [...r.sourceIds], inferred: true, ruleId: r.id };
|
|
178
|
+
};
|
|
179
|
+
const facts = new Map<string, { text: string; sourceIds: string[]; inferred: boolean; ruleId: string }>();
|
|
180
|
+
for (const f of previous?.versions[previous.versions.length - 1]?.body.facts ?? []) {
|
|
181
|
+
const id = f.ruleId ?? /\(rule ([^;)\s]+)/.exec(f.text)?.[1];
|
|
182
|
+
if (id) facts.set(id, { text: f.text.replace(/\(rule [^;)\s]+; /, '('), sourceIds: [...f.sourceIds], inferred: f.inferred, ruleId: id });
|
|
183
|
+
}
|
|
184
|
+
for (const q of asked) facts.set(q.ruleId, factOf(q.ruleId, q.changes));
|
|
185
|
+
const ruleIds = [...facts.keys()];
|
|
186
|
+
let judgementId = ctx.judgementId && store.judgements.has(ctx.judgementId) ? ctx.judgementId : previous?.versions[0]?.judgementRecordId ?? '';
|
|
187
|
+
if (!store.judgements.has(judgementId)) {
|
|
188
|
+
const judgement: JudgementRecord = {
|
|
189
|
+
id: newId('jdg'), projectId: ctx.project.id, jobId: ctx.jobId, at,
|
|
190
|
+
scope: { kind: 'rules', ids: ruleIds, label: `Rules inferred in ${job.scope.label}` },
|
|
191
|
+
inputs: { referenceIds: [], threadIds: [], areaIds: [], relationIds: [], keyEvidenceSourceIds: [...new Set([...facts.values()].flatMap((f) => f.sourceIds))], conflictingSourceIds: [], previousNoteIds: [], investigations: [] },
|
|
192
|
+
excluded: [], outcome: { noteIds: [], assessments: [], reconsideredOnly: false },
|
|
193
|
+
};
|
|
194
|
+
store.judgements.put(judgement, { jobId: ctx.jobId, summary: `Inputs recorded: ${judgement.scope.label}` });
|
|
195
|
+
judgementId = judgement.id;
|
|
196
|
+
}
|
|
197
|
+
const n = ruleIds.length;
|
|
198
|
+
const version: NoteVersion = {
|
|
199
|
+
version: (previous?.versions.length ?? 0) + 1, at,
|
|
200
|
+
title: `Do the rules inferred from how the project works hold?`,
|
|
201
|
+
// The first sentence is the question (§4.2, D105); the title says the same.
|
|
202
|
+
preview: `${n === 1 ? 'Does the rule' : `Do the ${n} rules`} this round inferred from the project’s records hold? ${n === 1 ? 'It' : 'Each'} would change how an agent works on the project. Until you confirm ${n === 1 ? 'it, it stays' : 'them, they stay'} marked as the Keeper’s inference.`,
|
|
203
|
+
body: {
|
|
204
|
+
options: [
|
|
205
|
+
{ option: 'Confirm a rule', then: 'it becomes the project’s rule, with your words as its source' },
|
|
206
|
+
{ option: 'Correct or reject a rule', then: 'it is recorded as you say, and agents are no longer given the inferred wording' },
|
|
207
|
+
{ option: 'Leave them', then: 'each stays in force for agents, marked as the Keeper’s inference' },
|
|
208
|
+
],
|
|
209
|
+
currentView: `Inferred in ${job.scope.label}. Each rule is listed below with what it changes for an agent and the records it rests on.`,
|
|
210
|
+
whyItMatters: 'An agent working on the project follows the project’s rules. An inferred rule is shown everywhere as the Keeper’s inference, never as the project’s or your rule, until you confirm it.',
|
|
211
|
+
facts: [...facts.values()], otherExplanations: null, keepAdjust: null,
|
|
212
|
+
whatWouldSettleIt: 'Your word in the conversation on each rule: confirmed, it becomes the project’s rule with your words as its source; corrected or rejected, it is recorded as you say.',
|
|
213
|
+
},
|
|
214
|
+
ask: 'For your decision', judgementRecordId: judgementId,
|
|
215
|
+
reason: previous ? `More rules inferred in the same round (${asked.length})` : 'The rules a round infers are put to the owner once per round',
|
|
216
|
+
};
|
|
217
|
+
const note: Note = previous
|
|
218
|
+
? { ...previous, versions: [...previous.versions, version], status: 'Current', updatedAt: at }
|
|
219
|
+
: { id: noteId, projectId: ctx.project.id, mount: { kind: 'project', ids: [] }, status: 'Current', ownerResponse: null, versions: [version], discussion: [], followUps: [], author: { agent: 'pi', model: ctx.model }, resolvedReason: null, withdrawnReason: null, delegatedTo: null, cameFrom: cameFromFor(store, job, []), language: ctx.project.language, updatedAt: at };
|
|
220
|
+
store.notes.put(note, { jobId: ctx.jobId, basisSourceIds: [...new Set(version.body.facts.flatMap((f) => f.sourceIds))], summary: `${previous ? `Note v${version.version}` : 'Note'}: ${version.title}` });
|
|
221
|
+
const j = store.judgements.get(judgementId)!;
|
|
222
|
+
store.judgements.put({ ...j, outcome: { ...j.outcome, noteIds: [...new Set([...j.outcome.noteIds, noteId])] } });
|
|
223
|
+
ctx.onSaved?.('notes', noteId, `Note: ${version.title}`);
|
|
224
|
+
return ok({ noteId, version: version.version, rules: ruleIds });
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|