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,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeper conversation (Spec §6.8, CKC-10): sessions continue across turns in one pi session;
|
|
3
|
+
* each owner message is a source (the owner's statement); a message sent while the Keeper is
|
|
4
|
+
* answering is delivered as a mid-course adjustment; a conversation on a note joins its discussion.
|
|
5
|
+
*/
|
|
6
|
+
import type { KeeperJob, Note, Project, Source } from '../model/types.ts';
|
|
7
|
+
import { newId } from '../model/ids.ts';
|
|
8
|
+
import { makeSessionSource } from '../sources/anchor.ts';
|
|
9
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
10
|
+
import type { App } from '../server/app.ts';
|
|
11
|
+
import { HttpError } from '../server/http.ts';
|
|
12
|
+
import type { ChatTelemetry, KeeperEvent } from './runtime.ts';
|
|
13
|
+
import { turnPrompt } from './jobs/answering.ts';
|
|
14
|
+
import { cameFromView, mountView, nodeDetail, relationDetail } from '../server/graph-view.ts';
|
|
15
|
+
import { ruleOrPlanSummary } from '../server/workbench-content.ts';
|
|
16
|
+
import { DEPTH_NOTE_ID } from './organize/takeover.ts';
|
|
17
|
+
import { optionsLines } from './owner-text.ts';
|
|
18
|
+
|
|
19
|
+
export interface ChatContext { readonly kind: string; readonly id: string; readonly label: string }
|
|
20
|
+
interface TurnExtra { readonly conversationId: string; readonly question: string; readonly context: ChatContext | null; readonly ownerSourceId: string; readonly followUps?: readonly { text: string; at: string; sourceId: string }[]; readonly leafId?: string | null; readonly turn: number; readonly asker?: 'owner' | 'agent' }
|
|
21
|
+
|
|
22
|
+
const extraOf = (job: KeeperJob): TurnExtra | null => { const x = (job.task as { extra?: TurnExtra } | null)?.extra; return x && typeof x.conversationId === 'string' ? x : null; };
|
|
23
|
+
const cut = (s: string | null | undefined, n: number) => (s ?? '').length > n ? `${(s ?? '').slice(0, n)}…` : (s ?? '');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The note whole, for the prompt when the owner discusses or confirms it (owner 2026-09-22): the panel's brief stays
|
|
27
|
+
* short, but the Keeper answers from everything the note carries — body, mount, origin, the judgement record behind
|
|
28
|
+
* this version, the rules a rules note lists, and the discussion so far.
|
|
29
|
+
*/
|
|
30
|
+
function noteBrief(store: ProjectStore, note: Note): string {
|
|
31
|
+
const v = note.versions[note.versions.length - 1]!;
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
lines.push(`Note ${note.id} — "${v.title}"`);
|
|
34
|
+
lines.push(`Ask: ${v.ask} · status: ${note.status} · owner response: ${note.ownerResponse ?? 'none yet'} · version ${v.version} at ${v.at}`);
|
|
35
|
+
lines.push(`Preview: ${v.preview}`);
|
|
36
|
+
if (v.body.currentView) lines.push(`Current view: ${v.body.currentView}`);
|
|
37
|
+
if (v.body.whyItMatters) lines.push(`Why it matters: ${v.body.whyItMatters}`);
|
|
38
|
+
if (v.body.options?.length) lines.push('Options:', ...optionsLines(v.body.options));
|
|
39
|
+
for (const f of v.body.facts) lines.push(`Fact${f.inferred ? ' (inferred)' : ''}: ${f.text} [sources: ${f.sourceIds.join(', ') || 'none'}]`);
|
|
40
|
+
if (v.body.otherExplanations) lines.push(`Other explanations: ${v.body.otherExplanations}`);
|
|
41
|
+
if (v.body.keepAdjust) lines.push(`Keep or adjust: ${v.body.keepAdjust}`);
|
|
42
|
+
if (v.body.whatWouldSettleIt) lines.push(`What would settle it: ${v.body.whatWouldSettleIt}`);
|
|
43
|
+
const mount = mountView(store, note.mount);
|
|
44
|
+
lines.push(mount.objects.length === 0 ? `Mounted on: the whole project (${mount.kind}).` : `Mounted on (${mount.kind}): ${mount.objects.map((o) => `${o.label} (${o.id})`).join('; ')}`);
|
|
45
|
+
const came = cameFromView(store, note);
|
|
46
|
+
if (came) lines.push(`Came from: ${came.kind ?? 'unknown origin'} · job: ${came.jobKind ?? 'unknown kind'}${note.cameFrom?.jobId ? ` (${note.cameFrom.jobId})` : ''}${came.changes.length ? ` · about the changes: ${came.changes.map((c) => `${c.title} (${c.id})`).join('; ')}` : ''}`);
|
|
47
|
+
const j = store.judgements.get(v.judgementRecordId);
|
|
48
|
+
if (j) {
|
|
49
|
+
lines.push(`Based on judgement record ${j.id}: scope ${j.scope.kind} "${j.scope.label}"${j.scope.ids.length ? ` — ${j.scope.ids.join(', ')}` : ''}`);
|
|
50
|
+
const inputs: string[] = [];
|
|
51
|
+
if (j.inputs.referenceIds.length) inputs.push(`reference: ${j.inputs.referenceIds.join(', ')}`);
|
|
52
|
+
if (j.inputs.threadIds.length) inputs.push(`work: ${j.inputs.threadIds.join(', ')}`);
|
|
53
|
+
if (j.inputs.areaIds.length) inputs.push(`areas: ${j.inputs.areaIds.join(', ')}`);
|
|
54
|
+
if (j.inputs.relationIds.length) inputs.push(`relations: ${j.inputs.relationIds.join(', ')}`);
|
|
55
|
+
if (j.inputs.keyEvidenceSourceIds.length) inputs.push(`key evidence: ${j.inputs.keyEvidenceSourceIds.join(', ')}`);
|
|
56
|
+
if (j.inputs.conflictingSourceIds.length) inputs.push(`conflicting: ${j.inputs.conflictingSourceIds.join(', ')}`);
|
|
57
|
+
for (const inv of j.inputs.investigations) inputs.push(`investigation ${inv.jobId}: ${inv.conclusion} [sources: ${inv.sourceIds.join(', ') || 'none'}]`);
|
|
58
|
+
if (inputs.length) lines.push(`Judgement inputs — ${inputs.join('; ')}`);
|
|
59
|
+
if (j.scope.kind === 'rules') {
|
|
60
|
+
for (const ruleId of j.scope.ids) {
|
|
61
|
+
const r = store.rules.get(ruleId);
|
|
62
|
+
lines.push(r
|
|
63
|
+
? `Rule ${r.id}: ${r.summary} · group: ${r.group}${r.category ? ` · category: ${r.category}` : ''} · basis: ${r.basis} · applies to: ${r.appliesTo.join('; ')} · validity: ${r.validity} · ${r.ownerConfirmation ? `confirmed by the owner in ${r.ownerConfirmation.sourceId}` : 'not confirmed by the owner yet'} · sources: ${r.sourceIds.join(', ')}`
|
|
64
|
+
: `Rule ${ruleId}: no longer in the assets.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (note.discussion.length) {
|
|
69
|
+
lines.push('Discussion so far:');
|
|
70
|
+
for (const d of note.discussion) lines.push(`- ${d.role} at ${d.at}${d.sourceId ? ` (source ${d.sourceId})` : ''}: ${d.text}`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class ConversationService {
|
|
76
|
+
private readonly app: App;
|
|
77
|
+
constructor(app: App) { this.app = app; }
|
|
78
|
+
|
|
79
|
+
start(): void {
|
|
80
|
+
this.app.keeper.on('event', (event: KeeperEvent) => { if (event.kind === 'done') this.afterTurn(event.projectId, event.jobId); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private turnJobs(store: ProjectStore, conversationId?: string): KeeperJob[] {
|
|
84
|
+
return store.jobs.filter((j) => { const x = extraOf(j); return x !== null && (!conversationId || x.conversationId === conversationId); }).sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sessions(projectId: string, asker: 'owner' | 'agent' = 'owner') {
|
|
88
|
+
const store = this.app.store(projectId);
|
|
89
|
+
const byId = new Map<string, KeeperJob[]>();
|
|
90
|
+
for (const j of this.turnJobs(store)) { const x = extraOf(j)!; if ((x.asker ?? 'owner') !== asker) continue; if (!byId.has(x.conversationId)) byId.set(x.conversationId, []); byId.get(x.conversationId)!.push(j); }
|
|
91
|
+
return [...byId.entries()].map(([id, jobs]) => ({ id, title: cut(extraOf(jobs[0]!)!.question, 60), startedAt: jobs[0]!.queuedAt, lastAt: jobs[jobs.length - 1]!.endedAt ?? jobs[jobs.length - 1]!.queuedAt, turns: jobs.length, sessionFile: jobs[jobs.length - 1]!.sessionFile, live: this.app.keeper.hasLiveSession(`conversation:${projectId}:${id}`) }))
|
|
92
|
+
.sort((a, b) => b.lastAt.localeCompare(a.lastAt));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
turns(projectId: string, conversationId: string) {
|
|
96
|
+
const store = this.app.store(projectId);
|
|
97
|
+
return this.turnJobs(store, conversationId).map((j) => {
|
|
98
|
+
const x = extraOf(j)!;
|
|
99
|
+
return { jobId: j.id, turn: x.turn, at: j.queuedAt, question: x.question, context: x.context, followUps: x.followUps ?? [], answer: j.resultText, status: j.status, kind: j.kind, requestBasis: j.requestBasis, error: j.error, steps: j.steps, savedResults: j.savedResults, usage: j.usage, canBranch: Boolean(x.leafId) && this.app.keeper.branchingSupported, result: (x as { result?: unknown }).result ?? null };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read-only telemetry for the selected conversation. A new conversation has a model/window but no usage yet. */
|
|
104
|
+
telemetry(projectId: string, conversationId: string | null): ChatTelemetry {
|
|
105
|
+
if (!conversationId) return this.app.keeper.chatTelemetry(null);
|
|
106
|
+
const jobs = this.turnJobs(this.app.store(projectId), conversationId);
|
|
107
|
+
const recorded = [...jobs].reverse().find((job) => job.sessionFile !== null || job.model !== null) ?? jobs[jobs.length - 1];
|
|
108
|
+
return this.app.keeper.chatTelemetry(`conversation:${projectId}:${conversationId}`, recorded ? {
|
|
109
|
+
sessionFile: recorded.sessionFile,
|
|
110
|
+
leafId: extraOf(recorded)?.leafId ?? null,
|
|
111
|
+
model: recorded.model,
|
|
112
|
+
} : null);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* What the Keeper wrote earlier about the current object (§6.8: shown before any message, as written earlier, not
|
|
117
|
+
* generated now). `at`: when it was written, where it is one piece of writing with a time — a note's latest version —
|
|
118
|
+
* for the line above it (`Written earlier · <time>`, D105).
|
|
119
|
+
*/
|
|
120
|
+
existing(projectId: string, context: ChatContext | null): { text: string; sourceIds: string[]; at?: string } | null {
|
|
121
|
+
const store = this.app.store(projectId);
|
|
122
|
+
const project = this.app.project(projectId);
|
|
123
|
+
if (!context) {
|
|
124
|
+
const note = store.notes.find((n) => n.status === 'Current' && n.mount.kind === 'project');
|
|
125
|
+
if (note) { const v = note.versions[note.versions.length - 1]!; return { text: [v.title, v.preview, v.body.currentView, ...optionsLines(v.body.options)].filter(Boolean).join('\n'), sourceIds: v.body.facts.flatMap((f) => f.sourceIds), at: v.at }; }
|
|
126
|
+
const areas = store.areas.all();
|
|
127
|
+
if (areas.length) return { text: areas.map((a) => `${store.reference.get(a.referenceId)?.name ?? a.referenceId}: ${cut(a.effectNow, 300)}`).join('\n'), sourceIds: [] };
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (context.kind === 'note') {
|
|
131
|
+
const n = store.notes.get(context.id);
|
|
132
|
+
if (!n) return null;
|
|
133
|
+
const v = n.versions[n.versions.length - 1]!;
|
|
134
|
+
return { text: [v.title, v.preview, v.body.currentView, ...optionsLines(v.body.options), v.body.keepAdjust].filter(Boolean).join('\n'), sourceIds: v.body.facts.flatMap((f) => f.sourceIds), at: v.at };
|
|
135
|
+
}
|
|
136
|
+
if (context.kind === 'relation') {
|
|
137
|
+
const r = relationDetail(store, context.id);
|
|
138
|
+
if (!r) return null;
|
|
139
|
+
return { text: `${r.type}: ${r.fromLabel} → ${r.toLabel}\nClaim: ${r.claim}\nEvidence so far: ${r.evidence.factsSoFar || '—'}\nAssessment: ${r.assessment}`, sourceIds: [...r.evidence.sourceIds] };
|
|
140
|
+
}
|
|
141
|
+
// A rule or the organizing plan, opened from Project scope for the owner to confirm or correct (§6.7; CKC-21 AC-11).
|
|
142
|
+
if (context.kind === 'rule' || context.kind === 'plan') return ruleOrPlanSummary(store, context.kind, context.id);
|
|
143
|
+
const d = nodeDetail(store, project, context.id);
|
|
144
|
+
if (!d) return null;
|
|
145
|
+
const lines: string[] = [`${d.node.label} · ${d.node.category} · ${d.node.validity}${d.node.progress ? ` · ${d.node.progress}` : ''}`];
|
|
146
|
+
if ('reference' in d && d.reference) { lines.push(cut(d.reference.text, 500)); if (d.reference.quote) lines.push(`Owner's words: “${cut(d.reference.quote, 300)}”`); }
|
|
147
|
+
if ('area' in d && d.area) lines.push(`Effect now: ${cut(d.area.effectNow, 500)}`, `Gaps: ${cut(d.area.gaps, 300)}`);
|
|
148
|
+
if ('thread' in d && d.thread) lines.push(`Doing: ${cut(d.thread.doing, 400)}`, `Results: ${cut(d.thread.results, 300)}`, `Unresolved: ${cut(d.thread.unresolved, 300)}`);
|
|
149
|
+
if ('change' in d && d.change) lines.push(cut(d.change.summary, 400));
|
|
150
|
+
for (const n of d.notes.slice(0, 3)) lines.push(`Note: ${n.title} — ${cut(n.preview, 200)}`);
|
|
151
|
+
return { text: lines.join('\n'), sourceIds: [...d.node.sourceIds].slice(0, 12) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Send a message: a new turn, or a mid-course adjustment when this conversation is answering. */
|
|
155
|
+
send(projectId: string, input: { text: string; context: ChatContext | null; conversationId: string | null; asker?: 'owner' | 'agent'; waiting?: readonly string[]; confirm?: boolean }): { conversationId: string; jobId: string; mode: 'turn' | 'steer'; status: string } {
|
|
156
|
+
const store = this.app.store(projectId);
|
|
157
|
+
const project = this.app.project(projectId);
|
|
158
|
+
const asker = input.asker ?? 'owner';
|
|
159
|
+
const conversationId = input.conversationId ?? newId(asker === 'agent' ? 'ask' : 'conv');
|
|
160
|
+
const previous = this.turnJobs(store, conversationId);
|
|
161
|
+
const turn = previous.length + (previous.flatMap((j) => extraOf(j)!.followUps ?? []).length) + 1;
|
|
162
|
+
const source = this.ownerSource(store, project, conversationId, turn, input.text, previous.length ? previous[previous.length - 1]!.sessionFile : null, asker);
|
|
163
|
+
const running = previous.find((j) => j.status === 'Running');
|
|
164
|
+
if (running) {
|
|
165
|
+
const x = extraOf(running)!;
|
|
166
|
+
store.jobs.put({ ...running, task: { ...(running.task as object), extra: { ...x, followUps: [...(x.followUps ?? []), { text: input.text, at: new Date().toISOString(), sourceId: source.id }] } } });
|
|
167
|
+
this.app.keeper.steer(projectId, running.id, `Owner (mid-course, source ${source.id}): ${input.text}`);
|
|
168
|
+
return { conversationId, jobId: running.id, mode: 'steer', status: 'Running' };
|
|
169
|
+
}
|
|
170
|
+
// The whole note goes into the prompt (owner 2026-09-22); the panel's brief from `existing` stays short.
|
|
171
|
+
let contextSummary: string | null = null;
|
|
172
|
+
let brief: string | null = null;
|
|
173
|
+
if (input.context) {
|
|
174
|
+
if (input.context.kind === 'note') {
|
|
175
|
+
const n = store.notes.get(input.context.id);
|
|
176
|
+
brief = n ? noteBrief(store, n) : null;
|
|
177
|
+
} else {
|
|
178
|
+
contextSummary = this.existing(projectId, input.context)?.text ?? null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const job = this.app.keeper.enqueue(projectId, {
|
|
182
|
+
kind: 'Answering', initiator: 'owner', priority: asker === 'agent' ? 1 : 0, stream: asker === 'owner',
|
|
183
|
+
scope: { kind: asker === 'agent' ? 'agent-query' : 'conversation', ids: [conversationId], label: cut(input.text, 80) },
|
|
184
|
+
prompt: turnPrompt({ project, text: input.text, ownerSourceId: source.id, context: input.context, contextSummary, noteBrief: brief, first: previous.length === 0, asker, waiting: input.waiting, confirming: input.confirm === true }),
|
|
185
|
+
sessionKey: `conversation:${projectId}:${conversationId}`, ownerSourceId: asker === 'owner' ? source.id : null, conversation: asker === 'owner',
|
|
186
|
+
task: { conversationId, question: input.text, context: input.context, ownerSourceId: source.id, turn, asker } satisfies TurnExtra,
|
|
187
|
+
});
|
|
188
|
+
return { conversationId, jobId: job.id, mode: 'turn', status: job.status };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `Confirm` on a note (owner 2026-09-22): the owner's fixed statement — their agreement with what the note says
|
|
193
|
+
* now — sent with the note as context and answered in the background. It joins the latest session, or opens a new
|
|
194
|
+
* one when that session is answering: a confirm is never a mid-course steer, which would lose the note's context.
|
|
195
|
+
*/
|
|
196
|
+
confirm(projectId: string, noteId: string): { conversationId: string; jobId: string; mode: 'turn' | 'steer'; status: string } {
|
|
197
|
+
const store = this.app.store(projectId);
|
|
198
|
+
const note = store.notes.get(noteId);
|
|
199
|
+
if (!note) throw new HttpError(404, 'Unknown note');
|
|
200
|
+
if (note.status !== 'Current') throw new HttpError(409, `Note ${noteId} is ${note.status}: only a Current note can be confirmed.`);
|
|
201
|
+
if (note.ownerResponse === 'Decided') throw new HttpError(409, `Note ${noteId} is already Decided.`);
|
|
202
|
+
// The depth question is answered by the owner picking one of the options it lists, not by a confirm (§3.7).
|
|
203
|
+
if (noteId === DEPTH_NOTE_ID) throw new HttpError(409, 'The takeover-depth note is answered by choosing one of the depths it offers, not by confirming it.');
|
|
204
|
+
const v = note.versions[note.versions.length - 1]!;
|
|
205
|
+
const text = note.language === 'zh' ? `我确认这条 note:「${v.title}」(${note.id})。` : `I confirm this note: "${v.title}" (${note.id}).`;
|
|
206
|
+
const latest = this.sessions(projectId)[0]?.id ?? null;
|
|
207
|
+
const answering = latest !== null && this.turnJobs(store, latest).some((j) => j.status === 'Running');
|
|
208
|
+
return this.send(projectId, { text, context: { kind: 'note', id: note.id, label: v.title }, conversationId: answering ? null : latest, confirm: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Branch the live session back to the end of a turn (pi keeps the tree in the same file). */
|
|
212
|
+
async branch(projectId: string, conversationId: string, jobId: string): Promise<{ ok: boolean; reason: string | null }> {
|
|
213
|
+
const store = this.app.store(projectId);
|
|
214
|
+
const job = store.jobs.get(jobId);
|
|
215
|
+
const leaf = job ? extraOf(job)?.leafId ?? null : null;
|
|
216
|
+
if (!leaf) return { ok: false, reason: 'This turn has no recorded branch point.' };
|
|
217
|
+
return this.app.keeper.branchSession(`conversation:${projectId}:${conversationId}`, leaf);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private ownerSource(store: ProjectStore, project: Project, conversationId: string, turn: number, text: string, sessionFile: string | null, asker: 'owner' | 'agent' = 'owner') {
|
|
221
|
+
const at = new Date().toISOString();
|
|
222
|
+
const scopeItemId = project.scope.find((i) => i.relation === 'Main project')?.id ?? project.scope[0]?.id ?? '';
|
|
223
|
+
// Who wrote the message, and where their words begin after the heading it is shown under, are recorded on the source
|
|
224
|
+
// (`said`; the time is the anchor's): reading them back out of the heading meant a new heading would have dropped the
|
|
225
|
+
// owner's words from what counts as theirs without a sign (Spec §1.2, §3.3).
|
|
226
|
+
const heading = `[${asker} ${at}]\n`;
|
|
227
|
+
const made = makeSessionSource({ projectId: project.id, host: 'pi', sessionId: conversationId, file: sessionFile ?? '', cwd: project.locations[0] ?? null, messageStart: turn, messageEnd: turn, at, excerpt: `${heading}${text}`, title: asker === 'agent' ? `Agent query · message ${turn}` : `Keeper conversation · message ${turn}`, scopeItemId });
|
|
228
|
+
const source: Source = { ...made, said: { by: asker, wordsFrom: heading.length } };
|
|
229
|
+
// An owner message is an owner statement (§2.6); what it is used as (a decision, a correction, a question) is judged per message, so it starts as Not yet judged.
|
|
230
|
+
store.sources.put({ ...source, usedAs: null, usedAsBy: null }, { jobId: null, summary: 'Owner message in the Keeper conversation' });
|
|
231
|
+
return source;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A conversation on a note joins its discussion; the owner's response becomes Discussed unless something stronger is recorded (§4.4). */
|
|
235
|
+
private afterTurn(projectId: string, jobId: string): void {
|
|
236
|
+
const store = this.app.store(projectId);
|
|
237
|
+
const job = store.jobs.get(jobId);
|
|
238
|
+
const x = job ? extraOf(job) : null;
|
|
239
|
+
if (!job || !x || x.context?.kind !== 'note') return;
|
|
240
|
+
const note = store.notes.get(x.context.id);
|
|
241
|
+
if (!note) return;
|
|
242
|
+
const entries = [...note.discussion, { role: 'owner' as const, text: x.question, at: job.queuedAt, sourceId: x.ownerSourceId }, ...(x.followUps ?? []).map((f) => ({ role: 'owner' as const, text: f.text, at: f.at, sourceId: f.sourceId }))];
|
|
243
|
+
if (job.resultText) entries.push({ role: 'keeper', text: job.resultText, at: job.endedAt ?? new Date().toISOString(), sourceId: null });
|
|
244
|
+
const delegated = job.kind === 'Your request';
|
|
245
|
+
// §4.4, §3.9: the note is Decided when this message's source was used to record a confirmation or a decision;
|
|
246
|
+
// otherwise the response stands as it was, Discussed by default.
|
|
247
|
+
const recorded = store.rules.find((r) => r.ownerConfirmation?.sourceId === x.ownerSourceId) !== undefined
|
|
248
|
+
|| store.reference.find((r) => r.attribution.identity === 'Decision' && r.sourceIds.includes(x.ownerSourceId)) !== undefined;
|
|
249
|
+
const ownerResponse = delegated ? 'Delegated' : recorded ? 'Decided' : (note.ownerResponse ?? 'Discussed');
|
|
250
|
+
const followUps = delegated && !note.followUps.some((f) => f.jobId === job.id) ? [...note.followUps, { kind: 'adjustment' as const, jobId: job.id, at: job.endedAt ?? new Date().toISOString(), summary: `${job.status}: ${(job.resultText ?? '').slice(0, 200)}` }] : note.followUps;
|
|
251
|
+
store.notes.put({ ...note, discussion: entries, ownerResponse, followUps, updatedAt: new Date().toISOString() }, { jobId, summary: delegated ? 'Adjustment delegated on the note' : 'Discussed in the Keeper conversation' });
|
|
252
|
+
}
|
|
253
|
+
}
|