@linimin/pi-letscook 0.1.44 → 0.1.46

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.
@@ -0,0 +1,458 @@
1
+ import * as fs from "node:fs";
2
+ import { promises as fsp } from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { CompletionStateSnapshot, JsonRecord } from "./types";
6
+
7
+ const PROTOCOL_ID = "completion";
8
+ const DEFAULT_TASK_TYPE = "completion-workflow";
9
+ const DEFAULT_EVALUATION_PROFILE = "completion-rubric-v1";
10
+
11
+ function isRecord(value: unknown): value is JsonRecord {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+
15
+ function asString(value: unknown): string | undefined {
16
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
17
+ }
18
+
19
+ function asStringArray(value: unknown): string[] {
20
+ return Array.isArray(value)
21
+ ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
22
+ : [];
23
+ }
24
+
25
+ function asNumber(value: unknown): number | undefined {
26
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
27
+ }
28
+
29
+ export function resolveFiles(root: string) {
30
+ const agentDir = path.join(root, ".agent");
31
+ const tmpDir = path.join(agentDir, "tmp");
32
+ return {
33
+ root,
34
+ agentDir,
35
+ tmpDir,
36
+ profilePath: path.join(agentDir, "profile.json"),
37
+ statePath: path.join(agentDir, "state.json"),
38
+ planPath: path.join(agentDir, "plan.json"),
39
+ activePath: path.join(agentDir, "active-slice.json"),
40
+ sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
41
+ stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
42
+ verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
43
+ compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
44
+ };
45
+ }
46
+
47
+ function walkUpForDir(startCwd: string, segments: string[]): string | undefined {
48
+ let current = path.resolve(startCwd);
49
+ while (true) {
50
+ const candidate = path.join(current, ...segments);
51
+ if (fs.existsSync(candidate)) return candidate;
52
+ const parent = path.dirname(current);
53
+ if (parent === current) return undefined;
54
+ current = parent;
55
+ }
56
+ }
57
+
58
+ function completionSearchRoots(startCwd: string): string[] {
59
+ return [...new Set([path.resolve(startCwd), path.resolve(process.cwd())])];
60
+ }
61
+
62
+ export function findCompletionRoot(startCwd: string): string | undefined {
63
+ for (const candidateRoot of completionSearchRoots(startCwd)) {
64
+ const profilePath = walkUpForDir(candidateRoot, [".agent", "profile.json"]);
65
+ if (profilePath) return path.dirname(path.dirname(profilePath));
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ export function findRepoRoot(startCwd: string): string | undefined {
71
+ for (const candidateRoot of completionSearchRoots(startCwd)) {
72
+ const gitPath = walkUpForDir(candidateRoot, [".git"]);
73
+ if (gitPath) return path.dirname(gitPath);
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ export function completionRootKey(snapshot: CompletionStateSnapshot | undefined, cwd: string): string {
79
+ return snapshot?.files.root ?? findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? path.resolve(cwd);
80
+ }
81
+
82
+ export async function readJson(filePath: string): Promise<JsonRecord | undefined> {
83
+ try {
84
+ const raw = await fsp.readFile(filePath, "utf8");
85
+ const parsed = JSON.parse(raw);
86
+ return isRecord(parsed) ? parsed : undefined;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ export async function readJsonl(filePath: string): Promise<JsonRecord[]> {
93
+ try {
94
+ const raw = await fsp.readFile(filePath, "utf8");
95
+ return raw
96
+ .split("\n")
97
+ .map((line) => line.trim())
98
+ .filter(Boolean)
99
+ .flatMap((line) => {
100
+ try {
101
+ const parsed = JSON.parse(line);
102
+ return isRecord(parsed) ? [parsed] : [];
103
+ } catch {
104
+ return [];
105
+ }
106
+ });
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+
112
+ export async function writeJsonFile(filePath: string, value: JsonRecord): Promise<void> {
113
+ await fsp.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
114
+ }
115
+
116
+ function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
117
+ const slices = plan?.candidate_slices;
118
+ return Array.isArray(slices) ? slices.filter(isRecord) : [];
119
+ }
120
+
121
+ function findActiveSlice(plan: JsonRecord | undefined, active: JsonRecord | undefined): JsonRecord | undefined {
122
+ const sliceId = asString(active?.slice_id);
123
+ if (!sliceId) return undefined;
124
+ return candidateSlices(plan).find((slice) => asString(slice.slice_id) === sliceId);
125
+ }
126
+
127
+ export async function loadCompletionSnapshot(startCwd: string): Promise<CompletionStateSnapshot | undefined> {
128
+ const root = findCompletionRoot(startCwd);
129
+ if (!root) return undefined;
130
+ const files = resolveFiles(root);
131
+ const profile = await readJson(files.profilePath);
132
+ if (asString(profile?.protocol_id) !== PROTOCOL_ID) return undefined;
133
+ const state = await readJson(files.statePath);
134
+ const plan = await readJson(files.planPath);
135
+ const active = await readJson(files.activePath);
136
+ const verificationEvidence = await readJson(files.verificationEvidencePath);
137
+ return {
138
+ files,
139
+ profile,
140
+ state,
141
+ plan,
142
+ active,
143
+ verificationEvidence,
144
+ activeSlice: findActiveSlice(plan, active),
145
+ };
146
+ }
147
+
148
+ export async function loadCompletionDataForReminder(startCwd: string) {
149
+ const snapshot = await loadCompletionSnapshot(startCwd);
150
+ if (!snapshot) return undefined;
151
+ const sliceHistory = await readJsonl(snapshot.files.sliceHistoryPath);
152
+ const stopHistory = await readJsonl(snapshot.files.stopHistoryPath);
153
+ return { snapshot, sliceHistory, stopHistory };
154
+ }
155
+
156
+ export async function pathExists(targetPath: string): Promise<boolean> {
157
+ try {
158
+ await fsp.access(targetPath);
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ export async function readText(filePath: string): Promise<string | undefined> {
166
+ try {
167
+ return await fsp.readFile(filePath, "utf8");
168
+ } catch {
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ export async function detectDocsSurfaces(root: string): Promise<string[]> {
174
+ const candidates = ["README.md", "docs/", "docs", "CHANGELOG.md"];
175
+ const found: string[] = [];
176
+ for (const candidate of candidates) {
177
+ if (await pathExists(path.join(root, candidate))) found.push(candidate.endsWith("/") ? candidate : candidate.replace(/\/$/, ""));
178
+ }
179
+ return found.length > 0 ? found : ["README.md"];
180
+ }
181
+
182
+ async function detectVerifierCommand(root: string): Promise<string | undefined> {
183
+ const packageJsonPath = path.join(root, "package.json");
184
+ const packageJson = await readJson(packageJsonPath);
185
+ if (packageJson) {
186
+ const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : undefined;
187
+ const packageManager = asString((packageJson as JsonRecord).packageManager) ?? "";
188
+ const runner = packageManager.startsWith("pnpm") ? "pnpm" : packageManager.startsWith("yarn") ? "yarn" : packageManager.startsWith("bun") ? "bun" : "npm";
189
+ if (scripts && asString(scripts.test)) return runner === "npm" ? "npm test" : `${runner} test`;
190
+ if (scripts && asString(scripts.check)) return runner === "npm" ? "npm run check" : `${runner} check`;
191
+ if (scripts && asString(scripts.lint)) return runner === "npm" ? "npm run lint" : `${runner} lint`;
192
+ }
193
+ if (await pathExists(path.join(root, "pnpm-lock.yaml"))) return "pnpm test";
194
+ if ((await pathExists(path.join(root, "bun.lockb"))) || (await pathExists(path.join(root, "bun.lock")))) return "bun test";
195
+ if (await pathExists(path.join(root, "yarn.lock"))) return "yarn test";
196
+ if (await pathExists(path.join(root, "Cargo.toml"))) return "cargo test";
197
+ if ((await pathExists(path.join(root, "pyproject.toml"))) || (await pathExists(path.join(root, "pytest.ini")))) return "pytest";
198
+ if (await pathExists(path.join(root, "go.mod"))) return "go test ./...";
199
+ if (await pathExists(path.join(root, "Makefile"))) return "make test";
200
+ return undefined;
201
+ }
202
+
203
+ export function buildProfileRecord(args: {
204
+ projectName: string;
205
+ requiredStopJudges: number;
206
+ priorityPolicyId?: string;
207
+ docsSurfaces: string[];
208
+ taskType?: string;
209
+ evaluationProfile?: string;
210
+ }): JsonRecord {
211
+ return {
212
+ schema_version: 1,
213
+ protocol_id: PROTOCOL_ID,
214
+ project_name: args.projectName,
215
+ required_stop_judges: args.requiredStopJudges,
216
+ priority_policy_id: args.priorityPolicyId ?? "completion-default",
217
+ task_type: args.taskType ?? DEFAULT_TASK_TYPE,
218
+ evaluation_profile: args.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
219
+ docs_surfaces: args.docsSurfaces,
220
+ };
221
+ }
222
+
223
+ export function defaultState(
224
+ missionAnchor: string,
225
+ routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
226
+ ): JsonRecord {
227
+ return {
228
+ schema_version: 1,
229
+ mission_anchor: missionAnchor,
230
+ current_phase: "reground",
231
+ continuation_policy: "continue",
232
+ continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
233
+ project_done: false,
234
+ task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
235
+ evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
236
+ requires_reground: true,
237
+ slices_since_last_reground: 0,
238
+ remaining_release_blockers: null,
239
+ remaining_high_value_gaps: null,
240
+ unsatisfied_contract_ids: [],
241
+ release_blocker_ids: [],
242
+ next_mandatory_action: "Reconcile canonical state from current repo truth",
243
+ next_mandatory_role: "completion-regrounder",
244
+ remaining_stop_judges: 3,
245
+ last_reground_at: null,
246
+ last_auditor_verdict: null,
247
+ contract_status: "unknown",
248
+ latest_completed_slice: null,
249
+ latest_verified_slice: null,
250
+ };
251
+ }
252
+
253
+ export function defaultPlan(
254
+ missionAnchor: string,
255
+ routing?: { taskType?: string; evaluationProfile?: string },
256
+ ): JsonRecord {
257
+ return {
258
+ schema_version: 1,
259
+ mission_anchor: missionAnchor,
260
+ task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
261
+ evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
262
+ last_reground_at: null,
263
+ plan_basis: "bootstrap",
264
+ candidate_slices: [],
265
+ };
266
+ }
267
+
268
+ export function defaultActiveSlice(
269
+ missionAnchor: string,
270
+ routing?: { taskType?: string; evaluationProfile?: string },
271
+ ): JsonRecord {
272
+ return {
273
+ schema_version: 1,
274
+ mission_anchor: missionAnchor,
275
+ task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
276
+ evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
277
+ status: "idle",
278
+ slice_id: null,
279
+ goal: null,
280
+ contract_ids: [],
281
+ acceptance_criteria: [],
282
+ priority: null,
283
+ why_now: null,
284
+ blocked_on: [],
285
+ locked_notes: [],
286
+ must_fix_findings: [],
287
+ implementation_surfaces: [],
288
+ verification_commands: [],
289
+ basis_commit: null,
290
+ remaining_contract_ids_before: [],
291
+ release_blocker_count_before: null,
292
+ high_value_gap_count_before: null,
293
+ };
294
+ }
295
+
296
+ export function defaultVerificationEvidence(): JsonRecord {
297
+ return {
298
+ schema_version: 1,
299
+ artifact_type: "completion-verification-evidence",
300
+ subject_type: "none",
301
+ slice_id: null,
302
+ goal: null,
303
+ contract_ids: [],
304
+ basis_commit: null,
305
+ head_sha: null,
306
+ verification_commands: [],
307
+ outcome: "not_recorded",
308
+ recorded_at: null,
309
+ summary: "No deterministic verification evidence is recorded yet because no selected slice or current-HEAD verification subject exists.",
310
+ };
311
+ }
312
+
313
+ export function buildAgentReadme(projectName: string): string {
314
+ return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/verification-evidence.json\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\n\`.agent/verification-evidence.json\` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
315
+ }
316
+
317
+ export function buildMission(projectName: string, missionAnchor: string): string {
318
+ return `# Mission\n\nProject: ${projectName}\n\nMission anchor:\n${missionAnchor}\n\nThis file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.\n`;
319
+ }
320
+
321
+ export function buildVerifyStopScript(verifierCommand?: string): string {
322
+ const repoCheck = verifierCommand
323
+ ? `echo "[completion] running repo-level verification: ${verifierCommand}"\n${verifierCommand}`
324
+ : `echo "[completion] no repo-specific verifier auto-detected; control-plane verification only"`;
325
+ return `#!/usr/bin/env bash\nset -euo pipefail\n\nbash .agent/verify_completion_control_plane.sh\n${repoCheck}\n`;
326
+ }
327
+
328
+ export function buildVerifyControlPlaneScript(): string {
329
+ const trackedScriptPath = path.resolve(__dirname, "..", "..", ".agent", "verify_completion_control_plane.sh");
330
+ if (fs.existsSync(trackedScriptPath)) {
331
+ return fs.readFileSync(trackedScriptPath, "utf8");
332
+ }
333
+ return `#!/usr/bin/env node
334
+ const fs = require('node:fs');
335
+
336
+ function fail(message) {
337
+ console.error(message);
338
+ process.exit(1);
339
+ }
340
+
341
+ function readJson(file) {
342
+ try {
343
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
344
+ } catch (error) {
345
+ fail('Failed to read ' + file + ': ' + error.message);
346
+ }
347
+ }
348
+
349
+ for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json', '.agent/verification-evidence.json']) {
350
+ readJson(file);
351
+ }
352
+ `;
353
+ }
354
+
355
+ async function ensureGitignore(root: string): Promise<boolean> {
356
+ const gitignorePath = path.join(root, ".gitignore");
357
+ const blockLines = [
358
+ "# completion protocol",
359
+ ".agent/*",
360
+ "!.agent/README.md",
361
+ "!.agent/mission.md",
362
+ "!.agent/profile.json",
363
+ "!.agent/verify_completion_stop.sh",
364
+ "!.agent/verify_completion_control_plane.sh",
365
+ ".agent/tmp/",
366
+ ];
367
+ const block = blockLines.join("\n");
368
+ const existing = (await pathExists(gitignorePath)) ? await fsp.readFile(gitignorePath, "utf8") : "";
369
+ const filteredLines = existing
370
+ .split(/\r?\n/)
371
+ .filter((line) => !blockLines.includes(line.trim()));
372
+ while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1]?.trim() === "") {
373
+ filteredLines.pop();
374
+ }
375
+ const base = filteredLines.join("\n").trimEnd();
376
+ const content = base.length > 0 ? `${base}\n\n${block}\n` : `${block}\n`;
377
+ if (content === existing) return false;
378
+ await fsp.writeFile(gitignorePath, content, "utf8");
379
+ return true;
380
+ }
381
+
382
+ export type ScaffoldResult = {
383
+ root: string;
384
+ created: string[];
385
+ updated: string[];
386
+ missionAnchor: string;
387
+ };
388
+
389
+ export async function scaffoldCompletionFiles(
390
+ root: string,
391
+ missionAnchor: string,
392
+ options?: { analysis?: { taskType?: string; evaluationProfile?: string }; continuationReason?: string },
393
+ ): Promise<ScaffoldResult> {
394
+ const files = resolveFiles(root);
395
+ const created: string[] = [];
396
+ const updated: string[] = [];
397
+ await fsp.mkdir(files.agentDir, { recursive: true });
398
+ await fsp.mkdir(path.join(files.agentDir, "tmp"), { recursive: true });
399
+ const projectName = path.basename(root);
400
+ const docsSurfaces = await detectDocsSurfaces(root);
401
+ const verifierCommand = await detectVerifierCommand(root);
402
+ const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
403
+ { path: path.join(files.agentDir, "README.md"), content: buildAgentReadme(projectName) },
404
+ { path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
405
+ {
406
+ path: files.profilePath,
407
+ content: `${JSON.stringify(buildProfileRecord({ projectName, requiredStopJudges: 3, docsSurfaces, taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n`,
408
+ },
409
+ { path: path.join(files.agentDir, "verify_completion_stop.sh"), content: buildVerifyStopScript(verifierCommand), executable: true },
410
+ { path: path.join(files.agentDir, "verify_completion_control_plane.sh"), content: buildVerifyControlPlaneScript(), executable: true },
411
+ {
412
+ path: files.statePath,
413
+ content: `${JSON.stringify(defaultState(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile, continuationReason: options?.continuationReason }), null, 2)}\n`,
414
+ },
415
+ { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
416
+ { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
417
+ { path: files.verificationEvidencePath, content: `${JSON.stringify(defaultVerificationEvidence(), null, 2)}\n` },
418
+ { path: files.sliceHistoryPath, content: "" },
419
+ { path: files.stopHistoryPath, content: "" },
420
+ ];
421
+ for (const file of trackedFiles) {
422
+ if (await pathExists(file.path)) continue;
423
+ await fsp.writeFile(file.path, file.content, "utf8");
424
+ if (file.executable) await fsp.chmod(file.path, 0o755);
425
+ created.push(path.relative(root, file.path));
426
+ }
427
+ if (await ensureGitignore(root)) updated.push(".gitignore");
428
+ return { root, created, updated, missionAnchor };
429
+ }
430
+
431
+ export function currentTaskType(snapshot: CompletionStateSnapshot): string | undefined {
432
+ return (
433
+ asString(snapshot.active?.task_type) ??
434
+ asString(snapshot.state?.task_type) ??
435
+ asString(snapshot.plan?.task_type) ??
436
+ asString(snapshot.profile?.task_type)
437
+ );
438
+ }
439
+
440
+ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): string | undefined {
441
+ return (
442
+ asString(snapshot.active?.evaluation_profile) ??
443
+ asString(snapshot.state?.evaluation_profile) ??
444
+ asString(snapshot.plan?.evaluation_profile) ??
445
+ asString(snapshot.profile?.evaluation_profile)
446
+ );
447
+ }
448
+
449
+ export function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
450
+ return (
451
+ asString(snapshot.state?.mission_anchor) ??
452
+ asString(snapshot.plan?.mission_anchor) ??
453
+ asString(snapshot.active?.mission_anchor) ??
454
+ path.basename(snapshot.files.root)
455
+ );
456
+ }
457
+
458
+ export { asNumber, asString, asStringArray, isRecord };