@linimin/pi-letscook 0.1.30 → 0.1.32

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,356 @@
1
+ const fs = require("node:fs/promises");
2
+
3
+ const RUBRIC_DIMENSIONS = [
4
+ "Contract coverage",
5
+ "Correctness risk",
6
+ "Verification evidence",
7
+ "Docs/state parity",
8
+ ];
9
+
10
+ const REVIEWER_REQUIRED_FIELDS = [
11
+ "MISSION ANCHOR",
12
+ "Remaining contract IDs",
13
+ "Findings",
14
+ "Acceptable as-is",
15
+ "Smallest follow-up slice",
16
+ ];
17
+
18
+ const AUDITOR_REQUIRED_FIELDS = [
19
+ "MISSION ANCHOR",
20
+ "Remaining contract IDs",
21
+ "Why the project is still not done",
22
+ "Open top-level contract IDs",
23
+ "Blocker count",
24
+ "High-value gap count",
25
+ "Tracked and unignored worktree is clean",
26
+ "Worktree blockers",
27
+ "Next mandatory slice",
28
+ "Stale or conflicting canonical state",
29
+ "Plan truthfully captures remaining slice backlog",
30
+ ];
31
+
32
+ const STOP_JUDGE_REQUIRED_FIELDS = [
33
+ "MISSION ANCHOR",
34
+ "Remaining contract IDs",
35
+ "Can the project stop now",
36
+ "Exact remaining open top-level contract IDs",
37
+ "Blocker count",
38
+ "High-value gap count",
39
+ "Latest completed slice commit",
40
+ "Docs/config/runbooks match shipped behavior",
41
+ "Tracked and unignored worktree is clean",
42
+ "Brief justification",
43
+ ];
44
+
45
+ function asString(value) {
46
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
47
+ }
48
+
49
+ function parseReportFields(text) {
50
+ const fields = {};
51
+ for (const rawLine of text.split("\n")) {
52
+ const line = rawLine.trim();
53
+ if (!line) continue;
54
+ const normalized = line.replace(/^-\s*/, "").replace(/^`/, "").replace(/`$/, "");
55
+ const match = normalized.match(/^([A-Za-z][A-Za-z0-9 _\/-]*?):\s*(.*)$/);
56
+ if (!match) continue;
57
+ const [, key, value] = match;
58
+ fields[key.trim()] = value.trim();
59
+ }
60
+ return fields;
61
+ }
62
+
63
+ function parseYesNo(value) {
64
+ if (!value) return undefined;
65
+ const normalized = value.trim().toLowerCase();
66
+ if (normalized.startsWith("yes")) return true;
67
+ if (normalized.startsWith("no")) return false;
68
+ return undefined;
69
+ }
70
+
71
+ function parseFirstNumber(value) {
72
+ if (!value) return undefined;
73
+ const match = value.match(/-?\d+/);
74
+ if (!match) return undefined;
75
+ const parsed = Number.parseInt(match[0], 10);
76
+ return Number.isFinite(parsed) ? parsed : undefined;
77
+ }
78
+
79
+ function rubricVerdicts(reportFields) {
80
+ return RUBRIC_DIMENSIONS.map((dimension) => {
81
+ const value = reportFields[dimension];
82
+ const match = typeof value === "string" ? value.match(/^(pass|concern|fail)\s*-\s*(.+)$/i) : undefined;
83
+ return {
84
+ dimension,
85
+ verdict: match?.[1]?.toLowerCase(),
86
+ explanation: match?.[2]?.trim(),
87
+ raw: value,
88
+ };
89
+ });
90
+ }
91
+
92
+ function validateRequiredFields(reportFields, requiredFields, errors, role) {
93
+ for (const field of requiredFields) {
94
+ if (!(field in reportFields)) {
95
+ errors.push(`Missing required ${role} field: ${field}.`);
96
+ continue;
97
+ }
98
+ const value = reportFields[field];
99
+ if (field === "Rubric") continue;
100
+ if (typeof value !== "string") {
101
+ errors.push(`Malformed ${role} field: ${field}.`);
102
+ continue;
103
+ }
104
+ if (field === "Findings") continue;
105
+ if (value.trim().length === 0) {
106
+ errors.push(`Empty required ${role} field: ${field}.`);
107
+ }
108
+ }
109
+ }
110
+
111
+ function validateYesNoField(reportFields, field, errors, message) {
112
+ const parsed = parseYesNo(reportFields[field]);
113
+ if (parsed === undefined) errors.push(message);
114
+ return parsed;
115
+ }
116
+
117
+ function validateRoleReport(role, output, reportFields = parseReportFields(output)) {
118
+ const errors = [];
119
+ if (!asString(output)) {
120
+ errors.push(`Empty ${role} report output.`);
121
+ return { valid: false, errors, reportFields, rubric: [] };
122
+ }
123
+ if (!/^Rubric:\s*$/m.test(output)) {
124
+ errors.push(`Missing Rubric heading for ${role}.`);
125
+ }
126
+ const rubric = rubricVerdicts(reportFields);
127
+ for (const line of rubric) {
128
+ if (!line.raw) {
129
+ errors.push(`Missing rubric line for ${role}: ${line.dimension}.`);
130
+ continue;
131
+ }
132
+ if (!line.verdict || !line.explanation) {
133
+ errors.push(`Malformed rubric line for ${role}: ${line.dimension}. Expected pass|concern|fail - explanation.`);
134
+ }
135
+ }
136
+ const anyFail = rubric.some((line) => line.verdict === "fail");
137
+
138
+ if (role === "completion-reviewer") {
139
+ validateRequiredFields(reportFields, REVIEWER_REQUIRED_FIELDS, errors, role);
140
+ const acceptable = parseYesNo(reportFields["Acceptable as-is"]);
141
+ if (acceptable === undefined) errors.push("Reviewer output must answer 'Acceptable as-is' with yes or no.");
142
+ if (anyFail && acceptable === true) {
143
+ errors.push("Reviewer output cannot mark 'Acceptable as-is: yes' when any rubric line is fail.");
144
+ }
145
+ if (acceptable === false && !asString(reportFields["Smallest follow-up slice"])) {
146
+ errors.push("Reviewer output must include a smallest follow-up slice when acceptance is no.");
147
+ }
148
+ } else if (role === "completion-auditor") {
149
+ validateRequiredFields(reportFields, AUDITOR_REQUIRED_FIELDS, errors, role);
150
+ if (parseFirstNumber(reportFields["Blocker count"]) === undefined) {
151
+ errors.push("Auditor output must include a numeric Blocker count.");
152
+ }
153
+ if (parseFirstNumber(reportFields["High-value gap count"]) === undefined) {
154
+ errors.push("Auditor output must include a numeric High-value gap count.");
155
+ }
156
+ validateYesNoField(
157
+ reportFields,
158
+ "Tracked and unignored worktree is clean",
159
+ errors,
160
+ "Auditor output must answer 'Tracked and unignored worktree is clean' with yes or no.",
161
+ );
162
+ validateYesNoField(
163
+ reportFields,
164
+ "Stale or conflicting canonical state",
165
+ errors,
166
+ "Auditor output must answer 'Stale or conflicting canonical state' with yes or no.",
167
+ );
168
+ validateYesNoField(
169
+ reportFields,
170
+ "Plan truthfully captures remaining slice backlog",
171
+ errors,
172
+ "Auditor output must answer 'Plan truthfully captures remaining slice backlog' with yes or no.",
173
+ );
174
+ } else if (role === "completion-stop-judge") {
175
+ validateRequiredFields(reportFields, STOP_JUDGE_REQUIRED_FIELDS, errors, role);
176
+ const canStop = validateYesNoField(
177
+ reportFields,
178
+ "Can the project stop now",
179
+ errors,
180
+ "Stop-judge output must answer 'Can the project stop now' with yes or no.",
181
+ );
182
+ if (anyFail && canStop === true) {
183
+ errors.push("Stop-judge output cannot mark 'Can the project stop now: yes' when any rubric line is fail.");
184
+ }
185
+ if (parseFirstNumber(reportFields["Blocker count"]) === undefined) {
186
+ errors.push("Stop-judge output must include a numeric Blocker count.");
187
+ }
188
+ if (parseFirstNumber(reportFields["High-value gap count"]) === undefined) {
189
+ errors.push("Stop-judge output must include a numeric High-value gap count.");
190
+ }
191
+ validateYesNoField(
192
+ reportFields,
193
+ "Docs/config/runbooks match shipped behavior",
194
+ errors,
195
+ "Stop-judge output must answer 'Docs/config/runbooks match shipped behavior' with yes or no.",
196
+ );
197
+ validateYesNoField(
198
+ reportFields,
199
+ "Tracked and unignored worktree is clean",
200
+ errors,
201
+ "Stop-judge output must answer 'Tracked and unignored worktree is clean' with yes or no.",
202
+ );
203
+ }
204
+
205
+ return { valid: errors.length === 0, errors, reportFields, rubric };
206
+ }
207
+
208
+ async function readJsonl(filePath) {
209
+ try {
210
+ const raw = await fs.readFile(filePath, "utf8");
211
+ return raw
212
+ .split("\n")
213
+ .map((line) => line.trim())
214
+ .filter(Boolean)
215
+ .flatMap((line) => {
216
+ try {
217
+ const parsed = JSON.parse(line);
218
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? [parsed] : [];
219
+ } catch {
220
+ return [];
221
+ }
222
+ });
223
+ } catch {
224
+ return [];
225
+ }
226
+ }
227
+
228
+ async function appendJsonlRecord(filePath, record) {
229
+ await fs.mkdir(require("node:path").dirname(filePath), { recursive: true });
230
+ await fs.appendFile(filePath, `${JSON.stringify(record)}\n`, "utf8");
231
+ }
232
+
233
+ async function transcribeCanonicalRoleReport({ role, output, reportFields = parseReportFields(output), snapshotFiles, headSha, sliceId, recordedAt = Date.now() }) {
234
+ const result = { appended: [], skipped: [], errors: [] };
235
+
236
+ if (!snapshotFiles || !headSha) {
237
+ result.errors.push("Missing canonical snapshot files or git HEAD for transcription.");
238
+ return result;
239
+ }
240
+
241
+ if (role === "completion-reviewer" || role === "completion-auditor" || role === "completion-stop-judge") {
242
+ const validation = validateRoleReport(role, output, reportFields);
243
+ if (!validation.valid) {
244
+ result.errors.push(...validation.errors);
245
+ return result;
246
+ }
247
+ }
248
+
249
+ if (role === "completion-reviewer" || role === "completion-auditor") {
250
+ if (!sliceId) {
251
+ result.errors.push(`Missing slice_id for ${role} transcription.`);
252
+ return result;
253
+ }
254
+ const type = role === "completion-reviewer" ? "reviewed" : "audited";
255
+ const history = await readJsonl(snapshotFiles.sliceHistoryPath);
256
+ const duplicate = history.some((entry) => {
257
+ return entry.type === type && entry.slice_id === sliceId && entry.head_sha === headSha && entry.report_text === output.trim();
258
+ });
259
+ if (duplicate) {
260
+ result.skipped.push(`Skipped duplicate ${type} record for slice ${sliceId} at ${headSha.slice(0, 12)}.`);
261
+ return result;
262
+ }
263
+ await appendJsonlRecord(snapshotFiles.sliceHistoryPath, {
264
+ schema_version: 1,
265
+ type,
266
+ recorded_at: recordedAt,
267
+ slice_id: sliceId,
268
+ commit_sha: headSha,
269
+ head_sha: headSha,
270
+ role,
271
+ report_fields: reportFields,
272
+ report_text: output.trim(),
273
+ });
274
+ result.appended.push(`${type}:${sliceId}`);
275
+ return result;
276
+ }
277
+
278
+ if (role === "completion-stop-judge") {
279
+ const canStop = parseYesNo(reportFields["Can the project stop now"]);
280
+ const blockerCount = parseFirstNumber(reportFields["Blocker count"]);
281
+ const highValueGapCount = parseFirstNumber(reportFields["High-value gap count"]);
282
+ if (canStop === undefined || blockerCount === undefined || highValueGapCount === undefined) {
283
+ result.errors.push("Missing required stop-judge fields for canonical judgment transcription.");
284
+ return result;
285
+ }
286
+ const history = await readJsonl(snapshotFiles.stopHistoryPath);
287
+ const duplicate = history.some((entry) => {
288
+ return entry.type === "judgment" && entry.head_sha === headSha && entry.report_text === output.trim();
289
+ });
290
+ if (duplicate) {
291
+ result.skipped.push(`Skipped duplicate judgment record at ${headSha.slice(0, 12)}.`);
292
+ return result;
293
+ }
294
+ await appendJsonlRecord(snapshotFiles.stopHistoryPath, {
295
+ schema_version: 1,
296
+ type: "judgment",
297
+ recorded_at: recordedAt,
298
+ head_sha: headSha,
299
+ can_stop: canStop,
300
+ blocker_count: blockerCount,
301
+ high_value_gap_count: highValueGapCount,
302
+ role,
303
+ report_fields: reportFields,
304
+ report_text: output.trim(),
305
+ });
306
+ result.appended.push(`judgment:${headSha.slice(0, 12)}`);
307
+ return result;
308
+ }
309
+
310
+ if (role === "completion-regrounder") {
311
+ const rawDecision = asString(reportFields["Reconciliation decision"])?.toLowerCase();
312
+ const decision = rawDecision?.match(/\b(accepted|reopened|none)\b/)?.[1];
313
+ if (!decision || decision === "none") {
314
+ result.skipped.push("No reconciliation decision emitted by completion-regrounder.");
315
+ return result;
316
+ }
317
+ const reconciledSliceId = asString(reportFields["Reconciled slice ID"]) ?? asString(reportFields["Current selected slice"]) ?? sliceId;
318
+ if (!reconciledSliceId || reconciledSliceId === "none" || reconciledSliceId === "(none)") {
319
+ result.errors.push("Missing reconciled slice id for completion-regrounder transcription.");
320
+ return result;
321
+ }
322
+ const history = await readJsonl(snapshotFiles.sliceHistoryPath);
323
+ const duplicate = history.some((entry) => {
324
+ return entry.type === decision && entry.slice_id === reconciledSliceId && entry.head_sha === headSha && entry.report_text === output.trim();
325
+ });
326
+ if (duplicate) {
327
+ result.skipped.push(`Skipped duplicate ${decision} record for slice ${reconciledSliceId} at ${headSha.slice(0, 12)}.`);
328
+ return result;
329
+ }
330
+ await appendJsonlRecord(snapshotFiles.sliceHistoryPath, {
331
+ schema_version: 1,
332
+ type: decision,
333
+ recorded_at: recordedAt,
334
+ slice_id: reconciledSliceId,
335
+ commit_sha: headSha,
336
+ head_sha: headSha,
337
+ role,
338
+ report_fields: reportFields,
339
+ report_text: output.trim(),
340
+ });
341
+ result.appended.push(`${decision}:${reconciledSliceId}`);
342
+ return result;
343
+ }
344
+
345
+ result.skipped.push(`No automatic transcription configured for ${role}.`);
346
+ return result;
347
+ }
348
+
349
+ module.exports = {
350
+ RUBRIC_DIMENSIONS,
351
+ parseReportFields,
352
+ parseYesNo,
353
+ parseFirstNumber,
354
+ validateRoleReport,
355
+ transcribeCanonicalRoleReport,
356
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -29,6 +29,7 @@
29
29
  "refocus-test": "bash ./scripts/refocus-test.sh",
30
30
  "context-proposal-test": "bash ./scripts/context-proposal-test.sh",
31
31
  "observability-status-test": "bash ./scripts/observability-status-test.sh",
32
+ "rubric-contract-test": "bash ./scripts/rubric-contract-test.sh",
32
33
  "release-check": "bash ./scripts/release-check.sh"
33
34
  },
34
35
  "engines": {