pi-gauntlet 5.3.1 → 5.3.2

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v5.3.2 - 2026-09-06
4
+
5
+ - Plan coverage: `## Spec coverage` rows accept the exact owner `Verification` for requirements the plan header's `**Verification:**` command proves. `table-closure` closes such rows (mechanical row, unparseable anchor, or literal-free anchor -> finding; never counted as task coverage); `quote-integrity` resolves their literals by verbatim containment in the backtick-stripped header. Orphan-owner reason now names `Verification`; anchor-parseability is checked once per row. `header-entrypoint` unchanged. `writing-plans` documents the owner form with a scoped-tests / final-suite example. Spec: `doc/specs/2026-09-06-gh-24-final-verification-ownership.md` (closes #24).
6
+
3
7
  ## v5.3.1 - 2026-09-06
4
8
 
5
9
  - Task tracking: planning initializes the list once; execution starts and completes existing indices explicitly. Execution retries and fix loops reuse their task; conformance adds only new remediation gaps and reuses their `Gn` indices across rounds.
@@ -16,6 +16,9 @@ const SPEC_TEXT = [
16
16
  "", // 11
17
17
  "## Other", // 12
18
18
  "Stuff.", // 13
19
+ "", // 14
20
+ "## Acceptance", // 15
21
+ "Full suite passes: `npm run fixture-verify`.", // 16
19
22
  ].join("\n");
20
23
 
21
24
  const VALID_PLAN = `# Fixture Plan
@@ -70,6 +73,7 @@ The literal TODO is intentionally documented here per spec quote-integrity requi
70
73
  | § "Testing" L9-L9 | banned token literal handling | Task 3 |
71
74
  | - | mechanical: wire test into CI | Task 2 |
72
75
  | § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |
76
+ | § "Acceptance" L16 | full suite passes | Verification |
73
77
  `;
74
78
 
75
79
  function alwaysTruePort(): FsPort {
@@ -157,7 +161,7 @@ test("check 1 owner-cell grammar: trailing junk after a Task <n> list is malform
157
161
  assert.ok(
158
162
  tc.some(
159
163
  (f) =>
160
- f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
164
+ f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
161
165
  f.text.includes("Task 1 (see note)"),
162
166
  ),
163
167
  `expected an owner-cell malformed finding, got: ${JSON.stringify(tc)}`,
@@ -165,7 +169,7 @@ test("check 1 owner-cell grammar: trailing junk after a Task <n> list is malform
165
169
  assert.ok(
166
170
  tc.some(
167
171
  (f) =>
168
- f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
172
+ f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
169
173
  f.line === lineOf(mutated, '| § "Design" L4-L6 | parser grammar basics | Task 1 (see note) |'),
170
174
  ),
171
175
  `expected the owner-cell malformed finding's line to point at the offending row, got: ${JSON.stringify(tc)}`,
@@ -180,19 +184,173 @@ test("check 1 owner-cell grammar: empty waiver reason is malformed, not accepted
180
184
  const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
181
185
  const tc = findingsFor(findings, "table-closure");
182
186
  assert.ok(
183
- tc.some((f) => f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'")),
187
+ tc.some((f) => f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'")),
184
188
  `expected an owner-cell malformed finding for empty waiver reason, got: ${JSON.stringify(tc)}`,
185
189
  );
186
190
  assert.ok(
187
191
  tc.some(
188
192
  (f) =>
189
- f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
193
+ f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
190
194
  f.line === lineOf(mutated, '| § "Other" L12-L13 | out of scope thing | waived: |'),
191
195
  ),
192
196
  `expected the owner-cell malformed finding's line to point at the offending row, got: ${JSON.stringify(tc)}`,
193
197
  );
194
198
  });
195
199
 
200
+ const VERIFICATION_ROW = '| § "Acceptance" L16 | full suite passes | Verification |';
201
+
202
+ function withRow(plan: string, row: string): string {
203
+ return plan.replace(
204
+ '| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |',
205
+ `| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |\n${row}`,
206
+ );
207
+ }
208
+
209
+ for (const owner of ["verification", "Verify", "VERIFICATION", "Task 1, Verification"]) {
210
+ test(`Verification owner: '${owner}' is a malformed owner -> table-closure`, () => {
211
+ const row = `| § "Acceptance" L16 | full suite passes | ${owner} |`;
212
+ const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
213
+ const tc = findingsFor(findings, "table-closure");
214
+ assert.ok(
215
+ tc.some(
216
+ (f) =>
217
+ f.reason === "owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'" &&
218
+ f.text === row,
219
+ ),
220
+ `expected orphan-owner finding for ${owner}, got: ${JSON.stringify(tc)}`,
221
+ );
222
+ });
223
+ }
224
+
225
+ test("Verification owner: unparseable anchor ('-') -> table-closure parseability reason", () => {
226
+ const row = "| - | full suite passes | Verification |";
227
+ const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
228
+ const tc = findingsFor(findings, "table-closure");
229
+ assert.ok(
230
+ tc.some(
231
+ (f) =>
232
+ f.reason === 'requirement row anchor is not a parseable § "heading" L<n>-L<n> anchor' && f.text === row,
233
+ ),
234
+ `expected parseability finding, got: ${JSON.stringify(tc)}`,
235
+ );
236
+ });
237
+
238
+ test("Verification owner: anchored lines without a backtick literal -> table-closure", () => {
239
+ const row = '| § "Other" L13 | stuff | Verification |';
240
+ const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
241
+ const tc = findingsFor(findings, "table-closure");
242
+ assert.ok(
243
+ tc.some(
244
+ (f) => f.reason === "Verification row has no backtick literal to check against the header" && f.text === row,
245
+ ),
246
+ `expected no-literal finding, got: ${JSON.stringify(tc)}`,
247
+ );
248
+ });
249
+
250
+ test("Verification owner: mechanical row -> table-closure", () => {
251
+ const row = "| - | mechanical: run the suite | Verification |";
252
+ const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
253
+ const tc = findingsFor(findings, "table-closure");
254
+ assert.ok(
255
+ tc.some((f) => f.reason === "mechanical row owner must be a Task <n>" && f.text === row),
256
+ `expected mechanical-owner finding, got: ${JSON.stringify(tc)}`,
257
+ );
258
+ });
259
+
260
+ test("table-closure: multi-owner row with unparseable anchor yields one parseability finding", () => {
261
+ const row = "| L999 | naming details | Task 1, Task 2 |";
262
+ const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
263
+ const parseability = findingsFor(findings, "table-closure").filter(
264
+ (f) => f.text === row && f.reason.includes("not a parseable"),
265
+ );
266
+ assert.equal(parseability.length, 1);
267
+ });
268
+
269
+ test("Verification owner: does not satisfy a task's coverage requirement", () => {
270
+ const mutated = withRow(VALID_PLAN, VERIFICATION_ROW).replace(
271
+ '| § "Testing" L9-L9 | banned token literal handling | Task 3 |\n',
272
+ "",
273
+ );
274
+ const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
275
+ assert.ok(
276
+ findingsFor(findings, "table-closure").some((f) =>
277
+ f.reason.includes("Task 3 does not appear as an owner"),
278
+ ),
279
+ );
280
+ });
281
+
282
+ test("Verification quote-integrity: literal missing from header -> quote-integrity on the row", () => {
283
+ const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify", "**Verification:** npm run other");
284
+ const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
285
+ const qi = findingsFor(findings, "quote-integrity");
286
+ const row = '| § "Acceptance" L16 | full suite passes | Verification |';
287
+ assert.ok(
288
+ qi.some(
289
+ (f) =>
290
+ f.reason === "verification header does not contain the required verbatim literal `npm run fixture-verify`" &&
291
+ f.text === row &&
292
+ f.line === lineOf(mutated, row),
293
+ ),
294
+ `expected header-containment finding, got: ${JSON.stringify(qi)}`,
295
+ );
296
+ });
297
+
298
+ test("Verification quote-integrity: multi-command header contains the literal", () => {
299
+ const mutated = VALID_PLAN.replace(
300
+ "**Verification:** npm run fixture-verify",
301
+ "**Verification:** `npm run fixture-verify && npm run lint`",
302
+ );
303
+ assert.deepEqual(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity"), []);
304
+ });
305
+
306
+ test("Verification quote-integrity: two-span header contains both literals", () => {
307
+ const spec = SPEC_TEXT.replace(
308
+ "Full suite passes: `npm run fixture-verify`.",
309
+ "Full suite passes: `npm run fixture-verify` and `npm run lint`.",
310
+ );
311
+ const mutated = VALID_PLAN.replace(
312
+ "**Verification:** npm run fixture-verify",
313
+ "**Verification:** `npm run fixture-verify`, `npm run lint`",
314
+ );
315
+ assert.deepEqual(findingsFor(checkPlan(mutated, spec, alwaysTruePort()), "quote-integrity"), []);
316
+ });
317
+
318
+ test("Verification quote-integrity: missing header -> quote-integrity per literal plus header-entrypoint", () => {
319
+ const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify\n", "");
320
+ const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
321
+ assert.equal(
322
+ findingsFor(findings, "quote-integrity").filter((f) => f.reason.includes("`npm run fixture-verify`")).length,
323
+ 1,
324
+ );
325
+ assert.ok(findingsFor(findings, "header-entrypoint").some((f) => f.reason.includes("missing header")));
326
+ });
327
+
328
+ test("Verification quote-integrity: task body containing the full header string still fails header-entrypoint", () => {
329
+ const mutated = VALID_PLAN.replace(
330
+ "This task handles naming details.",
331
+ "This task handles naming details. Run npm run fixture-verify here.",
332
+ );
333
+ const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
334
+ assert.ok(findingsFor(findings, "header-entrypoint").some((f) => f.text.includes("Run npm run fixture-verify here")));
335
+ assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
336
+ });
337
+
338
+ test("Verification quote-integrity: task body containing only a sub-command of a multi-command header is not caught by header-entrypoint", () => {
339
+ const mutated = VALID_PLAN.replace(
340
+ "**Verification:** npm run fixture-verify",
341
+ "**Verification:** `npm run fixture-verify && npm run lint`",
342
+ ).replace("This task handles naming details.", "This task handles naming details. Run npm run fixture-verify here.");
343
+ const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
344
+ assert.deepEqual(findingsFor(findings, "header-entrypoint"), []);
345
+ assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
346
+ });
347
+
348
+ test("Verification quote-integrity: task-owned literal check unchanged", () => {
349
+ const mutated = VALID_PLAN.replace("This task implements helperFn() for parsing.", "This task implements the helper.");
350
+ const qi = findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity");
351
+ assert.ok(qi.some((f) => f.reason.includes("Task 1 body does not contain the required verbatim literal `helperFn()`")));
352
+ });
353
+
196
354
  test("check 3 anchor-resolution: ambiguous heading match (duplicate spec heading)", () => {
197
355
  const dupSpec = SPEC_TEXT.replace('## Testing', '## Design\n\nduplicate section body.\n\n## Testing');
198
356
  const findings = checkPlan(VALID_PLAN, dupSpec, alwaysTruePort());
@@ -69,6 +69,7 @@ interface CoverageRow {
69
69
  isWaived: boolean;
70
70
  ownerTasks: number[];
71
71
  ownerMalformed: boolean;
72
+ isVerification: boolean;
72
73
  anchor: Anchor | undefined;
73
74
  }
74
75
 
@@ -270,6 +271,7 @@ function parsePlan(planText: string): ParsedPlan {
270
271
  let isWaived = false;
271
272
  let ownerTasks: number[] = [];
272
273
  let ownerMalformed = false;
274
+ const isVerification = ownerCell === "Verification";
273
275
  if (waivedMatch) {
274
276
  if (waivedMatch[1].trim().length === 0) {
275
277
  ownerMalformed = true;
@@ -278,7 +280,7 @@ function parsePlan(planText: string): ParsedPlan {
278
280
  }
279
281
  } else if (isTaskList) {
280
282
  ownerTasks = [...ownerCell.matchAll(/Task (\d+)/g)].map((mm) => Number(mm[1]));
281
- } else {
283
+ } else if (!isVerification) {
282
284
  ownerMalformed = true;
283
285
  }
284
286
  let anchor: Anchor | undefined;
@@ -296,6 +298,7 @@ function parsePlan(planText: string): ParsedPlan {
296
298
  isWaived,
297
299
  ownerTasks,
298
300
  ownerMalformed,
301
+ isVerification,
299
302
  anchor,
300
303
  });
301
304
  p++;
@@ -380,7 +383,7 @@ function computeRequiredLiteralsPerTask(parsed: ParsedPlan, specLines: string[])
380
383
  return map;
381
384
  }
382
385
 
383
- function checkTableClosure(parsed: ParsedPlan): PlanCheckFinding[] {
386
+ function checkTableClosure(parsed: ParsedPlan, specLines: string[]): PlanCheckFinding[] {
384
387
  const findings: PlanCheckFinding[] = [];
385
388
  if (!parsed.coverageTableFound) {
386
389
  findings.push({ check: "table-closure", line: 0, text: "", reason: "no '## Spec coverage' table found" });
@@ -396,34 +399,54 @@ function checkTableClosure(parsed: ParsedPlan): PlanCheckFinding[] {
396
399
  check: "table-closure",
397
400
  line: row.line,
398
401
  text: row.text,
399
- reason: "owner cell is not a 'Task <n>' list or 'waived: <reason>'",
402
+ reason: "owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'",
403
+ });
404
+ continue;
405
+ }
406
+ if (row.isVerification && row.isMechanical) {
407
+ findings.push({
408
+ check: "table-closure",
409
+ line: row.line,
410
+ text: row.text,
411
+ reason: "mechanical row owner must be a Task <n>",
400
412
  });
401
413
  continue;
402
414
  }
403
415
  if (row.isWaived) continue;
404
- for (const n of row.ownerTasks) {
405
- coveredTasks.add(n);
406
- if (row.isMechanical) mechanicalCoveredTasks.add(n);
407
- const task = taskByNumber.get(n);
408
- if (!task) {
416
+ const anchorUnparseable = !row.isMechanical && !row.anchor;
417
+ if (anchorUnparseable) {
418
+ findings.push({
419
+ check: "table-closure",
420
+ line: row.line,
421
+ text: row.text,
422
+ reason: 'requirement row anchor is not a parseable § "heading" L<n>-L<n> anchor',
423
+ });
424
+ }
425
+ if (row.isVerification) {
426
+ if (!anchorUnparseable && requiredLiteralsForRow(row, specLines).length === 0) {
409
427
  findings.push({
410
428
  check: "table-closure",
411
429
  line: row.line,
412
430
  text: row.text,
413
- reason: `row references Task ${n} but no such task exists`,
431
+ reason: "Verification row has no backtick literal to check against the header",
414
432
  });
415
- continue;
416
433
  }
417
- if (row.isMechanical) continue;
418
- if (!row.anchor) {
434
+ continue;
435
+ }
436
+ for (const n of row.ownerTasks) {
437
+ coveredTasks.add(n);
438
+ if (row.isMechanical) mechanicalCoveredTasks.add(n);
439
+ const task = taskByNumber.get(n);
440
+ if (!task) {
419
441
  findings.push({
420
442
  check: "table-closure",
421
443
  line: row.line,
422
444
  text: row.text,
423
- reason: 'requirement row anchor is not a parseable \u00a7 "heading" L<n>-L<n> anchor',
445
+ reason: `row references Task ${n} but no such task exists`,
424
446
  });
425
447
  continue;
426
448
  }
449
+ if (row.isMechanical || anchorUnparseable) continue;
427
450
  const contained = task.anchors.some(
428
451
  (a) => a.heading === row.anchor!.heading && a.start === row.anchor!.start && a.end === row.anchor!.end,
429
452
  );
@@ -465,10 +488,24 @@ function checkQuoteIntegrity(parsed: ParsedPlan, specLines: string[]): PlanCheck
465
488
  const findings: PlanCheckFinding[] = [];
466
489
  if (!parsed.coverageTableFound) return findings;
467
490
  const taskByNumber = new Map(parsed.tasks.map((t) => [t.number, t]));
491
+ const headerText = (parsed.header.verificationText ?? "").replaceAll("`", "");
468
492
  for (const row of parsed.coverageRows) {
469
493
  if (row.ownerMalformed || row.isWaived || row.isMechanical) continue;
470
494
  const literals = requiredLiteralsForRow(row, specLines);
471
495
  if (literals.length === 0) continue;
496
+ if (row.isVerification) {
497
+ for (const lit of literals) {
498
+ if (!headerText.includes(lit)) {
499
+ findings.push({
500
+ check: "quote-integrity",
501
+ line: row.line,
502
+ text: row.text,
503
+ reason: `verification header does not contain the required verbatim literal \`${lit}\``,
504
+ });
505
+ }
506
+ }
507
+ continue;
508
+ }
472
509
  for (const n of row.ownerTasks) {
473
510
  const task = taskByNumber.get(n);
474
511
  if (!task) continue;
@@ -805,7 +842,7 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): PlanC
805
842
  }
806
843
 
807
844
  const specLines = specText.split("\n");
808
- findings.push(...checkTableClosure(parsed));
845
+ findings.push(...checkTableClosure(parsed, specLines));
809
846
  findings.push(...checkQuoteIntegrity(parsed, specLines));
810
847
  findings.push(...checkAnchorResolution(parsed, specLines));
811
848
  findings.push(...checkPathsExist(parsed, fs));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "5.3.1",
3
+ "version": "5.3.2",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -264,11 +264,14 @@ Every plan ends with a `## Spec coverage` section — authored last, placed afte
264
264
  |---|---|---|
265
265
  | § "Design" L34-L37 | anchor line in task template | Task 2 |
266
266
  | § "Edge cases" L120 | stale anchor = blocking SR finding | Task 4, Task 5 |
267
+ | § "Testing" L84 | checker fixtures: `node --test extensions/lib/plan-check.test.ts` | Task 3 |
268
+ | § "Acceptance" L88 | full suite passes: `npm test` | Verification |
267
269
  | § "Out of scope" L131 | fix-round anchoring | waived: out of scope per spec |
268
270
  | - | mechanical: release commit | Task 7 |
269
271
  ```
270
272
 
271
- - **Requirement rows:** anchor + short requirement + owner = task-ID list, or `waived: <reason>` **only when the spec itself marks the item out of scope**. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
273
+ - **Requirement rows:** anchor + short requirement + owner = task-ID list, or `Verification`, or `waived: <reason>` **only when the spec itself marks the item out of scope**. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
274
+ - **`Verification` owner:** use for a requirement the header `**Verification:**` command proves. Write the exact string `Verification`, alone. Quote only literals contained in that header. Anchor the single requirement line. Keep scoped commands task-owned.
272
275
  - **Mechanical-task rows:** anchor `-`, requirement `mechanical: <short>`, owner = the task ID. One such row per anchor-less task.
273
276
  - The table is plan-authoring-time only — never passed to implementer or reviewer dispatches.
274
277
 
@@ -293,12 +296,13 @@ If a decision is genuinely open, put it in an explicit **Open Questions** sectio
293
296
  After drafting the plan and before announcing it complete, run the deterministic checker, then the judgment checks yourself — not a subagent dispatch.
294
297
 
295
298
  - **Deterministic checker.** Run `plan_check({ planPath })` on the saved plan. Assess and fix every finding yourself (no human involvement), then re-run until it passes — a pass writes the execution stamp that implement-start verifies mechanically. If the same finding survives 3 fix rounds, convert it to an explicit Open Question and stop (the pre-existing Open-Questions halt, resolved by the human in-session — not a new gate). The checker covers table closure, quote integrity, anchor resolution, path existence, placeholder scan, wave file-disjointness, solo-line presence, and header-only entrypoint.
296
- - **Code-vs-anchor sanity.** For each non-waived requirement row, re-read the anchored spec lines and confirm the owner tasks' bodies do what they say - mechanism present, not just the quoted literal. Fix the task, don't annotate.
299
+ - **Code-vs-anchor sanity.** For each task-owned requirement row, re-read the anchored spec lines and confirm the owner tasks' bodies do what they say - mechanism present, not just the quoted literal. For each `Verification` row, confirm the header command exercises the anchored requirement. Fix the task, don't annotate.
297
300
  - **Type / API consistency.** Function signatures and field names that appear in multiple tasks must match exactly. The plan is its own contract — internal contradictions surface as bugs during execution.
298
301
  - **Scoped-test coverage.** Every code-touching wave declares at least one scoped test command; only doc-only waves may have none.
299
302
  - **Runtime-resource disjointness.** For every multi-task wave, confirm no two tasks contend on a shared mutable runtime resource (DB/schema, port, fixture, external service, shared temp path) — `Files:` overlap is checked mechanically, resource contention is not. Contention = mis-grouped wave; split or re-order before handoff.
300
303
  - **Solo-reason validity.** Every single-task wave's `Solo:` line (presence is checked mechanically) must name its specific blocker — the blocking task/wave, the contended resource, or `lone remaining task`. Category-only justifications are under-justified; merge or justify before handoff.
301
304
  - **Waiver authorization.** Every `waived: <reason>` owner in `## Spec coverage` is authorized by the spec itself marking the item out of scope. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
305
+ - **Verification-ownership authorization.** `Verification` on a requirement no header command exercises is a Self-Review failure.
302
306
  - **Documentation-impact mapping.** Each Documentation impact entry maps to a plan task (or explicit "none").
303
307
 
304
308
  Fix what this review finds before handoff.