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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution agent's own entry (Spec §7.10; CKC-12 AC-20–AC-23, AC-26, AC-27, AC-29): the same choices, the same
|
|
3
|
+
* content and the same originals as the workbench, read from the running workbench so both come from the same code.
|
|
4
|
+
* Reading needs the workbench, not the Keeper; only `ask` needs the Keeper.
|
|
5
|
+
*
|
|
6
|
+
* Two callers say the same thing through it: the command line (`pk context`, `get`, `options`, `help`, `ask`, in
|
|
7
|
+
* src/cli.ts), which prints `out` on stdout and `err` on stderr, and the MCP server (`pk mcp`, src/mcp/server.ts; D108),
|
|
8
|
+
* which hands both back to its client as one tool result. Neither writes anywhere else, so the MCP server's stdout stays
|
|
9
|
+
* pure JSON-RPC.
|
|
10
|
+
*/
|
|
11
|
+
import { changeBrief, factBrief, nodeBrief, relationBrief } from './object-brief.ts';
|
|
12
|
+
import type { BriefChange, BriefFact, BriefNode } from './object-brief.ts';
|
|
13
|
+
|
|
14
|
+
/** Where an entry's text goes: `out` what was asked for, `err` what went wrong or is said beside it. */
|
|
15
|
+
export interface EntryOutput {
|
|
16
|
+
out(text: string): void;
|
|
17
|
+
err(text: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Leaving with a code without cutting the event loop short: on Windows, exiting while fetch handles are still
|
|
21
|
+
* closing trips a libuv assertion (seen in the 2026-09-18 trial as exit 127). The MCP server turns it into a result. */
|
|
22
|
+
export class Exit extends Error { readonly code: number; constructor(code: number) { super(`exit ${code}`); this.code = code; } }
|
|
23
|
+
|
|
24
|
+
/** Which workbench and which project: `project` by id, or else the project containing `cwd`. */
|
|
25
|
+
export interface EntryTarget {
|
|
26
|
+
readonly base: string;
|
|
27
|
+
readonly project: string | null;
|
|
28
|
+
readonly cwd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What `pk context` and a work pack are asked with (`--work`, `--purpose`, `--kind`, `--recipient`, `--since`). */
|
|
32
|
+
export interface PackOptions {
|
|
33
|
+
readonly work?: string | null;
|
|
34
|
+
readonly purpose?: string | null;
|
|
35
|
+
readonly kind?: string | null;
|
|
36
|
+
readonly recipient?: string | null;
|
|
37
|
+
readonly since?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const NOT_RUNNING = (base: string) => `ProjectKeeper is not running at ${base}. Start it with \`pk serve\` (packs, originals and options are read from the saved assets; the Keeper itself does not need to be running).`;
|
|
41
|
+
export const NOT_RUNNING_FOR_ASK = (base: string) => `ProjectKeeper is not running at ${base}. Start it with \`pk serve\`.`;
|
|
42
|
+
|
|
43
|
+
/** A GET or POST against the workbench; what is not running or answers with an error is said on `err` and leaves. */
|
|
44
|
+
function caller(base: string, io: EntryOutput, notRunning: (base: string) => string, json: boolean) {
|
|
45
|
+
return async <T,>(path: string, init?: RequestInit): Promise<T> => {
|
|
46
|
+
let res: Response;
|
|
47
|
+
try { res = await (json ? fetch(`${base}${path}`, { headers: { 'Content-Type': 'application/json' }, ...init }) : fetch(`${base}${path}`)); }
|
|
48
|
+
catch { io.err(notRunning(base)); throw new Exit(3); }
|
|
49
|
+
const data = await res.json() as T & { error?: string };
|
|
50
|
+
if (!res.ok) { io.err(String(data.error ?? res.statusText)); throw new Exit(1); }
|
|
51
|
+
return data;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `pk context`, `pk get <id>`, `pk options` and `pk help` for the project at hand. `args` are the command's
|
|
57
|
+
* positionals (`get` reads the first as its id).
|
|
58
|
+
*/
|
|
59
|
+
export async function agentEntry(command: string, target: EntryTarget, io: EntryOutput, args: readonly string[], options: PackOptions = {}): Promise<number> {
|
|
60
|
+
const call = caller(target.base, io, NOT_RUNNING, false);
|
|
61
|
+
let projectId = target.project;
|
|
62
|
+
if (!projectId) {
|
|
63
|
+
const r = await call<{ projectId: string | null; message?: string }>(`/api/resolve?cwd=${encodeURIComponent(target.cwd)}`);
|
|
64
|
+
if (!r.projectId) { io.out(`${r.message} Give --project <id>; \`pk projects\` lists them.`); return 4; }
|
|
65
|
+
projectId = r.projectId;
|
|
66
|
+
}
|
|
67
|
+
const P = encodeURIComponent(projectId);
|
|
68
|
+
if (command === 'help') {
|
|
69
|
+
const r = await call<{ lines: string[] }>(`/api/projects/${P}/usage`);
|
|
70
|
+
io.out(`ProjectKeeper for execution agents · project ${projectId}\n\n${r.lines.map((l) => `- ${l}`).join('\n')}`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (command === 'options') {
|
|
74
|
+
type Difference = { name: string; difference: string };
|
|
75
|
+
const o = await call<{ forWork: { kind: string; id: string; label: string; progress?: string }[]; purposes: string[]; kinds: string[]; recipients: string[]; roles: string[]; kindDifferences: Difference[]; recipientDifferences: Difference[] }>(`/api/projects/${P}/context/options`);
|
|
76
|
+
const k = await call<{
|
|
77
|
+
breakpoints: { id: string; kind: string; targetId: string; targetName: string }[];
|
|
78
|
+
sendBacks: { id: string; stage: string; to: string; what: string; targetId: string; targetName: string }[];
|
|
79
|
+
sixThings: { thing: number; objects: { id: string; name: string }[] }[];
|
|
80
|
+
}>(`/api/projects/${P}/k-options`);
|
|
81
|
+
const byKind = (k: string) => o.forWork.filter((x) => x.kind === k);
|
|
82
|
+
io.out(`Areas:\n${byKind('area').map((x) => `- ${x.id} ${x.label}`).join('\n') || '- none'}`);
|
|
83
|
+
io.out(`\nWork items:\n${byKind('work').map((x) => `- ${x.id} ${x.label}${x.progress ? ` · ${x.progress}` : ''}`).join('\n') || '- none'}`);
|
|
84
|
+
io.out(`\nRoles recognised: ${o.roles.length ? o.roles.join(', ') : 'none (that is not a gap)'}`);
|
|
85
|
+
io.out(`Purposes: ${o.purposes.join(', ')}`);
|
|
86
|
+
// Each kind and recipient says how its pack differs from the default one on this project, measured by assembling
|
|
87
|
+
// both and comparing them, so nobody has to fetch all nine to find out (Spec §7.10; CKC-12 AC-21).
|
|
88
|
+
const differences = (title: string, list: Difference[]) => io.out(`\n${title} (against the default pack: --purpose Start --kind Implement --recipient "Incoming agent", whole project):\n${list.map((x) => `- ${x.name} — ${x.difference}`).join('\n')}`);
|
|
89
|
+
differences('Kinds', o.kindDifferences);
|
|
90
|
+
differences('Recipients', o.recipientDifferences);
|
|
91
|
+
io.out(`\nLit breakpoints:\n${k.breakpoints.map((b) => `- ${b.id} · ${b.kind} · on ${b.targetName} (${b.targetId})`).join('\n') || '- none'}`);
|
|
92
|
+
io.out(`\nOpen send-backs:\n${k.sendBacks.map((s) => `- ${s.id} · ${s.stage} · to ${s.to} · ${s.what} · on ${s.targetName} (${s.targetId})`).join('\n') || '- none'}`);
|
|
93
|
+
const sixNames: Record<number, string> = { 1: 'stale', 2: 'drift', 3: 'dropped along the way', 4: 'grown by itself', 5: 'let pass', 6: 'looks residual' };
|
|
94
|
+
io.out(`\nSix things:\n${k.sixThings.map((s) => `${s.thing} ${sixNames[s.thing] ?? ''}:\n${s.objects.map((x) => `- ${x.id} ${x.name}`).join('\n') || '- none'}`).join('\n')}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
const pack = async (scope: { kind: string; id: string } | null, purposeDefault: 'Start' | 'Work') => {
|
|
98
|
+
const q = new URLSearchParams({ scope: scope?.kind ?? 'project', purpose: options.purpose ?? purposeDefault, kind: options.kind ?? 'Implement', recipient: options.recipient ?? 'Incoming agent' });
|
|
99
|
+
if (scope) q.set('ids', scope.id);
|
|
100
|
+
const since = options.since;
|
|
101
|
+
if (since) q.set('since', since);
|
|
102
|
+
const r = await call<{ markdown: string }>(`/api/projects/${P}/context?${q}`);
|
|
103
|
+
io.out(r.markdown);
|
|
104
|
+
return 0;
|
|
105
|
+
};
|
|
106
|
+
if (command === 'context') {
|
|
107
|
+
const work = options.work;
|
|
108
|
+
if (!work) return pack(null, 'Start');
|
|
109
|
+
const found = await call<{ id?: string; kind: string | null }>(`/api/projects/${P}/lookup/${encodeURIComponent(work)}`).catch(() => ({ id: work, kind: null }));
|
|
110
|
+
// A merged work item's id comes back as the id of the one kept (Spec §1.4).
|
|
111
|
+
if (found.kind === 'work' || found.kind === 'area') return pack({ kind: found.kind, id: found.id ?? work }, 'Work');
|
|
112
|
+
// Not an id: a name, matched against the choices the workbench offers.
|
|
113
|
+
const o = await call<{ forWork: { kind: string; id: string; label: string }[] }>(`/api/projects/${P}/context/options`);
|
|
114
|
+
const w = work.toLowerCase();
|
|
115
|
+
const named = (x: { label: string }) => { const l = x.label.toLowerCase(); return l === w || l.startsWith(`${w} `) || l.startsWith(`${w}·`) || l.startsWith(`${w}:`) || l.startsWith(`${w}:`); };
|
|
116
|
+
const exact = o.forWork.filter((x) => x.kind !== 'project' && named(x));
|
|
117
|
+
if (exact.length === 1) return pack({ kind: exact[0]!.kind, id: exact[0]!.id }, 'Work');
|
|
118
|
+
const hits = exact.length ? exact : o.forWork.filter((x) => x.kind !== 'project' && x.label.toLowerCase().includes(w));
|
|
119
|
+
if (hits.length === 1) return pack({ kind: hits[0]!.kind, id: hits[0]!.id }, 'Work');
|
|
120
|
+
io.err(hits.length ? `"${work}" matches ${hits.length} items; give one id:\n${hits.slice(0, 20).map((x) => `- ${x.id} ${x.label}`).join('\n')}` : `No work item or area is named "${work}". \`pk options\` lists them with their ids.`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
// pk get <id>
|
|
124
|
+
const asked = args[0];
|
|
125
|
+
if (!asked) { io.err('usage: pk get <id> (work item, area, source, note, rule, change, mark, breakpoint, send-back, semantic patch, code territory …)'); return 2; }
|
|
126
|
+
if (/[\\/]/.test(asked) || /^[A-Za-z]:/.test(asked)) { io.err('pk get reads ids from the assets, not paths. A pack names every source by its id (src_…).'); return 2; }
|
|
127
|
+
const what = await call<{ id?: string; kind: string | null; mergedFrom?: string; elsewhere?: { projectId: string; name: string } | null }>(`/api/projects/${P}/lookup/${encodeURIComponent(asked)}`);
|
|
128
|
+
if (!what.kind) {
|
|
129
|
+
io.err(what.elsewhere ? `${asked} belongs to project ${what.elsewhere.name} (${what.elsewhere.projectId}), not ${projectId}; it is not read here. Ask that project: pk get --project ${what.elsewhere.projectId} ${asked}` : `${asked} is not an id of project ${projectId}.`);
|
|
130
|
+
return 4;
|
|
131
|
+
}
|
|
132
|
+
// A merged work item's id reaches the one kept (Spec §1.4, §7.10): everything below reads that one.
|
|
133
|
+
const id = what.id ?? asked;
|
|
134
|
+
if (what.mergedFrom) io.err(`${asked} was merged into ${id}; reading ${id}.`);
|
|
135
|
+
const out = (lines: (string | null | undefined | false)[]) => { io.out(lines.filter((l) => l !== null && l !== undefined && l !== false).join('\n')); return 0; };
|
|
136
|
+
if (['breakpoint', 'sendback', 'patch', 'territory'].includes(what.kind)) {
|
|
137
|
+
const brief = await call<{ text: string }>(`/api/projects/${P}/k-briefs/${encodeURIComponent(id)}`);
|
|
138
|
+
return out([brief.text]);
|
|
139
|
+
}
|
|
140
|
+
if (what.kind === 'work' || what.kind === 'area') return pack({ kind: what.kind, id }, 'Work');
|
|
141
|
+
if (what.kind === 'source') {
|
|
142
|
+
// The same as the workbench's original reading (§6.4, §7.10), except a code file: where it is and the version read,
|
|
143
|
+
// never its lines — the agent opens the current version itself (D65). The workbench composes the text.
|
|
144
|
+
const s = await call<{ text: string }>(`/api/projects/${P}/sources/${encodeURIComponent(id)}?for=agent`);
|
|
145
|
+
return out([s.text]);
|
|
146
|
+
}
|
|
147
|
+
if (what.kind === 'rule') {
|
|
148
|
+
// A project rule in full, with where it is written (§1.15, §7.10).
|
|
149
|
+
const r = await call<{ text: string }>(`/api/projects/${P}/rules/${encodeURIComponent(id)}`);
|
|
150
|
+
return out([r.text]);
|
|
151
|
+
}
|
|
152
|
+
if (what.kind === 'note') {
|
|
153
|
+
const n = await call<{ status: string; ownerResponse: string | null; mount: { kind: string; ids: string[] }; versions: { version: number; at: string; title: string; preview: string; ask: string; reason: string; body: Record<string, unknown> & { facts?: { text: string; sourceIds: string[]; inferred: boolean }[] } }[]; discussion: { role: string; text: string }[]; followUps: { kind: string; at: string; summary: string }[]; cameFrom: { kind: string | null; changes: { id: string; title: string }[] } | null; judgement: { scope: { label: string }; at: string } | null ; looked?: { where: string[]; upTo: string; sessionsUpTo?: string | null; behind: string | null } | null }>(`/api/projects/${P}/notes/${encodeURIComponent(id)}`);
|
|
154
|
+
const v = n.versions[n.versions.length - 1]!;
|
|
155
|
+
const sec = (title: string, text: unknown) => (text ? `\n## ${title}\n${String(text)}` : null);
|
|
156
|
+
return out([
|
|
157
|
+
`# ${v.title}`, `Keeper note · v${v.version} · ${v.at} · ${v.ask} · ${n.status}${n.ownerResponse ? ` · owner: ${n.ownerResponse}` : ''}`,
|
|
158
|
+
`Mounted on: ${n.mount.kind}${n.mount.ids.length ? ` ${n.mount.ids.join(', ')}` : ''}`,
|
|
159
|
+
n.cameFrom?.kind ? `Came from: ${n.cameFrom.kind}${n.cameFrom.changes.length ? ` — ${n.cameFrom.changes.map((c) => `${c.title} (${c.id})`).join('; ')}` : ''}` : null,
|
|
160
|
+
'', v.preview,
|
|
161
|
+
sec('Current view', v.body.currentView), sec('Why it matters', v.body.whyItMatters),
|
|
162
|
+
Array.isArray(v.body.options) && v.body.options.length ? `\n## Options\n${(v.body.options as { option: string; then: string }[]).map((o) => `- ${o.option} — ${o.then}`).join('\n')}` : null,
|
|
163
|
+
v.body.facts?.length ? `\n## Facts\n${v.body.facts.map((f) => `- ${f.inferred ? '(Inferred) ' : ''}${f.text}${f.sourceIds.length ? ` [${f.sourceIds.join(', ')}]` : ''}`).join('\n')}` : null,
|
|
164
|
+
sec('Other explanations', v.body.otherExplanations), sec('Keep or adjust', v.body.keepAdjust), sec('What would settle it', v.body.whatWouldSettleIt),
|
|
165
|
+
n.looked ? `\n## What was read\n${n.looked.where.map((w) => `- ${w}`).join('\n')}\nRead up to ${n.looked.upTo}${n.looked.sessionsUpTo ? ` · sessions to ${n.looked.sessionsUpTo}` : ''}${n.looked.behind ? `\nDoes not reach the present: ${n.looked.behind}` : ''}` : null,
|
|
166
|
+
n.judgement ? `\nBased on: judged ${n.judgement.at} · scope ${n.judgement.scope.label}` : null,
|
|
167
|
+
n.followUps.length ? `\n## Course\n${n.followUps.map((f) => `- ${f.kind} · ${f.at} · ${f.summary}`).join('\n')}` : null,
|
|
168
|
+
n.discussion.length ? `\n## Discussion\n${n.discussion.map((d) => `- ${d.role}: ${d.text}`).join('\n')}` : null,
|
|
169
|
+
n.versions.length > 1 ? `\n## Earlier versions\n${n.versions.slice(0, -1).reverse().map((x) => `- v${x.version} · ${x.at} · ${x.title} — ${x.reason}`).join('\n')}` : null,
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
if (what.kind === 'change') {
|
|
173
|
+
// The record itself, without its whole propagation list: only what has not followed it, the rest counted (D59).
|
|
174
|
+
const r = await call<{ changes: BriefChange[] }>(`/api/projects/${P}/changes`);
|
|
175
|
+
const c = r.changes.find((x) => x.id === id)!;
|
|
176
|
+
return out([changeBrief(c)]);
|
|
177
|
+
}
|
|
178
|
+
if (what.kind === 'mark') {
|
|
179
|
+
const m = await call<{ kind: string; targetId: string; targetLabel: string; clue: string; since: string; clueSources: { id: string; title: string; label: string }[]; note: { id: string; title: string } | null; closed: { result: string; at: string; reason: string } | null }>(`/api/projects/${P}/marks/${encodeURIComponent(id)}`);
|
|
180
|
+
return out([`# ${m.kind} on ${m.targetLabel} (${m.targetId})`, `Since ${m.since}${m.closed ? ` · closed ${m.closed.at}: ${m.closed.result} — ${m.closed.reason}` : ' · open'}`, '', `Clue: ${m.clue}`, m.clueSources.length ? `Clue sources: ${m.clueSources.map((s) => `${s.title} — ${s.label} (${s.id})`).join('; ')}` : null, m.note ? `Note: ${m.note.title} (${m.note.id})` : null]);
|
|
181
|
+
}
|
|
182
|
+
// Fact records, relations, reference items and other graph objects: the same facts as the workbench's object
|
|
183
|
+
// detail, written for an agent to read (Spec §7.10; CKC-12 AC-23) — the object's own content whole, its
|
|
184
|
+
// neighbours by name and id, none of the workbench's own bookkeeping.
|
|
185
|
+
if (what.kind === 'fact') return out([factBrief(await call<BriefFact>(`/api/projects/${P}/facts/${encodeURIComponent(id)}`))]);
|
|
186
|
+
if (what.kind === 'relation') return out([relationBrief(await call<Parameters<typeof relationBrief>[0]>(`/api/projects/${P}/relations/${encodeURIComponent(id)}`))]);
|
|
187
|
+
return out([nodeBrief(await call<BriefNode>(`/api/projects/${P}/nodes/${encodeURIComponent(id)}`))]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** What `pk ask` is given, and how its answer says to fetch it later (the command line names `pk ask`, MCP its tool). */
|
|
191
|
+
export interface AskOptions {
|
|
192
|
+
readonly question: string;
|
|
193
|
+
readonly fetch: string | null;
|
|
194
|
+
readonly thread: string | null;
|
|
195
|
+
readonly wait: boolean;
|
|
196
|
+
readonly fetchLater: (projectId: string, jobId: string) => string;
|
|
197
|
+
readonly askAgain: (projectId: string, thread: string) => string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** `pk ask` (§7.6): a query entry without any host integration, talking to the running workbench. */
|
|
201
|
+
export async function askEntry(target: EntryTarget, io: EntryOutput, options: AskOptions): Promise<number> {
|
|
202
|
+
const call = caller(target.base, io, NOT_RUNNING_FOR_ASK, true) as <T = Record<string, unknown>>(path: string, init?: RequestInit) => Promise<T>;
|
|
203
|
+
const fetchId = options.fetch;
|
|
204
|
+
let projectId = target.project;
|
|
205
|
+
if (!projectId) {
|
|
206
|
+
const r = await call<{ projectId: string | null; message?: string }>(`/api/resolve?cwd=${encodeURIComponent(target.cwd)}`);
|
|
207
|
+
if (!r.projectId) { io.out(String(r.message)); return 4; }
|
|
208
|
+
projectId = r.projectId;
|
|
209
|
+
}
|
|
210
|
+
if (fetchId) {
|
|
211
|
+
const r = await call<{ status: string; answer: string | null; error: string | null; steps: number; startedAt?: string | null; sources: { id: string; title: string; label: string }[] }>(`/api/projects/${encodeURIComponent(projectId)}/ask/${encodeURIComponent(fetchId)}`);
|
|
212
|
+
if (r.status === 'Done') { io.out(r.answer ?? ''); if (r.sources.length) io.out(`\nSources:\n${r.sources.map((s) => `- ${s.id} ${s.title} — ${s.label}`).join('\n')}`); }
|
|
213
|
+
else {
|
|
214
|
+
// What the investigation is doing, so waiting is not blind (trial: "no progress or ETA").
|
|
215
|
+
const running = r.status === 'Running' && r.startedAt ? ` for ${Math.max(1, Math.round((Date.now() - Date.parse(r.startedAt)) / 60000))} min, ${r.steps} step${r.steps === 1 ? '' : 's'} so far` : '';
|
|
216
|
+
io.out(`${r.status}${running}${r.error ? `: ${r.error}` : ''}. Fetch again later: ${options.fetchLater(projectId, fetchId)}`);
|
|
217
|
+
}
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
const question = options.question.trim();
|
|
221
|
+
if (!question) { io.err('usage: pk ask [--project <id> | --cwd <dir>] [--thread <id>] [--wait] "<question>"'); return 2; }
|
|
222
|
+
const r = await call<{ known: string; investigation: { jobId: string; thread: string; status: string } }>(`/api/projects/${encodeURIComponent(projectId)}/ask`, { method: 'POST', body: JSON.stringify({ question, thread: options.thread }) });
|
|
223
|
+
if (r.known) io.out(`Known now (from the organized assets):\n${r.known}\n`); else io.out('Nothing organized answers this yet.\n');
|
|
224
|
+
io.out(`Investigation ${r.investigation.jobId} (${r.investigation.status}) in thread ${r.investigation.thread}.`);
|
|
225
|
+
if (!options.wait) { io.out(`Fetch the answer later: ${options.fetchLater(projectId, r.investigation.jobId)}\nKeep asking in the same thread: ${options.askAgain(projectId, r.investigation.thread)}`); return 0; }
|
|
226
|
+
for (;;) {
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
228
|
+
const s = await call<{ status: string; answer: string | null; error: string | null; sources: { id: string; title: string; label: string }[] }>(`/api/projects/${encodeURIComponent(projectId)}/ask/${encodeURIComponent(r.investigation.jobId)}`);
|
|
229
|
+
if (['Done', 'Failed', 'Stopped'].includes(s.status)) { io.out(`\n${s.answer ?? s.error ?? s.status}`); if (s.sources.length) io.out(`\nSources:\n${s.sources.map((x) => `- ${x.id} ${x.title} — ${x.label}`).join('\n')}`); return s.status === 'Done' ? 0 : 1; }
|
|
230
|
+
if (s.status === 'Waiting for quota') { io.out(`Waiting for quota; fetch later: ${options.fetchLater(projectId, r.investigation.jobId)}`); return 0; }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query entry for agents without a host integration (Spec §7.6, CKC-12 AC-13/14). The known
|
|
3
|
+
* part is answered at once from the assets, deterministically; what needs investigation runs as
|
|
4
|
+
* a Keeper turn the agent fetches later by job id. Questions in one thread keep their context.
|
|
5
|
+
*/
|
|
6
|
+
import type { PendingWait, Project } from '../model/types.ts';
|
|
7
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
8
|
+
import { anchorLabel } from '../sources/anchor.ts';
|
|
9
|
+
import { canonicalPath, isWithin } from '../util/paths.ts';
|
|
10
|
+
|
|
11
|
+
function terms(question: string): string[] {
|
|
12
|
+
const out = new Set<string>();
|
|
13
|
+
for (const w of question.toLowerCase().match(/[a-z0-9][a-z0-9_-]{2,}/g) ?? []) out.add(w);
|
|
14
|
+
const cjk = question.match(/[㐀-鿿]+/g) ?? [];
|
|
15
|
+
for (const run of cjk) for (let i = 0; i + 1 < run.length; i++) out.add(run.slice(i, i + 2));
|
|
16
|
+
return [...out];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function score(text: string, ts: string[]): number {
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
let n = 0;
|
|
22
|
+
for (const t of ts) if (lower.includes(t)) n += t.length >= 4 ? 2 : 1;
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface KnownPart {
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly sourceIds: readonly string[];
|
|
29
|
+
readonly hits: number;
|
|
30
|
+
/** Changed material the entries in the known part are still waiting for: the investigation reads it (AC-25). */
|
|
31
|
+
readonly pendingSourceIds: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What the assets already say about the question: conclusions first, sources after. */
|
|
35
|
+
export function knownPart(store: ProjectStore, question: string): KnownPart {
|
|
36
|
+
const ts = terms(question);
|
|
37
|
+
if (ts.length === 0) return { text: '', sourceIds: [], hits: 0, pendingSourceIds: [] };
|
|
38
|
+
const hits: { score: number; line: string; sourceIds: readonly string[]; pending?: readonly string[] }[] = [];
|
|
39
|
+
// Each hit is short and carries its id; `pk get <id>` reads the rest (trial, 2026-09-18: a 39 KB known part, mostly
|
|
40
|
+
// not about the question, buried the few lines that were).
|
|
41
|
+
const short = (text: string | null | undefined) => { const x = (text ?? '').replace(/\s+/g, ' ').trim(); return x.length > 320 ? `${x.slice(0, 320)}…` : x; };
|
|
42
|
+
// An entry waiting for changed material says so, and which changes and how they reach it (Spec §7.6): what is known may
|
|
43
|
+
// be out of date, and the investigation reads them (AC-25). A mark written before the changes were named gives the documents.
|
|
44
|
+
const waiting = (ids: readonly string[] | undefined, waits?: readonly PendingWait[]) => {
|
|
45
|
+
if (waits?.length) return ` [Update pending: waiting for ${waits.slice(0, 4).map((w) => `${w.label}${w.reasons.length ? ` (${w.reasons.map((r) => r.detail).join('; ')})` : ''}`).join(', ')}${waits.length > 4 ? ` and ${waits.length - 4} more` : ''}; this may be out of date]`;
|
|
46
|
+
if (!ids?.length) return '';
|
|
47
|
+
const docs = [...new Set(ids.map((id) => { const a = store.sources.get(id)?.anchor; return !a ? id : a.kind === 'file' ? a.path.split(/[\\/]/).pop()! : a.kind === 'session' ? `${a.host} session ${a.sessionId.slice(0, 8)}` : a.kind === 'commit' ? `commit ${a.commit.slice(0, 8)}` : a.kind; }))];
|
|
48
|
+
return ` [Update pending: waiting for changes in ${docs.slice(0, 4).join(', ')}${docs.length > 4 ? ` and ${docs.length - 4} more` : ''}; this may be out of date]`;
|
|
49
|
+
};
|
|
50
|
+
for (const n of store.notes.filter((x) => x.status === 'Current')) {
|
|
51
|
+
const v = n.versions[n.versions.length - 1]!;
|
|
52
|
+
const s = score(`${v.title} ${v.preview} ${v.body.currentView ?? ''} ${v.body.keepAdjust ?? ''}`, ts);
|
|
53
|
+
if (s) hits.push({ score: s + 1, line: `Note “${v.title}” (\`${n.id}\`, ${v.ask}): ${short(`${v.preview}${v.body.currentView ? ` ${v.body.currentView}` : ''}`)}`, sourceIds: v.body.facts.flatMap((f) => f.sourceIds) });
|
|
54
|
+
}
|
|
55
|
+
for (const t of store.threads.all()) {
|
|
56
|
+
const s = score(`${t.title} ${t.ids.join(' ')} ${t.doing} ${t.results} ${t.unresolved}`, ts);
|
|
57
|
+
if (s) hits.push({ score: s, line: `Work “${t.title}” (\`${t.id}\`, ${t.progress}${t.validity !== 'Current' ? `, ${t.validity}` : ''})${waiting(t.pendingSourceIds, t.waitsFor)}: ${short(t.doing)} Results: ${short(t.results) || '—'} Unresolved: ${short(t.unresolved) || '—'}`, sourceIds: t.factRecordIds.flatMap((f) => store.facts.get(f)?.aboutSourceIds ?? []).slice(0, 6), pending: t.pendingSourceIds });
|
|
58
|
+
}
|
|
59
|
+
for (const r of store.reference.all()) {
|
|
60
|
+
const s = score(`${r.name} ${r.ids.join(' ')} ${r.text} ${r.quote ?? ''}`, ts);
|
|
61
|
+
if (s) hits.push({ score: s, line: `${r.category} “${r.name}” (\`${r.id}\`, ${r.validity}${r.basis === 'Inferred' ? ', Inferred' : ''})${waiting(r.pendingSourceIds, r.waitsFor)}: ${short(r.text)}${r.quote ? ` Owner: “${short(r.quote)}”` : ''}`, sourceIds: r.sourceIds, pending: r.pendingSourceIds ?? [] });
|
|
62
|
+
}
|
|
63
|
+
for (const a of store.areas.all()) {
|
|
64
|
+
const s = score(`${a.effectNow} ${a.gaps}`, ts);
|
|
65
|
+
if (s) hits.push({ score: s, line: `Area ${store.reference.get(a.referenceId)?.name ?? a.referenceId} (\`${a.referenceId}\`)${waiting(a.pendingSourceIds, a.waitsFor)}: effect now — ${short(a.effectNow)} Gaps — ${short(a.gaps)}`, sourceIds: [], pending: a.pendingSourceIds });
|
|
66
|
+
}
|
|
67
|
+
for (const f of store.facts.all()) {
|
|
68
|
+
for (const st of f.statements) {
|
|
69
|
+
const s = score(st.text, ts);
|
|
70
|
+
if (s >= 2) hits.push({ score: s - 1, line: `${st.type} (fact record \`${f.id}\`): ${short(st.text)}`, sourceIds: st.sourceIds });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
hits.sort((a, b) => b.score - a.score);
|
|
74
|
+
const top = hits.slice(0, 6);
|
|
75
|
+
const sourceIds = [...new Set(top.flatMap((h) => h.sourceIds))].slice(0, 12);
|
|
76
|
+
const lines = top.map((h) => `- ${h.line}`);
|
|
77
|
+
const sources = sourceIds.map((id, i) => { const s = store.sources.get(id); return `[${i + 1}] ${s ? `${s.title} — ${anchorLabel(s.anchor)}` : id}`; });
|
|
78
|
+
const pendingSourceIds = [...new Set(top.flatMap((h) => h.pending ?? []))];
|
|
79
|
+
return { text: top.length ? `${lines.join('\n')}${sources.length ? `\n\nSources:\n${sources.join('\n')}` : ''}` : '', sourceIds, hits: top.length, pendingSourceIds };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The project a working directory belongs to (§7.2): its scope, including worktrees and subdirectories. The directory
|
|
84
|
+
* is taken in the file system's own spelling, the one every location is kept in: an agent that works in the project
|
|
85
|
+
* through a junction or a `subst` drive asks from there.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveProject(projects: readonly Project[], cwd: string): Project | null {
|
|
88
|
+
const path = canonicalPath(cwd);
|
|
89
|
+
for (const p of projects) {
|
|
90
|
+
for (const item of p.scope) {
|
|
91
|
+
if (item.relation === 'Excluded' || item.category === 'Session source' || item.missing) continue;
|
|
92
|
+
if (isWithin(item.path, path)) return p;
|
|
93
|
+
}
|
|
94
|
+
for (const loc of p.locations) if (isWithin(loc, path)) return p;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|