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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pk takeover`: a takeover by the table in the three steps the trials proved (template/FILLING-IN.md, "Who fills
|
|
3
|
+
* what"; D107). The reader of the owner's words fills `ownerWants` from the session material alone; the reader of the
|
|
4
|
+
* project fills the rest of the table from a copy of the project alone; the main agent sees both, merges them, links
|
|
5
|
+
* each want to what carries it out, and writes where the owner and the project part ways.
|
|
6
|
+
*
|
|
7
|
+
* The two routes are kept apart the way the owner keeps trials apart: by physical separation. Each works in its own
|
|
8
|
+
* folder holding only its material — the session material for the first, a COPY of the project for the second — and
|
|
9
|
+
* neither folder is the live project. All of it is laid out in a work folder in ProjectKeeper's own folder for the
|
|
10
|
+
* project, never inside the project, since the material carries the owner's words and the project may be public.
|
|
11
|
+
*
|
|
12
|
+
* `prepareTakeover` lays the work folder out and writes each step's instructions, so that any agent (Claude Code,
|
|
13
|
+
* Codex, Kimi…) can be run on the three steps by the owner. `runTakeover` also runs them headless with Codex CLI, the
|
|
14
|
+
* way the trials ran them, then checks the merged table, puts it where the table lives and takes it in (import.ts).
|
|
15
|
+
* There is no Claude runner: running a Claude subscription from a third-party tool is against Anthropic's terms, so a
|
|
16
|
+
* Claude Code user runs the three steps from the prepared folders in their own Claude Code.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import { copyFileSync, cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { projectDir as homeProjectDir, tableDir } from '../store/paths.ts';
|
|
23
|
+
import { git } from '../util/git.ts';
|
|
24
|
+
import { canonicalPath, isWithin, pathKey } from '../util/paths.ts';
|
|
25
|
+
import { checkOwnerPartFile, checkTableFile, readSessionMaterial, SCHEMA_FILE, type TableProblem } from './check.ts';
|
|
26
|
+
import { importTable, type ImportResult } from './import.ts';
|
|
27
|
+
import { withTableProject } from './project-home.ts';
|
|
28
|
+
import { exportSessionMaterial, JSONL_FILE, MARKDOWN_FILE } from './sessions.ts';
|
|
29
|
+
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const appRoot = resolve(here, '..', '..');
|
|
32
|
+
const TEMPLATE_DIR = dirname(SCHEMA_FILE);
|
|
33
|
+
|
|
34
|
+
export const OWNER_FOLDER = 'owner';
|
|
35
|
+
export const READER_FOLDER = 'project-reader';
|
|
36
|
+
export const INSTRUCTIONS_FILE = 'instructions.md';
|
|
37
|
+
export const MAIN_INSTRUCTIONS_FILE = 'main-instructions.md';
|
|
38
|
+
export const OWNER_PART_FILE = 'owner-part.json';
|
|
39
|
+
/** The merged table the main agent writes at the top of the work folder; route 2's own table stays as it was written. */
|
|
40
|
+
export const MAIN_TABLE_FILE = 'project.json';
|
|
41
|
+
/** Where a headless run keeps each step's events, last message and times; outside both routes' folders. */
|
|
42
|
+
export const RUNS_FOLDER = 'runs';
|
|
43
|
+
|
|
44
|
+
/** Where the table lives once taken in: in the project (a private project may) or in ProjectKeeper's own folder. */
|
|
45
|
+
export type TableIn = 'home' | 'project';
|
|
46
|
+
export type StepName = 'owner' | 'project-reader' | 'main';
|
|
47
|
+
|
|
48
|
+
export interface TakeoverOptions {
|
|
49
|
+
/** The project folder. */
|
|
50
|
+
readonly projectDir: string;
|
|
51
|
+
/** The ProjectKeeper home; by default the user's. */
|
|
52
|
+
readonly home?: string;
|
|
53
|
+
/** The project's name when it is added; by default the folder's. */
|
|
54
|
+
readonly name?: string;
|
|
55
|
+
/** Where the taken-in table lives; by default `home`, safe for a public project (D107). */
|
|
56
|
+
readonly tableIn?: TableIn;
|
|
57
|
+
/** The moment the work folder is named after and the project copy is dated by; now by default. */
|
|
58
|
+
readonly now?: Date;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TakeoverRepo {
|
|
62
|
+
/** Relative to the project folder, with forward slashes; "." for the folder itself. */
|
|
63
|
+
readonly path: string;
|
|
64
|
+
readonly commit: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TakeoverStep {
|
|
68
|
+
readonly name: StepName;
|
|
69
|
+
/** The folder the agent works in, and the only one it is given. */
|
|
70
|
+
readonly folder: string;
|
|
71
|
+
readonly instructions: string;
|
|
72
|
+
/** What the step writes. */
|
|
73
|
+
readonly output: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PreparedTakeover {
|
|
77
|
+
readonly projectId: string;
|
|
78
|
+
readonly projectName: string;
|
|
79
|
+
readonly projectDir: string;
|
|
80
|
+
readonly home: string;
|
|
81
|
+
readonly added: boolean;
|
|
82
|
+
readonly workDir: string;
|
|
83
|
+
readonly steps: { readonly owner: TakeoverStep; readonly reader: TakeoverStep; readonly main: TakeoverStep };
|
|
84
|
+
/** The session material in the owner folder; `--sessions` for the checks and the import. */
|
|
85
|
+
readonly sessionsFile: string;
|
|
86
|
+
/** The copy of the project route 2 reads, and the folder its table's paths are relative to. */
|
|
87
|
+
readonly readerProject: string;
|
|
88
|
+
readonly repos: readonly TakeoverRepo[];
|
|
89
|
+
readonly ownerMessages: number;
|
|
90
|
+
readonly sessions: number;
|
|
91
|
+
/** Entries of the project the copy left out (what git ignores, node_modules, links), relative to the project. */
|
|
92
|
+
readonly skipped: readonly string[];
|
|
93
|
+
readonly tableIn: TableIn;
|
|
94
|
+
/** Where the table goes when it is taken in. */
|
|
95
|
+
readonly destination: string;
|
|
96
|
+
/** The exact command that runs `pk`, as the instructions give it. */
|
|
97
|
+
readonly pk: string;
|
|
98
|
+
/** The command that takes the table in once it stands at `destination`. */
|
|
99
|
+
readonly importCommand: string;
|
|
100
|
+
readonly warnings: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ───────────────────────── the project copy ─────────────────────────
|
|
104
|
+
|
|
105
|
+
const slash = (p: string): string => p.split('\\').join('/');
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Copy the project for route 2, and find its repositories on the way: every folder holding `.git` is one, recorded with
|
|
109
|
+
* its HEAD. Each repository's `.git` is copied whole, so the reader has the history. Left out:
|
|
110
|
+
* - what a repository ignores (`git ls-files --others --ignored --directory`): dependencies, build output, and local
|
|
111
|
+
* material that is not the project as it is kept — which can hold the owner's words (exported sessions, trial
|
|
112
|
+
* folders) and so would break the separation of the two routes; a folder git ignores that is itself a repository is
|
|
113
|
+
* still copied;
|
|
114
|
+
* - every `node_modules`, ignored or not;
|
|
115
|
+
* - `projectkeeper/` at the top, an earlier table, which may quote the owner (the reader writes its own there);
|
|
116
|
+
* - symbolic links, which may lead out of the project;
|
|
117
|
+
* - the folders in `avoid` (a home or a work folder that lies inside the project).
|
|
118
|
+
*/
|
|
119
|
+
export function copyProject(src: string, dest: string, avoid: readonly string[] = []): { repos: TakeoverRepo[]; skipped: string[]; warnings: string[] } {
|
|
120
|
+
const repos: TakeoverRepo[] = [];
|
|
121
|
+
const skipped: string[] = [];
|
|
122
|
+
const warnings: string[] = [];
|
|
123
|
+
const ignored = new Set<string>();
|
|
124
|
+
const avoided = new Set(avoid.map((a) => pathKey(a)));
|
|
125
|
+
const walk = (rel: string) => {
|
|
126
|
+
const dir = rel ? join(src, rel) : src;
|
|
127
|
+
mkdirSync(rel ? join(dest, rel) : dest, { recursive: true });
|
|
128
|
+
if (existsSync(join(dir, '.git'))) {
|
|
129
|
+
const head = git(dir, ['rev-parse', '--short', 'HEAD']);
|
|
130
|
+
if (head.ok && head.out.trim()) repos.push({ path: rel || '.', commit: head.out.trim() });
|
|
131
|
+
else warnings.push(`${rel || 'The project folder'} is a git repository with no commit; the table cannot stand on it.`);
|
|
132
|
+
const listed = git(dir, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--directory'], 60_000);
|
|
133
|
+
if (listed.ok) for (const entry of listed.out.split('\0')) if (entry) ignored.add(pathKey(resolve(dir, entry.replace(/\/$/, ''))));
|
|
134
|
+
}
|
|
135
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
const from = join(dir, entry.name);
|
|
137
|
+
const relEntry = rel ? `${rel}/${entry.name}` : entry.name;
|
|
138
|
+
const to = join(dest, relEntry);
|
|
139
|
+
if (entry.isSymbolicLink()) { skipped.push(relEntry); continue; }
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
if (entry.name === '.git') { cpSync(from, to, { recursive: true }); continue; }
|
|
142
|
+
const isRepo = existsSync(join(from, '.git'));
|
|
143
|
+
if (entry.name === 'node_modules' || avoided.has(pathKey(from)) || (!rel && entry.name === 'projectkeeper')
|
|
144
|
+
|| (ignored.has(pathKey(from)) && !isRepo)) { skipped.push(`${relEntry}/`); continue; }
|
|
145
|
+
walk(relEntry);
|
|
146
|
+
} else if (entry.isFile()) {
|
|
147
|
+
if (ignored.has(pathKey(from))) { skipped.push(relEntry); continue; }
|
|
148
|
+
copyFileSync(from, to);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
walk('');
|
|
153
|
+
return { repos, skipped, warnings };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ───────────────────────── the instructions ─────────────────────────
|
|
157
|
+
// The trials' instructions, in the owner's language and as the owner read them, with the project filled in.
|
|
158
|
+
|
|
159
|
+
interface InstructionFacts {
|
|
160
|
+
readonly projectName: string;
|
|
161
|
+
readonly date: string;
|
|
162
|
+
readonly repos: readonly TakeoverRepo[];
|
|
163
|
+
readonly ownerMessages: number;
|
|
164
|
+
readonly sessions: number;
|
|
165
|
+
readonly pk: string;
|
|
166
|
+
readonly workFolderName: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const where = (): string => slash(appRoot);
|
|
170
|
+
|
|
171
|
+
export function ownerInstructions(f: InstructionFacts): string {
|
|
172
|
+
return `# 任务:从 owner 的原话里,填 ProjectKeeper 表的 \`ownerWants\`
|
|
173
|
+
|
|
174
|
+
这个文件夹里有:
|
|
175
|
+
- \`${MARKDOWN_FILE}\`:软件项目「${f.projectName}」的 agent 会话里,owner 说的全部 ${f.ownerMessages} 句话(来自 ${f.sessions} 个会话),按时间排。每句前面附着它回应的那条 agent 最后输出(只有正文,没有 thinking 和工具调用)。每条都标了会话名和第几句。
|
|
176
|
+
- \`${JSONL_FILE}\`:同一份材料,给检查命令用。
|
|
177
|
+
- \`template/FILLING-IN.md\`:ProjectKeeper 表的填写说明;\`template/projectkeeper.schema.json\` 是格式;\`template/examples/papertrail.json\` 是一份填好的样例。
|
|
178
|
+
|
|
179
|
+
你是「谁填什么」里的第一步:**只读原话这一路**。
|
|
180
|
+
|
|
181
|
+
## 要做的
|
|
182
|
+
|
|
183
|
+
1. 读 \`template/FILLING-IN.md\`,特别是 \`ownerWants\`、\`notes\`、Sources 三节。
|
|
184
|
+
2. 读完全部会话材料。怎么读、按什么顺序读,由你定。
|
|
185
|
+
3. 写 \`${OWNER_PART_FILE}\`,只有两个字段:
|
|
186
|
+
|
|
187
|
+
\`\`\`json
|
|
188
|
+
{ "ownerWants": [ … ], "notes": [ … ] }
|
|
189
|
+
\`\`\`
|
|
190
|
+
|
|
191
|
+
- \`ownerWants\` 照填写说明写。
|
|
192
|
+
- \`said\` 里的每句都要带 \`quote\`,并且逐字照抄。
|
|
193
|
+
- owner 回答「对」「可以」这类短句时,认可的是前一条 agent 输出里的哪一部分,写进 \`approvedProposal\`,\`source\` 指向那条 agent 消息。
|
|
194
|
+
- \`reflectedIn\` 一律写成空数组 \`[]\`。项目这一边由别人读,最后由 main agent 来连。
|
|
195
|
+
- \`notes\` 只写你从原话里看出来、需要 owner 注意的事。比如 owner 前后矛盾、没有定论、说了又没再提。
|
|
196
|
+
- \`by\` 写 \`"Owner's words"\`。
|
|
197
|
+
- \`on\` 只能指向你自己写的 \`ownerWants\` 编号。
|
|
198
|
+
4. 运行下面这条命令,改到它说 \`no problems\` 为止:
|
|
199
|
+
|
|
200
|
+
\`\`\`
|
|
201
|
+
${f.pk} check --owner-part ${OWNER_PART_FILE} --sessions ${JSONL_FILE}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## 边界
|
|
205
|
+
|
|
206
|
+
- 只在这个文件夹里活动。不要打开这个文件夹外面的任何东西;材料里提到的路径也不要去看。检查命令会读 \`${where()}\` 里的格式程序,这是允许的。
|
|
207
|
+
- 没有网络。
|
|
208
|
+
|
|
209
|
+
做完后,最后一条消息用三五行说:多少件、其中几件已被取代、几件没有定论、写了几条 note。
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const repoLine = (r: TakeoverRepo): string => (r.path === '.' ? `- \`project/\` 本身,HEAD 是 \`${r.commit}\`;` : `- \`project/${r.path}/\`,HEAD 是 \`${r.commit}\`;`);
|
|
214
|
+
|
|
215
|
+
export function readerInstructions(f: InstructionFacts): string {
|
|
216
|
+
const asOf = `[${f.repos.map((r) => `{ "path": ${JSON.stringify(r.path)}, "commit": "${r.commit}" }`).join(', ')}]`;
|
|
217
|
+
const lines = f.repos.map(repoLine);
|
|
218
|
+
if (lines.length) lines[lines.length - 1] = lines[lines.length - 1]!.replace(/;$/, '。');
|
|
219
|
+
return `# 任务:读一个项目,填 ProjectKeeper 表
|
|
220
|
+
|
|
221
|
+
\`project/\` 是软件项目「${f.projectName}」的一份副本,时间停在 ${f.date}。里面有${f.repos.length === 1 ? '一' : ` ${f.repos.length} `}个 git 仓库:
|
|
222
|
+
${lines.join('\n')}
|
|
223
|
+
|
|
224
|
+
\`template/FILLING-IN.md\` 是 ProjectKeeper 表的填写说明,\`template/projectkeeper.schema.json\` 是格式,\`template/examples/papertrail.json\` 是一份填好的样例。
|
|
225
|
+
|
|
226
|
+
你是「谁填什么」里的第二步:**只读项目这一路**。
|
|
227
|
+
|
|
228
|
+
## 要做的
|
|
229
|
+
|
|
230
|
+
1. 读 \`template/FILLING-IN.md\`。
|
|
231
|
+
2. 读懂这个项目。读什么、按什么顺序、读多深,由你定:文档、git 历史、代码都可以用,shell 随便用。
|
|
232
|
+
3. 把表写到 \`project/projectkeeper/project.json\`。
|
|
233
|
+
- \`project.asOf.repos\` 写 \`${asOf}\`。
|
|
234
|
+
- \`ownerWants\` 写空数组 \`[]\`。owner 的原话由另一路从会话里读,最后由 main agent 合进来。
|
|
235
|
+
- 其余各部分照填写说明写。文档里逐字引用 owner 的话,可以作为出处的 \`quote\` 用。
|
|
236
|
+
- \`notes\` 的 \`by\` 写 \`"Project"\`。
|
|
237
|
+
4. 运行下面这条命令,改到它说 \`no problems\` 为止:
|
|
238
|
+
|
|
239
|
+
\`\`\`
|
|
240
|
+
${f.pk} check project/projectkeeper/project.json --project project
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## 边界
|
|
244
|
+
|
|
245
|
+
- 只在这个文件夹里活动,不要打开它外面的任何东西。例外只有上面那条检查命令,它会读 \`${where()}\` 里的检查程序。
|
|
246
|
+
- 文档里提到的、这个文件夹外面的路径不要去看。
|
|
247
|
+
- 除了 \`project/projectkeeper/\` 这个新文件夹,\`project/\` 里的东西一律不要改。
|
|
248
|
+
- 没有网络。
|
|
249
|
+
|
|
250
|
+
做完后,最后一条消息用三五行说:各部分各有多少条,以及你认为这个项目现在最不对劲的三件事。
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function mainInstructions(f: InstructionFacts): string {
|
|
255
|
+
return `# 任务:把两路合成一张 ProjectKeeper 表
|
|
256
|
+
|
|
257
|
+
你是「谁填什么」里的第三步:**main agent**。前两步已经各自做完:
|
|
258
|
+
- \`${OWNER_FOLDER}/${OWNER_PART_FILE}\`:一个 agent 只读 owner 的会话原话(\`${OWNER_FOLDER}/${MARKDOWN_FILE}\`),填好的 \`ownerWants\` 和它的 notes。
|
|
259
|
+
- \`${READER_FOLDER}/project/projectkeeper/project.json\`:另一个 agent 只读项目(\`${READER_FOLDER}/project/\`,时间停在 ${f.date}),填好的其余各部分和它的 notes,\`ownerWants\` 是空的。
|
|
260
|
+
|
|
261
|
+
两路互相没看过对方。填写说明在 \`${READER_FOLDER}/template/FILLING-IN.md\`。
|
|
262
|
+
|
|
263
|
+
## 要做的
|
|
264
|
+
|
|
265
|
+
1. 读填写说明的「Who fills what」一节,再读两路的产出。需要核实时,可以回到会话材料和项目里查。
|
|
266
|
+
2. 以 \`${READER_FOLDER}/project/projectkeeper/project.json\` 为底,把 \`${OWNER_PART_FILE}\` 的 \`ownerWants\` 和它的 notes 合进去,写成这个文件夹顶层的 \`${MAIN_TABLE_FILE}\`。两路的原文件都不要改。两路的 notes 原样保留,可以修正明显的错,不删。
|
|
267
|
+
3. 给每件 \`ownerWants\` 填 \`reflectedIn\`:项目里哪些目标、区域、需求、设计、决定、工作项在落实它。
|
|
268
|
+
4. 两边对不上的地方,写 note:\`by\` 写 \`"Main agent"\`,\`kind\` 写 \`"Owner and project differ"\`。比如:
|
|
269
|
+
- owner 要的、项目里没有;
|
|
270
|
+
- 项目里有的、owner 没要过或已经推翻了;
|
|
271
|
+
- owner 已经答过、文档里还写着待定;
|
|
272
|
+
- 项目里写成现行的、owner 后来改口了。
|
|
273
|
+
|
|
274
|
+
每条 note 的 \`on\` 同时指向那件 want 和项目里的对象;\`sources\` 两边都引。
|
|
275
|
+
5. 运行下面这条命令,改到它说 \`no problems\` 为止:
|
|
276
|
+
|
|
277
|
+
\`\`\`
|
|
278
|
+
${f.pk} check ${MAIN_TABLE_FILE} --project ${READER_FOLDER}/project --sessions ${OWNER_FOLDER}/${JSONL_FILE}
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
## 边界
|
|
282
|
+
|
|
283
|
+
- 只在这个文件夹(\`${f.workFolderName}/\`)里活动,不要打开它外面的任何东西。例外只有上面那条检查命令。
|
|
284
|
+
- 除了 \`${MAIN_TABLE_FILE}\`,其他文件一律不要改。
|
|
285
|
+
- 没有网络。
|
|
286
|
+
|
|
287
|
+
做完后,最后一条消息用三五行说:合并后各部分各有多少条、你写了几条「对不上」的 note,以及你认为 owner 最该先看的三条。
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ───────────────────────── prepare ─────────────────────────
|
|
292
|
+
|
|
293
|
+
const quoteArg = (a: string): string => (/[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
|
|
294
|
+
/** `pk` as the instructions give it: this installation's own entry, run by node, so it works wherever the agent is. */
|
|
295
|
+
export const pkCommand = (): string => `node ${quoteArg(slash(join(appRoot, 'bin', 'pk.mjs')))}`;
|
|
296
|
+
|
|
297
|
+
function stamp(now: Date): string {
|
|
298
|
+
return now.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Lay out the work folder: `owner/` with the session material, `project-reader/` with a copy of the project, a copy of
|
|
303
|
+
* the template in each, each route's instructions in its folder and the main agent's at the top. Nothing is written
|
|
304
|
+
* into the project. The project is found by its folder or added and read in, as `pk sessions` does.
|
|
305
|
+
*/
|
|
306
|
+
export async function prepareTakeover(options: TakeoverOptions): Promise<PreparedTakeover> {
|
|
307
|
+
const projectDir = canonicalPath(options.projectDir);
|
|
308
|
+
const tableIn = options.tableIn ?? 'home';
|
|
309
|
+
const now = options.now ?? new Date();
|
|
310
|
+
return withTableProject({ projectDir, home: options.home, name: options.name ?? basename(projectDir) }, async ({ app, project, home, added, warnings }) => {
|
|
311
|
+
const ledger = app.ledger.ledger(project.id);
|
|
312
|
+
if (!ledger) throw new Error(`The ledger of ${project.name} could not be built, so there is no session material for the owner's words.${warnings.length ? ` ${warnings.join(' ')}` : ''}`);
|
|
313
|
+
const tableFolder = tableDir(project.id, home);
|
|
314
|
+
let workDir = join(tableFolder, `takeover-${stamp(now)}`);
|
|
315
|
+
for (let n = 2; existsSync(workDir); n++) workDir = join(tableFolder, `takeover-${stamp(now)}-${n}`);
|
|
316
|
+
// The home may lie inside the project; the work folder never does.
|
|
317
|
+
const inside = project.locations.find((l) => isWithin(canonicalPath(l), workDir));
|
|
318
|
+
if (inside !== undefined) throw new Error(`${workDir} is inside the project folder ${inside}. The takeover's work folder holds the session material, which carries the owner's words, and the project may be a public repository, so it is laid out only outside the project: point --home at a ProjectKeeper folder outside it (D107).`);
|
|
319
|
+
|
|
320
|
+
const ownerDir = join(workDir, OWNER_FOLDER);
|
|
321
|
+
const readerDir = join(workDir, READER_FOLDER);
|
|
322
|
+
const readerProject = join(readerDir, 'project');
|
|
323
|
+
mkdirSync(ownerDir, { recursive: true });
|
|
324
|
+
const material = exportSessionMaterial(ledger, ownerDir);
|
|
325
|
+
const copied = copyProject(projectDir, readerProject, [home, workDir]);
|
|
326
|
+
if (!copied.repos.length) throw new Error(`${projectDir} holds no git repository with a commit. The table stands on the commit each repository was read at (project.asOf), so a takeover by the table needs one.`);
|
|
327
|
+
for (const dir of [ownerDir, readerDir]) cpSync(TEMPLATE_DIR, join(dir, 'template'), { recursive: true });
|
|
328
|
+
|
|
329
|
+
const pk = pkCommand();
|
|
330
|
+
const facts: InstructionFacts = {
|
|
331
|
+
projectName: project.name, date: now.toISOString().slice(0, 10), repos: copied.repos,
|
|
332
|
+
ownerMessages: material.ownerMessages, sessions: material.sessions, pk, workFolderName: basename(workDir),
|
|
333
|
+
};
|
|
334
|
+
const ownerFile = join(ownerDir, INSTRUCTIONS_FILE);
|
|
335
|
+
const readerFile = join(readerDir, INSTRUCTIONS_FILE);
|
|
336
|
+
const mainFile = join(workDir, MAIN_INSTRUCTIONS_FILE);
|
|
337
|
+
writeFileSync(ownerFile, ownerInstructions(facts));
|
|
338
|
+
writeFileSync(readerFile, readerInstructions(facts));
|
|
339
|
+
writeFileSync(mainFile, mainInstructions(facts));
|
|
340
|
+
|
|
341
|
+
const destination = tableIn === 'project' ? join(projectDir, 'projectkeeper', 'project.json') : join(tableFolder, 'project.json');
|
|
342
|
+
const importCommand = [pk, 'import', quoteArg(destination), '--project', quoteArg(projectDir), '--home', quoteArg(home), '--sessions', quoteArg(material.jsonl)].join(' ');
|
|
343
|
+
return {
|
|
344
|
+
projectId: project.id, projectName: project.name, projectDir, home, added, workDir,
|
|
345
|
+
steps: {
|
|
346
|
+
owner: { name: 'owner', folder: ownerDir, instructions: ownerFile, output: join(ownerDir, OWNER_PART_FILE) },
|
|
347
|
+
reader: { name: 'project-reader', folder: readerDir, instructions: readerFile, output: join(readerProject, 'projectkeeper', 'project.json') },
|
|
348
|
+
main: { name: 'main', folder: workDir, instructions: mainFile, output: join(workDir, MAIN_TABLE_FILE) },
|
|
349
|
+
},
|
|
350
|
+
sessionsFile: material.jsonl, readerProject, repos: copied.repos, ownerMessages: material.ownerMessages, sessions: material.sessions,
|
|
351
|
+
skipped: copied.skipped, tableIn, destination, pk, importCommand, warnings: [...warnings, ...copied.warnings],
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ───────────────────────── run with Codex ─────────────────────────
|
|
357
|
+
|
|
358
|
+
export interface RunTakeoverOptions extends TakeoverOptions {
|
|
359
|
+
/** Codex's model; absent, `-m` is left out and Codex uses its configured default. */
|
|
360
|
+
readonly model?: string;
|
|
361
|
+
/** Codex's reasoning effort; `high` by default, as in the trials. */
|
|
362
|
+
readonly effort?: string;
|
|
363
|
+
/** The program standing in for `codex` and its first arguments (tests); `['codex']` by default. */
|
|
364
|
+
readonly agentCommand?: readonly string[];
|
|
365
|
+
/** Progress, one line at a time. */
|
|
366
|
+
readonly log?: (line: string) => void;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface StepRun {
|
|
370
|
+
readonly step: StepName;
|
|
371
|
+
readonly exitCode: number | null;
|
|
372
|
+
/** The folder holding events.jsonl, stderr.txt, started.txt, ended.txt and last-message.md. */
|
|
373
|
+
readonly logDir: string;
|
|
374
|
+
readonly lastMessage: string | null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface TakeoverRun {
|
|
378
|
+
readonly prepared: PreparedTakeover;
|
|
379
|
+
readonly runs: readonly StepRun[];
|
|
380
|
+
/** The step the run stopped after without taking a table in, and why; null when the table was taken in. */
|
|
381
|
+
readonly stopped: { readonly after: StepName | 'check' | 'import'; readonly why: string } | null;
|
|
382
|
+
/** The merged table's problems when it did not check clean (or the import's). */
|
|
383
|
+
readonly problems: readonly TableProblem[];
|
|
384
|
+
/** Problems of a route's own output, reported and passed to the main agent, which holds the whole table to the check. */
|
|
385
|
+
readonly routeProblems: { readonly owner: readonly TableProblem[]; readonly reader: readonly TableProblem[] };
|
|
386
|
+
readonly imported: ImportResult | null;
|
|
387
|
+
/** An earlier table that stood at the destination, kept in the work folder before it was replaced. */
|
|
388
|
+
readonly previousTable: string | null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Codex's arguments as the trials ran it: memories off, writing only in the step's folder, events as JSON. */
|
|
392
|
+
export function codexArgs(folder: string, lastMessage: string, model: string | undefined, effort: string): string[] {
|
|
393
|
+
return [
|
|
394
|
+
'exec', ...(model ? ['-m', model] : []), '-c', `model_reasoning_effort=${effort}`,
|
|
395
|
+
'--disable', 'memories', '-c', 'memories.use_memories=false', '-c', 'memories.generate_memories=false',
|
|
396
|
+
'-s', 'workspace-write', '-C', folder, '--skip-git-repo-check', '--json', '-o', lastMessage, '-',
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** A Windows command line for cmd.exe: a `.cmd` shim such as npm's `codex.cmd` runs only through it. */
|
|
401
|
+
const cmdArg = (a: string): string => (a === '' || /[\s"&|<>^()]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a);
|
|
402
|
+
const utcSeconds = (): string => `${new Date().toISOString().slice(0, 19)}Z`;
|
|
403
|
+
|
|
404
|
+
/** One step, headless: the instructions on stdin, events to events.jsonl, stderr to stderr.txt, the times beside them. */
|
|
405
|
+
async function runStep(step: StepName, folder: string, instructions: string, logDir: string, options: RunTakeoverOptions): Promise<StepRun> {
|
|
406
|
+
mkdirSync(logDir, { recursive: true });
|
|
407
|
+
const lastFile = join(logDir, 'last-message.md');
|
|
408
|
+
const [program, ...first] = options.agentCommand ?? ['codex'];
|
|
409
|
+
const args = [...first, ...codexArgs(folder, lastFile, options.model, options.effort ?? 'high')];
|
|
410
|
+
writeFileSync(join(logDir, 'started.txt'), `${utcSeconds()}\n`);
|
|
411
|
+
const viaShell = process.platform === 'win32' && !/\.(exe|com)$/i.test(program!);
|
|
412
|
+
const events = createWriteStream(join(logDir, 'events.jsonl'));
|
|
413
|
+
const stderr = createWriteStream(join(logDir, 'stderr.txt'));
|
|
414
|
+
const exitCode = await new Promise<number | null>((done) => {
|
|
415
|
+
const child = viaShell
|
|
416
|
+
? spawn([program!, ...args].map(cmdArg).join(' '), { cwd: folder, shell: true, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
417
|
+
: spawn(program!, args, { cwd: folder, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
418
|
+
child.stdout.pipe(events);
|
|
419
|
+
child.stderr.pipe(stderr);
|
|
420
|
+
child.on('error', (e) => { stderr.write(`${e.message}\n`); done(null); });
|
|
421
|
+
child.on('close', (code) => done(code));
|
|
422
|
+
child.stdin.on('error', () => undefined);
|
|
423
|
+
child.stdin.end(readFileSync(instructions));
|
|
424
|
+
});
|
|
425
|
+
await Promise.all([new Promise((r) => events.end(r)), new Promise((r) => stderr.end(r))]);
|
|
426
|
+
writeFileSync(join(logDir, 'ended.txt'), `${utcSeconds()}\n`);
|
|
427
|
+
return { step, exitCode, logDir, lastMessage: existsSync(lastFile) ? readFileSync(lastFile, 'utf8') : null };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Prepare, then run the three steps with Codex: the two routes at once, then the main agent. The merged table is held
|
|
432
|
+
* to the whole check against the project and the session material; when it holds it is put at its destination and
|
|
433
|
+
* taken in, so the project's `takenOverByTable.file` names the destination. When it does not, nothing is taken in and
|
|
434
|
+
* the problems and the work folder are returned.
|
|
435
|
+
*/
|
|
436
|
+
export async function runTakeover(options: RunTakeoverOptions): Promise<TakeoverRun> {
|
|
437
|
+
const log = options.log ?? (() => undefined);
|
|
438
|
+
const prepared = await prepareTakeover(options);
|
|
439
|
+
const { owner, reader, main } = prepared.steps;
|
|
440
|
+
const runsDir = join(prepared.workDir, RUNS_FOLDER);
|
|
441
|
+
const empty = { owner: [] as TableProblem[], reader: [] as TableProblem[] };
|
|
442
|
+
const result = (rest: Omit<TakeoverRun, 'prepared'>): TakeoverRun => ({ prepared, ...rest });
|
|
443
|
+
|
|
444
|
+
log(`Routes 1 and 2 running: ${owner.folder} and ${reader.folder}`);
|
|
445
|
+
const routes = await Promise.all([
|
|
446
|
+
runStep('owner', owner.folder, owner.instructions, join(runsDir, 'owner'), options),
|
|
447
|
+
runStep('project-reader', reader.folder, reader.instructions, join(runsDir, 'project-reader'), options),
|
|
448
|
+
]);
|
|
449
|
+
for (const r of routes) log(` ${r.step}: exit ${r.exitCode ?? 'none (did not start)'}`);
|
|
450
|
+
const missing = [owner, reader].filter((s) => !existsSync(s.output));
|
|
451
|
+
if (missing.length) {
|
|
452
|
+
return result({ runs: routes, stopped: { after: missing[0]!.name, why: `${missing.map((s) => s.output).join(' and ')} ${missing.length === 1 ? 'was' : 'were'} not written` }, problems: [], routeProblems: empty, imported: null, previousTable: null });
|
|
453
|
+
}
|
|
454
|
+
const sessions = readSessionMaterial(prepared.sessionsFile);
|
|
455
|
+
const routeProblems = {
|
|
456
|
+
owner: checkOwnerPartFile(owner.output, sessions),
|
|
457
|
+
reader: checkTableFile(reader.output, { projectDir: prepared.readerProject }),
|
|
458
|
+
};
|
|
459
|
+
if (routeProblems.owner.length) log(` the owner part has ${routeProblems.owner.length} problem(s); the main agent's check covers them`);
|
|
460
|
+
if (routeProblems.reader.length) log(` route 2's table has ${routeProblems.reader.length} problem(s); the main agent's check covers them`);
|
|
461
|
+
|
|
462
|
+
log(`Main agent running: ${main.folder}`);
|
|
463
|
+
const mainRun = await runStep('main', main.folder, main.instructions, join(runsDir, 'main'), options);
|
|
464
|
+
log(` main: exit ${mainRun.exitCode ?? 'none (did not start)'}`);
|
|
465
|
+
const runs = [...routes, mainRun];
|
|
466
|
+
if (!existsSync(main.output)) return result({ runs, stopped: { after: 'main', why: `${main.output} was not written` }, problems: [], routeProblems, imported: null, previousTable: null });
|
|
467
|
+
|
|
468
|
+
// The whole check, against the project itself: what the import will hold the table to.
|
|
469
|
+
const problems = checkTableFile(main.output, { projectDir: prepared.projectDir, sessions });
|
|
470
|
+
if (problems.length) return result({ runs, stopped: { after: 'check', why: `the merged table has ${problems.length} problem${problems.length === 1 ? '' : 's'}` }, problems, routeProblems, imported: null, previousTable: null });
|
|
471
|
+
|
|
472
|
+
let previousTable: string | null = null;
|
|
473
|
+
if (existsSync(prepared.destination)) {
|
|
474
|
+
previousTable = join(prepared.workDir, 'previous-project.json');
|
|
475
|
+
copyFileSync(prepared.destination, previousTable);
|
|
476
|
+
}
|
|
477
|
+
mkdirSync(dirname(prepared.destination), { recursive: true });
|
|
478
|
+
copyFileSync(main.output, prepared.destination);
|
|
479
|
+
log(`Taking the table in from ${prepared.destination}`);
|
|
480
|
+
const imported = await importTable({ tableFile: prepared.destination, projectDir: prepared.projectDir, home: prepared.home, name: options.name, sessions });
|
|
481
|
+
if (imported.problems.length) return result({ runs, stopped: { after: 'import', why: 'the import refused the table' }, problems: imported.problems, routeProblems, imported, previousTable });
|
|
482
|
+
return result({ runs, stopped: null, problems: [], routeProblems, imported, previousTable });
|
|
483
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether git is there, said once when the workbench starts.
|
|
3
|
+
*
|
|
4
|
+
* ProjectKeeper reads a project's history through git. Without git every read-only call answers "not a repository"
|
|
5
|
+
* and a project with years of history looks like a folder of files, with nothing said. That is the usual state of a
|
|
6
|
+
* new Mac: `/usr/bin/git` is a stand-in until the Xcode command line tools are installed, and running it only offers
|
|
7
|
+
* to install them. An old git lacks options the reads use (`--path-format`, `--diff-merges`: 2.31).
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
/** The oldest git whose options the reads use. */
|
|
12
|
+
export const GIT_MIN = [2, 31] as const;
|
|
13
|
+
|
|
14
|
+
/** What `git --version` printed, or null when git could not be run. */
|
|
15
|
+
export function gitVersionOutput(): string | null {
|
|
16
|
+
try { return execFileSync('git', ['--version'], { encoding: 'utf8', timeout: 15_000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What to tell the owner about the git this machine has; null when there is nothing to say. */
|
|
20
|
+
export function gitNotice(versionOutput: string | null, platform: NodeJS.Platform = process.platform): string | null {
|
|
21
|
+
const install = platform === 'darwin' ? 'On macOS, install the Xcode command line tools (xcode-select --install) or git from Homebrew.'
|
|
22
|
+
: platform === 'win32' ? 'Install Git for Windows (https://git-scm.com/download/win).'
|
|
23
|
+
: 'Install git with your system’s package manager.';
|
|
24
|
+
const m = versionOutput === null ? null : /git version (\d+)\.(\d+)/.exec(versionOutput);
|
|
25
|
+
if (!m) return `git was not found. ProjectKeeper reads a project’s history with git: without it a repository is read as a folder of files, with no commits, no earlier versions of documents and no worktrees. ${install} Then start the workbench again.`;
|
|
26
|
+
const [major, minor] = [Number(m[1]), Number(m[2])];
|
|
27
|
+
if (major > GIT_MIN[0] || (major === GIT_MIN[0] && minor >= GIT_MIN[1])) return null;
|
|
28
|
+
return `This machine’s git is ${major}.${minor}; ProjectKeeper needs ${GIT_MIN[0]}.${GIT_MIN[1]} or later to read a project’s history in full. ${install}`;
|
|
29
|
+
}
|