@linimin/pi-letscook 0.1.75 → 0.1.77

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/.agent/README.md CHANGED
@@ -22,7 +22,7 @@ This repository uses the `completion` workflow for long-running coding tasks.
22
22
  - `.agent/*.log`
23
23
  - `.agent/tmp/`
24
24
 
25
- `.agent/profile.json` carries the stop-wave defaults for this repo, including `required_stop_judges` and `stop_aggregation_policy`. The packaged default is `required_stop_judges: 2` plus `stop_aggregation_policy: "unanimous-current-head-v1"`.
25
+ `.agent/profile.json` carries the stop-wave defaults for this repo, including `required_stop_judges` and `stop_aggregation_policy`. The packaged default is `required_stop_judges: 2` plus `stop_aggregation_policy: "unanimous-current-head-v1"`. Canonical `.agent/state.json current_stop_wave_id` carries the current stop-wave epoch so the same HEAD may restart stop evaluation without requiring a synthetic tracked commit.
26
26
 
27
27
  `.agent/startup-brief.json` preserves the confirmed `/cook` startup intent as canonical intake for re-grounding. It does not replace `.agent/plan.json` or `.agent/active-slice.json`, which remain under regrounder authority.
28
28
 
@@ -118,6 +118,12 @@ if (asString(active.evaluation_profile) !== evaluationProfile) fail('.agent/acti
118
118
  const remainingStopJudges = asNumber(state.remaining_stop_judges);
119
119
  if (remainingStopJudges === undefined) fail('.agent/state.json remaining_stop_judges must be numeric');
120
120
  if (remainingStopJudges < 0) fail('.agent/state.json remaining_stop_judges must not be negative');
121
+ const currentStopWaveId = asNumber(state.current_stop_wave_id);
122
+ if (currentStopWaveId !== undefined) {
123
+ if (!Number.isInteger(currentStopWaveId) || currentStopWaveId < 0) {
124
+ fail('.agent/state.json current_stop_wave_id must be a non-negative integer');
125
+ }
126
+ }
121
127
 
122
128
  if (asString(evidence.artifact_type) !== 'completion-verification-evidence') {
123
129
  fail('.agent/verification-evidence.json artifact_type must be completion-verification-evidence');
@@ -53,6 +53,11 @@ if (stopAggregationPolicy !== 'unanimous-current-head-v1') {
53
53
 
54
54
  const currentPhase = asString(state.current_phase) ?? 'unknown';
55
55
  const stopWaveActive = currentPhase === 'stop_wave' || currentPhase === 'done';
56
+ const currentStopWaveId = asNumber(state.current_stop_wave_id) ?? 0;
57
+ if (!Number.isInteger(currentStopWaveId) || currentStopWaveId < 0) {
58
+ fail('.agent/state.json current_stop_wave_id must be a non-negative integer before stop verification can run.');
59
+ }
60
+ const activeStopWaveId = stopWaveActive ? currentStopWaveId || 1 : currentStopWaveId;
56
61
  const rawHistory = fs.existsSync('.agent/stop-check-history.jsonl') ? fs.readFileSync('.agent/stop-check-history.jsonl', 'utf8') : '';
57
62
  const seededHeadSha = asString(process.env.COMPLETION_STOP_HEAD);
58
63
  if (!seededHeadSha && !stopWaveActive && rawHistory.trim().length === 0) {
@@ -75,6 +80,11 @@ for (const [index, rawLine] of rawHistory.split(/\r?\n/).entries()) {
75
80
  }
76
81
  if (parsed.type !== 'judgment') continue;
77
82
  if (asString(parsed.head_sha) !== headSha) continue;
83
+ const recordStopWaveId = asNumber(parsed.stop_wave_id) ?? 0;
84
+ if (!Number.isInteger(recordStopWaveId) || recordStopWaveId < 0) {
85
+ fail('Current-HEAD judgment at line ' + (index + 1) + ' must carry a non-negative integer stop_wave_id.');
86
+ }
87
+ if (recordStopWaveId !== activeStopWaveId) continue;
78
88
  if (typeof parsed.can_stop !== 'boolean') {
79
89
  fail('Current-HEAD judgment at line ' + (index + 1) + ' must carry boolean can_stop.');
80
90
  }
@@ -98,10 +108,10 @@ if (!stopWaveActive && currentHeadJudgments.length === 0) {
98
108
  }
99
109
 
100
110
  if (currentHeadJudgments.length < requiredStopJudges) {
101
- fail('Need ' + requiredStopJudges + ' valid current-HEAD judgments for HEAD ' + headSha + '; found ' + currentHeadJudgments.length + '.');
111
+ fail('Need ' + requiredStopJudges + ' valid current-HEAD judgments for HEAD ' + headSha + ' in stop_wave_id ' + activeStopWaveId + '; found ' + currentHeadJudgments.length + '.');
102
112
  }
103
113
 
104
- console.log('[completion] stop-wave policy unanimous-current-head-v1 satisfied for HEAD ' + headSha + ' with ' + currentHeadJudgments.length + ' valid current-HEAD judgments');
114
+ console.log('[completion] stop-wave policy unanimous-current-head-v1 satisfied for HEAD ' + headSha + ' in stop_wave_id ' + activeStopWaveId + ' with ' + currentHeadJudgments.length + ' valid current-HEAD judgments');
105
115
  NODE
106
116
 
107
117
  echo "[completion] running repo-level verification: npm run release-check >/dev/null"
package/CHANGELOG.md CHANGED
@@ -1,17 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.77
4
+
5
+ ### Fixed
6
+
7
+ - introduced `current_stop_wave_id` / `stop_wave_id` stop-wave epochs so the same HEAD can restart stop evaluation after stale no-stop history without requiring a synthetic tracked commit
8
+ - taught stop-judge transcription, verifier policy, protocol docs, and release regression coverage to scope stop-wave aggregation to the active epoch instead of permanently poisoning a HEAD on the first `can_stop=no`
9
+
10
+ ## 0.1.76
11
+
12
+ ### Fixed
13
+
14
+ - taught the completion protocol and core role prompts to auto-preserve routine unrelated tracked worktree dirt with a reversible stash-plus-note flow instead of asking the user to choose between stash/cleanup/background continuation for every dirty-worktree checkpoint
15
+ - refined sticky `/cook` continuation detection so clear workflow-follow-up turns stay inside the active workflow while unrelated ordinary chat stays outside it, and aligned smoke/release regressions with that split
16
+
3
17
  ## 0.1.75
4
18
 
5
19
  ### Fixed
6
20
 
7
21
  - stopped treating a fresh but under-specified explicit `cook_handoff` as an automatic startup blocker; `/cook` now uses the user's explicit entry as implementation intent and lets same-entry primary-agent startup synthesis tighten the first slice before it gives up
8
22
  - aligned startup, sticky-workflow, and canonical-evidence regressions with the new implementation-first `/cook` behavior so long-running workflows no longer bounce users back into handoff-authoring loops
23
+ - taught the completion protocol and core role prompts to auto-preserve routine unrelated tracked worktree dirt with a reversible stash-plus-note flow instead of asking the user to choose between stash/cleanup/background continuation for every dirty-worktree checkpoint
9
24
 
10
25
  ## 0.1.74
11
26
 
12
27
  ### Fixed
13
28
 
14
- - made active `/cook` workflows sticky across subsequent turns so routine continuation, exact await-user-input replies, and mandatory completion-role dispatch no longer depend on prompt-shaped driver turns or repeated manual `/cook` re-entry
29
+ - made active `/cook` workflows sticky across routine continuation turns, exact await-user-input replies, and mandatory completion-role dispatch so long-running workflows keep moving without repeated manual `/cook` re-entry while unrelated ordinary chat stays outside workflow mode
15
30
  - updated smoke, canonical-evidence, release-check, and completion-role gating regressions to enforce the new sticky active-workflow self-healing behavior
16
31
  - stopped letting fresh but under-specified explicit `cook_handoff` capsules block `/cook` startup by default; `/cook` now treats the user's entry as implementation intent and tries same-entry primary-agent startup synthesis to tighten the first slice before failing closed
17
32
 
package/README.md CHANGED
@@ -190,6 +190,8 @@ Deterministic active-slice contract regression now lives in `bash scripts/active
190
190
 
191
191
  Deterministic verification for this packaged contract also lives in `npm run rubric-contract-test`, which now exercises reviewer, auditor, and stop-judge transcription paths while the bootstrap/refocus/context regressions plus control-plane verifier fail closed when required canonical signaling is missing.
192
192
 
193
+ Active `/cook` workflows now also auto-reconcile routine unrelated tracked worktree dirt instead of bouncing that decision back to the user. When the dirty tracked files are outside the latest slice or current reconciliation surfaces and can be isolated safely, the workflow should preserve them with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note, continue the mandatory step on a clean worktree, and restore them before handing control back. Only overlapping changes, ownership ambiguity, or stash/restore conflicts should force a user-facing decision.
194
+
193
195
  ## Canonical files
194
196
 
195
197
  This package stores canonical workflow state under:
@@ -234,6 +236,7 @@ Ignored execution-state files:
234
236
  - `.agent/active-slice.json`
235
237
  - `.agent/slice-history.jsonl`
236
238
  - `.agent/stop-check-history.jsonl`
239
+ - `state.json current_stop_wave_id` defines the current stop-wave epoch so the same HEAD can restart stop evaluation without requiring a synthetic tracked commit.
237
240
  - `.agent/verification-evidence.json`
238
241
  - `.agent/*.log`
239
242
  - `.agent/tmp/`
@@ -64,13 +64,14 @@ These lines are for workflow observability, not hidden reasoning. Keep them brie
64
64
  3. Confirm the canonical slice ID, goal, acceptance criteria, contract IDs, priority, why_now, implementation_surfaces, verification_commands, locked notes, must-fix findings, basis_commit, and before-slice counters in `.agent/active-slice.json` match canonical `.agent/plan.json`. If they do not match, stop and report the mismatch instead of guessing.
65
65
  4. Make truthful `.agent/state.json` and `.agent/active-slice.json` updates before implementation if needed.
66
66
  5. If implementation reveals roadmap-level drift — for example a missing prerequisite slice, invalid slice boundary, dependency reorder, or blocker that changes the current slice contract — do not silently redesign the plan. Report the discrepancy explicitly, make only the minimal truthful local state updates needed for the current slice, and hand control back for canonical re-grounding by `completion-regrounder`.
67
- 6. Make the smallest correct tracked-file change.
68
- 7. Add or strengthen tests or deterministic proof.
69
- 8. Run focused verification first, then broader verification if shared surfaces changed.
70
- 9. If the chosen slice changes top-level validation entrypoints or is explicitly about verifier freshness, refresh `.agent/verify_completion_stop.sh` so it remains a truthful repo-level baseline verifier.
71
- 10. Create a new commit.
72
- 11. Make truthful `.agent/state.json`, `.agent/active-slice.json`, and `.agent/plan.json` updates after the commit, including `current_phase = post_commit_review`, `continuation_policy = continue`, `continuation_reason`, and `next_mandatory_role = completion-reviewer`.
73
- 12. Append exactly one `implemented` record to `.agent/slice-history.jsonl`.
67
+ 6. If unrelated tracked worktree changes are present and would otherwise block the mandatory dirty-worktree reconciliation or the current slice commit, auto-preserve them yourself with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note, continue the current slice on a clean worktree, and restore them before handing control back. Ask the user only when overlap, ownership ambiguity, or stash/restore conflicts make automatic isolation unsafe.
68
+ 7. Make the smallest correct tracked-file change.
69
+ 8. Add or strengthen tests or deterministic proof.
70
+ 9. Run focused verification first, then broader verification if shared surfaces changed.
71
+ 10. If the chosen slice changes top-level validation entrypoints or is explicitly about verifier freshness, refresh `.agent/verify_completion_stop.sh` so it remains a truthful repo-level baseline verifier.
72
+ 11. Create a new commit.
73
+ 12. Make truthful `.agent/state.json`, `.agent/active-slice.json`, and `.agent/plan.json` updates after the commit, including `current_phase = post_commit_review`, `continuation_policy = continue`, `continuation_reason`, and `next_mandatory_role = completion-reviewer`.
74
+ 13. Append exactly one `implemented` record to `.agent/slice-history.jsonl`.
74
75
 
75
76
  Do not stop after editing or verification if the slice changes remain uncommitted.
76
77
 
@@ -41,14 +41,18 @@ These lines are for workflow observability, not hidden reasoning. Keep them brie
41
41
  5. Reopen any previously `done` slice whose acceptance criteria no longer hold.
42
42
  6. Keep `.agent/state.json` and `.agent/active-slice.json` truthful, including `current_phase`, `continuation_policy`, `continuation_reason`, `next_mandatory_role`, and any exact implementer handoff snapshot fields.
43
43
  7. Reconcile canonical state after review, audit, and final stop verification waves when required.
44
- 8. If the latest committed slice leaves the tracked and unignored worktree dirty, treat that dirty state as a blocker, reopen or continue that latest slice for reconciliation, set `Next role to invoke` to `completion-implementer`, and do not select or hand off any different next slice until it is reconciled.
45
- 9. When reconciling after review, audit, or dirty-worktree follow-up for the latest committed slice, emit an explicit reconciliation record decision:
44
+ 8. When entering a fresh stop wave after all implementation slices are done, set or increment `.agent/state.json current_stop_wave_id` for the new stop-wave epoch and reset `remaining_stop_judges` from `.agent/profile.json required_stop_judges`.
45
+ 9. If a prior stop-wave epoch on the same HEAD recorded `can_stop = no`, do not permanently poison that HEAD by itself. If canonical state, docs/state parity, or verification truth have changed enough to justify a fresh stop evaluation on the same HEAD, increment `current_stop_wave_id`, preserve the old judgments as history, and restart stop-wave collection for the new epoch.
46
+ 10. If the latest committed slice leaves the tracked and unignored worktree dirty, first classify the dirty tracked files against the latest slice's `implementation_surfaces` and the tracked reconciliation surfaces you need to touch now.
47
+ 11. If the dirty tracked files are unrelated and can be isolated safely, auto-preserve them yourself with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note, continue the mandatory reconciliation on a clean worktree, and restore them before handing control back. Do not ask the user for this routine unrelated-dirty-worktree case.
48
+ 12. If overlap, ownership ambiguity, or stash/restore conflicts make automatic isolation unsafe, treat that dirty state as a blocker, reopen or continue the latest slice for reconciliation, set `Next role to invoke` to `completion-implementer`, and do not select or hand off any different next slice until it is reconciled.
49
+ 13. When reconciling after review, audit, dirty-worktree follow-up, or stop-wave epoch restart for the latest committed slice, emit an explicit reconciliation record decision:
46
50
  - `accepted` only when the latest committed slice is truthfully accepted as-is
47
51
  - `reopened` only when the latest committed slice must be reopened for follow-up work
48
52
  - `none` when this re-ground was not a post-commit reconciliation decision
49
- 10. If you emit `accepted` or `reopened`, also emit the exact reconciled slice id in the report.
50
- 11. If a slice is already selected, ensure `.agent/active-slice.json` contains the exact implementer handoff snapshot and return that exact handoff payload for `completion-implementer` instead of implementing it yourself.
51
- 12. If no slice is selected, return the exact next recommended slice and why.
53
+ 14. If you emit `accepted` or `reopened`, also emit the exact reconciled slice id in the report.
54
+ 15. If a slice is already selected, ensure `.agent/active-slice.json` contains the exact implementer handoff snapshot and return that exact handoff payload for `completion-implementer` instead of implementing it yourself.
55
+ 16. If no slice is selected, return the exact next recommended slice and why.
52
56
 
53
57
  Output format:
54
58
 
@@ -19,7 +19,7 @@ You must not:
19
19
  - append stop-check history yourself
20
20
  - create commits
21
21
 
22
- The workflow driver records your returned verdict into `.agent/stop-check-history.jsonl` during the final stop wave. Your output must therefore be explicit enough to transcribe faithfully as one canonical `judgment` record for the current HEAD.
22
+ The workflow driver records your returned verdict into `.agent/stop-check-history.jsonl` during the final stop wave. Your output must therefore be explicit enough to transcribe faithfully as one canonical `judgment` record for the current HEAD and current `state.json current_stop_wave_id` epoch.
23
23
 
24
24
  During long work, emit short operator-facing progress lines when useful using these exact prefixes:
25
25
  - `PROGRESS: ...`
@@ -257,8 +257,26 @@ function isCompletionDriverPromptTurn(snapshot: CompletionStateSnapshot | undefi
257
257
  return true;
258
258
  }
259
259
 
260
- function isCompletionWorkflowSessionTurn(snapshot: CompletionStateSnapshot | undefined, _ctx: { sessionManager?: any }): boolean {
261
- return hasCompletionRoutingActivation(snapshot) || hasActiveWorkflowEntry(snapshot);
260
+ function workflowContinuationIntentText(text: string | undefined): string {
261
+ return (text ?? "").trim().toLowerCase();
262
+ }
263
+
264
+ function isLikelyWorkflowContinuationTurn(
265
+ snapshot: CompletionStateSnapshot | undefined,
266
+ ctx: { sessionManager?: any },
267
+ ): boolean {
268
+ if (!hasActiveWorkflowEntry(snapshot)) return false;
269
+ const latest = workflowContinuationIntentText(latestUserOrCustomTurnText(ctx));
270
+ if (!latest) return false;
271
+ if (isCookCommandTurn(ctx) || isCompletionDriverPromptTurn(snapshot, ctx)) return true;
272
+ if (asString(snapshot?.state?.continuation_policy) === "await_user_input") return true;
273
+ return /(\b(continue|resume|proceed|go ahead|keep going|next|finish|fix|repair|reconcile|commit|stash|audit|review|reground|implement|phase|slice|batch)\b|\.agent\b|\bworktree\b|\bworkflow\b|\bdirty\b|繼續|继续|開始|开始|先做|先把|修好|修復|修复|清理|處理|处理|提交|下一步|接著|继续做|做完|完成)/iu.test(latest);
274
+ }
275
+
276
+ function isCompletionWorkflowSessionTurn(snapshot: CompletionStateSnapshot | undefined, ctx: { sessionManager?: any }): boolean {
277
+ if (hasCompletionRoutingActivation(snapshot)) return true;
278
+ if (!hasActiveWorkflowEntry(snapshot)) return false;
279
+ return isCookCommandTurn(ctx) || isCompletionDriverPromptTurn(snapshot, ctx) || isLikelyWorkflowContinuationTurn(snapshot, ctx);
262
280
  }
263
281
 
264
282
  function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot | undefined, ctx: { sessionManager?: any }): boolean {
@@ -46,6 +46,10 @@ function asString(value) {
46
46
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
47
47
  }
48
48
 
49
+ function asNumber(value) {
50
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
51
+ }
52
+
49
53
  function parseReportFields(text) {
50
54
  const fields = {};
51
55
  for (const rawLine of text.split("\n")) {
@@ -317,6 +321,16 @@ async function appendJsonlRecord(filePath, record) {
317
321
  await fs.appendFile(filePath, `${JSON.stringify(record)}\n`, "utf8");
318
322
  }
319
323
 
324
+ async function readJsonFile(filePath) {
325
+ try {
326
+ const raw = await fs.readFile(filePath, "utf8");
327
+ const parsed = JSON.parse(raw);
328
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
329
+ } catch {
330
+ return undefined;
331
+ }
332
+ }
333
+
320
334
  async function transcribeCanonicalRoleReport({ role, output, reportFields = parseReportFields(output), snapshotFiles, headSha, sliceId, recordedAt = Date.now() }) {
321
335
  const result = { appended: [], skipped: [], errors: [] };
322
336
 
@@ -370,12 +384,18 @@ async function transcribeCanonicalRoleReport({ role, output, reportFields = pars
370
384
  result.errors.push("Missing required stop-judge fields for canonical judgment transcription.");
371
385
  return result;
372
386
  }
387
+ const state = snapshotFiles.statePath ? await readJsonFile(snapshotFiles.statePath) : undefined;
388
+ const currentStopWaveId = asNumber(state?.current_stop_wave_id) ?? 1;
389
+ if (!Number.isInteger(currentStopWaveId) || currentStopWaveId < 0) {
390
+ result.errors.push("Canonical state must carry a non-negative integer current_stop_wave_id before stop-judge transcription.");
391
+ return result;
392
+ }
373
393
  const history = await readJsonl(snapshotFiles.stopHistoryPath);
374
394
  const duplicate = history.some((entry) => {
375
- return entry.type === "judgment" && entry.head_sha === headSha && entry.report_text === output.trim();
395
+ return entry.type === "judgment" && entry.head_sha === headSha && asNumber(entry.stop_wave_id) === currentStopWaveId && entry.report_text === output.trim();
376
396
  });
377
397
  if (duplicate) {
378
- result.skipped.push(`Skipped duplicate judgment record at ${headSha.slice(0, 12)}.`);
398
+ result.skipped.push(`Skipped duplicate judgment record for stop_wave_id ${currentStopWaveId} at ${headSha.slice(0, 12)}.`);
379
399
  return result;
380
400
  }
381
401
  await appendJsonlRecord(snapshotFiles.stopHistoryPath, {
@@ -383,6 +403,7 @@ async function transcribeCanonicalRoleReport({ role, output, reportFields = pars
383
403
  type: "judgment",
384
404
  recorded_at: recordedAt,
385
405
  head_sha: headSha,
406
+ stop_wave_id: currentStopWaveId,
386
407
  can_stop: canStop,
387
408
  blocker_count: blockerCount,
388
409
  high_value_gap_count: highValueGapCount,
@@ -390,7 +411,7 @@ async function transcribeCanonicalRoleReport({ role, output, reportFields = pars
390
411
  report_fields: reportFields,
391
412
  report_text: output.trim(),
392
413
  });
393
- result.appended.push(`judgment:${headSha.slice(0, 12)}`);
414
+ result.appended.push(`judgment:${headSha.slice(0, 12)}:wave:${currentStopWaveId}`);
394
415
  return result;
395
416
  }
396
417
 
@@ -272,6 +272,7 @@ export function defaultState(
272
272
  next_mandatory_action: "Reconcile canonical state from current repo truth",
273
273
  next_mandatory_role: "completion-regrounder",
274
274
  remaining_stop_judges: requiredStopJudges,
275
+ current_stop_wave_id: 0,
275
276
  last_reground_at: null,
276
277
  last_auditor_verdict: null,
277
278
  contract_status: "unknown",
@@ -367,7 +368,7 @@ export function defaultVerificationEvidence(): JsonRecord {
367
368
  }
368
369
 
369
370
  export function buildAgentReadme(projectName: string): string {
370
- 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/startup-brief.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/profile.json\` carries the stop-wave defaults for this repo, including \`required_stop_judges\` and \`stop_aggregation_policy\`. The packaged default is \`required_stop_judges: 2\` plus \`stop_aggregation_policy: "${DEFAULT_STOP_AGGREGATION_POLICY}"\`.\n\n\`.agent/startup-brief.json\` preserves the confirmed \`/cook\` startup intent as canonical intake for re-grounding. It does not replace \`.agent/plan.json\` or \`.agent/active-slice.json\`, which remain under regrounder authority.\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`;
371
+ 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/startup-brief.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/profile.json\` carries the stop-wave defaults for this repo, including \`required_stop_judges\` and \`stop_aggregation_policy\`. The packaged default is \`required_stop_judges: 2\` plus \`stop_aggregation_policy: "${DEFAULT_STOP_AGGREGATION_POLICY}"\`. Canonical \`.agent/state.json current_stop_wave_id\` carries the current stop-wave epoch so the same HEAD may restart stop evaluation without requiring a synthetic tracked commit.\n\n\`.agent/startup-brief.json\` preserves the confirmed \`/cook\` startup intent as canonical intake for re-grounding. It does not replace \`.agent/plan.json\` or \`.agent/active-slice.json\`, which remain under regrounder authority.\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`;
371
372
  }
372
373
 
373
374
  export function buildMission(projectName: string, missionAnchor: string): string {
@@ -433,6 +434,11 @@ if (stopAggregationPolicy !== '${DEFAULT_STOP_AGGREGATION_POLICY}') {
433
434
 
434
435
  const currentPhase = asString(state.current_phase) ?? 'unknown';
435
436
  const stopWaveActive = currentPhase === 'stop_wave' || currentPhase === 'done';
437
+ const currentStopWaveId = asNumber(state.current_stop_wave_id) ?? 0;
438
+ if (!Number.isInteger(currentStopWaveId) || currentStopWaveId < 0) {
439
+ fail('.agent/state.json current_stop_wave_id must be a non-negative integer before stop verification can run.');
440
+ }
441
+ const activeStopWaveId = stopWaveActive ? currentStopWaveId || 1 : currentStopWaveId;
436
442
  const rawHistory = fs.existsSync('.agent/stop-check-history.jsonl') ? fs.readFileSync('.agent/stop-check-history.jsonl', 'utf8') : '';
437
443
  const seededHeadSha = asString(process.env.COMPLETION_STOP_HEAD);
438
444
  if (!seededHeadSha && !stopWaveActive && rawHistory.trim().length === 0) {
@@ -455,6 +461,11 @@ for (const [index, rawLine] of rawHistory.split(/\\r?\\n/).entries()) {
455
461
  }
456
462
  if (parsed.type !== 'judgment') continue;
457
463
  if (asString(parsed.head_sha) !== headSha) continue;
464
+ const recordStopWaveId = asNumber(parsed.stop_wave_id) ?? 0;
465
+ if (!Number.isInteger(recordStopWaveId) || recordStopWaveId < 0) {
466
+ fail('Current-HEAD judgment at line ' + (index + 1) + ' must carry a non-negative integer stop_wave_id.');
467
+ }
468
+ if (recordStopWaveId !== activeStopWaveId) continue;
458
469
  if (typeof parsed.can_stop !== 'boolean') {
459
470
  fail('Current-HEAD judgment at line ' + (index + 1) + ' must carry boolean can_stop.');
460
471
  }
@@ -478,10 +489,10 @@ if (!stopWaveActive && currentHeadJudgments.length === 0) {
478
489
  }
479
490
 
480
491
  if (currentHeadJudgments.length < requiredStopJudges) {
481
- fail('Need ' + requiredStopJudges + ' valid current-HEAD judgments for HEAD ' + headSha + '; found ' + currentHeadJudgments.length + '.');
492
+ fail('Need ' + requiredStopJudges + ' valid current-HEAD judgments for HEAD ' + headSha + ' in stop_wave_id ' + activeStopWaveId + '; found ' + currentHeadJudgments.length + '.');
482
493
  }
483
494
 
484
- console.log('[completion] stop-wave policy ${DEFAULT_STOP_AGGREGATION_POLICY} satisfied for HEAD ' + headSha + ' with ' + currentHeadJudgments.length + ' valid current-HEAD judgments');
495
+ console.log('[completion] stop-wave policy ${DEFAULT_STOP_AGGREGATION_POLICY} satisfied for HEAD ' + headSha + ' in stop_wave_id ' + activeStopWaveId + ' with ' + currentHeadJudgments.length + ' valid current-HEAD judgments');
485
496
  NODE
486
497
 
487
498
  ${repoCheck}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
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,
@@ -42,6 +42,8 @@
42
42
  "context-proposal-test": "bash ./scripts/context-proposal-test.sh",
43
43
  "observability-status-test": "bash ./scripts/observability-status-test.sh",
44
44
  "completion-role-gating-test": "bash ./scripts/completion-role-gating-test.sh",
45
+ "dirty-worktree-policy-test": "bash ./scripts/dirty-worktree-policy-test.sh",
46
+ "stop-wave-epoch-test": "bash ./scripts/stop-wave-epoch-test.sh",
45
47
  "evaluator-calibration-test": "bash ./scripts/evaluator-calibration-test.sh",
46
48
  "rubric-contract-test": "bash ./scripts/rubric-contract-test.sh",
47
49
  "release-check": "bash ./scripts/release-check.sh"
@@ -510,10 +510,7 @@ import sys
510
510
  from pathlib import Path
511
511
 
512
512
  reminder = Path(sys.argv[1])
513
- assert reminder.exists(), 'active selected-slice canonical state should inject the completion reminder on subsequent non-/cook turns'
514
- text = reminder.read_text()
515
- assert 'Completion workflow detected.' in text, 'selected-slice reminder should expose canonical workflow context'
516
- assert 'Verification evidence subject: selected_slice' in text, 'selected-slice reminder should expose the canonical evidence subject'
513
+ assert not reminder.exists(), 'ordinary non-/cook turn should not inject completion reminder solely from selected-slice canonical state'
517
514
  PY
518
515
 
519
516
  python3 - <<'PY'
@@ -21,26 +21,24 @@ const assertNotIncludes = (file, snippet) => {
21
21
  }
22
22
  };
23
23
 
24
+ assertIncludes('extensions/completion/index.ts', 'function isLikelyWorkflowContinuationTurn(');
24
25
  assertIncludes('extensions/completion/index.ts', 'function isCompletionWorkflowSessionTurn(');
25
- assertIncludes('extensions/completion/index.ts', 'return hasCompletionRoutingActivation(snapshot) || hasActiveWorkflowEntry(snapshot);');
26
+ assertIncludes('extensions/completion/index.ts', 'return isCookCommandTurn(ctx) || isCompletionDriverPromptTurn(snapshot, ctx) || isLikelyWorkflowContinuationTurn(snapshot, ctx);');
26
27
  assertIncludes('extensions/completion/index.ts', 'const completionRoleDispatchAllowed = Boolean(role) || isCompletionWorkflowSessionTurn(snapshot, ctx);');
27
28
  assertIncludes('extensions/completion/policy-guards.ts', 'return "completion_role may only be used from an active /cook workflow session.";');
28
- assertIncludes('CHANGELOG.md', 'made active `/cook` workflows sticky across subsequent turns so completion-role dispatch and workflow context continue to self-heal from canonical active state instead of depending on prompt-shaped driver turns');
29
29
  assertIncludes('CHANGELOG.md', 'stopped pushing users to rerun `/cook` for routine active-workflow continuation or exact await-user-input replies when canonical workflow state is already active');
30
30
 
31
- assertNotIncludes('extensions/completion/index.ts', 'function isOrdinaryMainChatTurnDuringActiveWorkflow(');
32
- assertNotIncludes('extensions/completion/index.ts', 'function isCompletionRoleDispatchAllowedTurn(');
33
- assertNotIncludes('extensions/completion/index.ts', 'function isAwaitingUserInputWorkflowReplyTurn(');
31
+ assertNotIncludes('extensions/completion/index.ts', 'return hasCompletionRoutingActivation(snapshot) || hasActiveWorkflowEntry(snapshot);');
34
32
 
35
33
  const indexText = read('extensions/completion/index.ts');
36
- const sessionTurnIndex = indexText.indexOf('function isCompletionWorkflowSessionTurn(');
37
- const stickyReturnIndex = indexText.indexOf('return hasCompletionRoutingActivation(snapshot) || hasActiveWorkflowEntry(snapshot);');
34
+ const continuationIntentIndex = indexText.indexOf('function isLikelyWorkflowContinuationTurn(');
35
+ const stickyReturnIndex = indexText.indexOf('return isCookCommandTurn(ctx) || isCompletionDriverPromptTurn(snapshot, ctx) || isLikelyWorkflowContinuationTurn(snapshot, ctx);');
38
36
  const toolGateIndex = indexText.indexOf('const completionRoleDispatchAllowed = Boolean(role) || isCompletionWorkflowSessionTurn(snapshot, ctx);');
39
- if (sessionTurnIndex === -1 || stickyReturnIndex === -1 || toolGateIndex === -1) {
40
- throw new Error('extensions/completion/index.ts must derive workflow legitimacy from canonical active state and reuse that gate for completion_role dispatch.');
37
+ if (continuationIntentIndex === -1 || stickyReturnIndex === -1 || toolGateIndex === -1) {
38
+ throw new Error('extensions/completion/index.ts must gate workflow continuation through explicit workflow turns or likely continuation turns before dispatching completion_role.');
41
39
  }
42
- if (!(sessionTurnIndex < stickyReturnIndex && stickyReturnIndex < toolGateIndex)) {
43
- throw new Error('extensions/completion/index.ts should define sticky workflow-session detection before reusing it for completion_role dispatch.');
40
+ if (!(continuationIntentIndex < stickyReturnIndex && stickyReturnIndex < toolGateIndex)) {
41
+ throw new Error('extensions/completion/index.ts should define continuation-turn detection before reusing it for completion_role dispatch.');
44
42
  }
45
43
  NODE
46
44
 
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ node <<'NODE'
8
+ const fs = require('node:fs');
9
+
10
+ const read = (file) => fs.readFileSync(file, 'utf8');
11
+ const assertIncludes = (file, snippet) => {
12
+ const text = read(file);
13
+ if (!text.includes(snippet)) {
14
+ throw new Error(`${file} is missing required dirty-worktree policy text: ${snippet}`);
15
+ }
16
+ };
17
+
18
+ assertIncludes('skills/completion-protocol/SKILL.md', 'auto-preserve them with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note');
19
+ assertIncludes('skills/completion-protocol/SKILL.md', 'Ask the user only when overlap, ownership ambiguity, or stash/restore conflicts make automatic isolation unsafe.');
20
+ assertIncludes('skills/completion-protocol/references/completion.md', 'Dirty-worktree auto-reconcile. If tracked worktree dirt is unrelated to the latest slice or current reconciliation surfaces and can be isolated safely');
21
+ assertIncludes('agents/completion-regrounder.md', 'Do not ask the user for this routine unrelated-dirty-worktree case.');
22
+ assertIncludes('agents/completion-implementer.md', 'auto-preserve them yourself with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note');
23
+ assertIncludes('README.md', 'Active `/cook` workflows now also auto-reconcile routine unrelated tracked worktree dirt instead of bouncing that decision back to the user.');
24
+ assertIncludes('CHANGELOG.md', 'auto-preserve routine unrelated tracked worktree dirt with a reversible stash-plus-note flow');
25
+ NODE
26
+
27
+ echo "dirty-worktree policy test passed"
@@ -44,9 +44,11 @@ const tempRootBase = path.join(process.cwd(), '.agent', 'tmp');
44
44
  fs.mkdirSync(tempRootBase, { recursive: true });
45
45
  const tempRoot = fs.mkdtempSync(path.join(tempRootBase, 'evaluator-calibration-'));
46
46
  const snapshotFiles = {
47
+ statePath: path.join(tempRoot, 'state.json'),
47
48
  sliceHistoryPath: path.join(tempRoot, 'slice-history.jsonl'),
48
49
  stopHistoryPath: path.join(tempRoot, 'stop-check-history.jsonl'),
49
50
  };
51
+ fs.writeFileSync(snapshotFiles.statePath, JSON.stringify({ current_stop_wave_id: 1 }, null, 2));
50
52
  fs.writeFileSync(snapshotFiles.sliceHistoryPath, '');
51
53
  fs.writeFileSync(snapshotFiles.stopHistoryPath, '');
52
54
 
@@ -378,7 +380,7 @@ Brief justification: This should be rejected because remaining contracts still e
378
380
  recordedAt: 5,
379
381
  });
380
382
  assert(judged.errors.length === 0, `stop-judge passing fixture should transcribe cleanly: ${judged.errors.join(' | ')}`);
381
- assert(judged.appended.includes('judgment:555555555555'), 'stop-judge passing fixture should append a judgment record');
383
+ assert(judged.appended.includes('judgment:555555555555:wave:1'), 'stop-judge passing fixture should append a judgment record for the active stop-wave epoch');
382
384
  assert(readJsonl(snapshotFiles.stopHistoryPath).length === 1, 'stop-judge passing fixture should create one judgment record');
383
385
 
384
386
  const judgeRejected = await transcribeCanonicalRoleReport({
@@ -5,7 +5,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
5
  cd "$ROOT"
6
6
  export PI_COMPLETION_RUNNING_RELEASE_CHECK=1
7
7
 
8
- echo "[release-check] running control-plane validation, tracked .agent contract coverage, slice-surface parity, explicit-/cook parity, startup/refocus/context regressions, canonical evidence artifact, active-slice contract, observability, completion-role gating, legacy cleanup, evaluator calibration, and rubric contract coverage"
8
+ echo "[release-check] running control-plane validation, tracked .agent contract coverage, slice-surface parity, explicit-/cook parity, startup/refocus/context regressions, canonical evidence artifact, active-slice contract, observability, completion-role gating, dirty-worktree policy, stop-wave epoch, legacy cleanup, evaluator calibration, and rubric contract coverage"
9
9
  bash .agent/verify_completion_control_plane.sh
10
10
  git ls-files --error-unmatch .agent/README.md .agent/mission.md .agent/profile.json .agent/verify_completion_stop.sh .agent/verify_completion_control_plane.sh >/dev/null
11
11
 
@@ -34,8 +34,8 @@ checks = {
34
34
  'description: "/cook workflow: start or replace workflow only from an explicit primary-agent handoff, or resume the current workflow from canonical state"',
35
35
  '"Do not call completion_role from ordinary chat; it is reserved for active /cook workflow sessions."',
36
36
  '`COMPLETION WORKFLOW DRIVER\\nStart or continue the completion workflow for this repo.',
37
- 'function isCompletionWorkflowSessionTurn(',
38
- 'return hasCompletionRoutingActivation(snapshot) || hasActiveWorkflowEntry(snapshot);',
37
+ 'function isLikelyWorkflowContinuationTurn(',
38
+ 'return isCookCommandTurn(ctx) || isCompletionDriverPromptTurn(snapshot, ctx) || isLikelyWorkflowContinuationTurn(snapshot, ctx);',
39
39
  ],
40
40
  "extensions/completion/policy-guards.ts": [
41
41
  'return "completion_role may only be used from an active /cook workflow session.";',
@@ -84,6 +84,8 @@ bash ./scripts/canonical-evidence-artifact-test.sh
84
84
  bash ./scripts/active-slice-contract-test.sh
85
85
  npm run observability-status-test
86
86
  npm run completion-role-gating-test
87
+ npm run dirty-worktree-policy-test
88
+ npm run stop-wave-epoch-test
87
89
  bash ./scripts/legacy-cleanup-test.sh
88
90
  npm run evaluator-calibration-test
89
91
  npm run rubric-contract-test
@@ -117,9 +117,11 @@ const tempRootBase = path.join(process.cwd(), '.agent', 'tmp');
117
117
  fs.mkdirSync(tempRootBase, { recursive: true });
118
118
  const tempRoot = fs.mkdtempSync(path.join(tempRootBase, 'rubric-role-reporting-'));
119
119
  const snapshotFiles = {
120
+ statePath: path.join(tempRoot, 'state.json'),
120
121
  sliceHistoryPath: path.join(tempRoot, 'slice-history.jsonl'),
121
122
  stopHistoryPath: path.join(tempRoot, 'stop-check-history.jsonl'),
122
123
  };
124
+ fs.writeFileSync(snapshotFiles.statePath, JSON.stringify({ current_stop_wave_id: 1 }, null, 2));
123
125
  fs.writeFileSync(snapshotFiles.sliceHistoryPath, '');
124
126
  fs.writeFileSync(snapshotFiles.stopHistoryPath, '');
125
127
 
@@ -204,7 +206,7 @@ const stopJudgeMalformedYesNo = `MISSION ANCHOR: test mission\nRemaining contrac
204
206
  recordedAt: 5,
205
207
  });
206
208
  assert(judged.errors.length === 0, `stop-judge valid report should transcribe cleanly: ${judged.errors.join(' | ')}`);
207
- assert(judged.appended.includes('judgment:555555555555'), 'stop-judge transcription should append judgment record');
209
+ assert(judged.appended.includes('judgment:555555555555:wave:1'), 'stop-judge transcription should append judgment record for the active stop-wave epoch');
208
210
  assert(readJsonl(snapshotFiles.stopHistoryPath).length === 1, 'stop-judge transcription should create one judgment record');
209
211
 
210
212
  const judgeRejected = await transcribeCanonicalRoleReport({
@@ -273,15 +273,20 @@ reminder = Path(sys.argv[3])
273
273
  handoff = Path(sys.argv[4])
274
274
  auto_resume = Path(sys.argv[5])
275
275
 
276
- assert reminder.exists(), 'active workflow should inject the completion reminder on subsequent non-/cook turns'
277
- reminder_text = reminder.read_text()
278
- assert 'Completion workflow detected.' in reminder_text, 'active workflow reminder should inject canonical workflow context'
279
- assert 'If continuation_policy == continue, do not stop after a slice or ask whether to continue; dispatch the next mandatory role directly.' in reminder_text, 'active workflow reminder should direct mandatory continuation'
280
- assert not handoff.exists(), 'active workflow should not fall back to the ordinary /cook handoff boundary reminder'
281
- if auto_resume.exists():
282
- auto_resume_text = auto_resume.read_text()
283
- assert 'COMPLETION WORKFLOW DRIVER' in auto_resume_text, 'auto-resume prompt should use the workflow driver format when it is queued'
284
- assert 'Resume the completion workflow from canonical state.' in auto_resume_text, 'auto-resume prompt should resume canonical workflow state when it is queued'
276
+ assert not reminder.exists(), 'ordinary non-/cook turn should not inject completion reminder solely from canonical state'
277
+ assert handoff.exists(), 'ordinary non-/cook turn should inject the /cook handoff boundary reminder'
278
+ handoff_text = handoff.read_text()
279
+ assert 'ordinary main chat unless the user explicitly runs /cook' in handoff_text, 'ordinary handoff reminder should preserve explicit /cook workflow entry'
280
+ assert 'directly implement requested repo changes, including multi-file work' in handoff_text, 'ordinary handoff reminder should allow direct ordinary-chat implementation'
281
+ assert 'Do not proactively tell the user to run /cook' in handoff_text, 'ordinary handoff reminder should keep ordinary chat neutral until explicit /cook entry'
282
+ assert '/cook is optional workflow mode' in handoff_text, 'ordinary handoff reminder should position /cook as optional workflow mode'
283
+ assert 'In ordinary chat, do not load or follow completion-protocol, and do not call completion_role.' in handoff_text, 'ordinary handoff reminder should forbid workflow-role routing before explicit /cook'
284
+ assert 'If the user wants direct implementation now, stay in ordinary chat and help directly instead of blocking on /cook.' in handoff_text, 'ordinary handoff reminder should avoid blocking implementation on /cook'
285
+ assert 'the extension should call a primary-agent handoff synthesis step from the current task context' in handoff_text, 'ordinary handoff reminder should describe same-entry primary-agent handoff synthesis for /cook'
286
+ assert 'Do not expect /cook to infer or guess startup intent from recent discussion alone' in handoff_text, 'ordinary handoff reminder should forbid /cook-side guessing'
287
+ assert 'do not silently rewrite discussion into canonical workflow state' in handoff_text, 'ordinary handoff reminder should preserve non-canonical ordinary-chat behavior'
288
+ assert not auto_resume.exists(), 'ordinary non-/cook turn should not queue auto-resume before /cook activation'
289
+ assert 'Skipped completion workflow auto-resume prompt (test mode)' not in output, 'ordinary non-/cook turn should not attempt auto-resume'
285
290
  PY
286
291
 
287
292
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
@@ -303,11 +308,10 @@ routing = json.loads(Path(sys.argv[2]).read_text())
303
308
  chooser_path = Path(sys.argv[3])
304
309
  state = json.loads(Path('.agent/state.json').read_text())
305
310
 
306
- if resume_path.exists():
307
- resume = resume_path.read_text()
308
- assert 'Canonical routing profile:' in resume, 'resume prompt should expose canonical routing profile when it is queued'
309
- assert f'- task_type: {expected_task_type}' in resume, 'resume prompt missing canonical task_type when it is queued'
310
- assert f'- evaluation_profile: {expected_eval_profile}' in resume, 'resume prompt missing canonical evaluation_profile when it is queued'
311
+ resume = resume_path.read_text()
312
+ assert 'Canonical routing profile:' in resume, 'resume prompt should expose canonical routing profile'
313
+ assert f'- task_type: {expected_task_type}' in resume, 'resume prompt missing canonical task_type'
314
+ assert f'- evaluation_profile: {expected_eval_profile}' in resume, 'resume prompt missing canonical evaluation_profile'
311
315
  assert routing['mode'] == 'bare', 'active bare /cook should snapshot bare routing mode'
312
316
  assert routing['action'] == 'continue', 'no-discussion active bare /cook should resume from canonical state without a concrete replacement mission'
313
317
  assert routing['reason'] == 'missing_explicit_handoff', 'no-discussion active bare /cook should explain that resume happened because no fresh explicit handoff existed'
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ TMPDIR="$(mktemp -d)"
8
+ trap 'rm -rf "$TMPDIR"' EXIT
9
+ REPO="$TMPDIR/repo"
10
+ mkdir -p "$REPO/.agent"
11
+ cd "$REPO"
12
+
13
+ git init -q
14
+ git config user.name "stop-wave-epoch-test"
15
+ git config user.email "stop-wave-epoch-test@example.invalid"
16
+ printf '# stop wave epoch fixture\n' > README.md
17
+ git add README.md
18
+ git commit -q -m "fixture baseline"
19
+ HEAD_SHA="$(git rev-parse HEAD)"
20
+
21
+ cp "$ROOT/.agent/README.md" .agent/README.md
22
+ cp "$ROOT/.agent/mission.md" .agent/mission.md
23
+ cp "$ROOT/.agent/profile.json" .agent/profile.json
24
+ cp "$ROOT/.agent/verify_completion_control_plane.sh" .agent/verify_completion_control_plane.sh
25
+ cp "$ROOT/.agent/verify_completion_stop.sh" .agent/verify_completion_stop.sh
26
+ chmod +x .agent/verify_completion_control_plane.sh .agent/verify_completion_stop.sh
27
+ python3 - <<'PY'
28
+ from pathlib import Path
29
+ path = Path('.agent/verify_completion_stop.sh')
30
+ text = path.read_text()
31
+ path.write_text(text.replace('npm run release-check >/dev/null', 'true'))
32
+ PY
33
+
34
+ git add .agent/README.md .agent/mission.md .agent/profile.json .agent/verify_completion_control_plane.sh .agent/verify_completion_stop.sh
35
+ git commit -q -m "scaffold tracked completion contract files"
36
+ HEAD_SHA="$(git rev-parse HEAD)"
37
+
38
+ HEAD_SHA="$HEAD_SHA" python3 - <<'PY'
39
+ import json
40
+ import os
41
+ from pathlib import Path
42
+ head = os.environ['HEAD_SHA']
43
+ mission = 'Stop-wave epoch regression fixture.'
44
+ profile = json.loads(Path('.agent/profile.json').read_text())
45
+ state = {
46
+ 'schema_version': 1,
47
+ 'mission_anchor': mission,
48
+ 'workflow_entry_status': 'active',
49
+ 'workflow_entry_source': '/cook',
50
+ 'workflow_entry_confirmed_at': '2026-05-24T00:00:00Z',
51
+ 'workflow_session_id': 'stop-wave-epoch-fixture',
52
+ 'startup_brief_path': '.agent/startup-brief.json',
53
+ 'current_phase': 'stop_wave',
54
+ 'continuation_policy': 'continue',
55
+ 'continuation_reason': 'Restart stop wave on the same HEAD after earlier no-stop evidence became stale.',
56
+ 'project_done': False,
57
+ 'task_type': profile['task_type'],
58
+ 'evaluation_profile': profile['evaluation_profile'],
59
+ 'requires_reground': False,
60
+ 'slices_since_last_reground': 0,
61
+ 'remaining_release_blockers': 0,
62
+ 'remaining_high_value_gaps': 0,
63
+ 'unsatisfied_contract_ids': [],
64
+ 'release_blocker_ids': [],
65
+ 'next_mandatory_action': 'Collect stop-wave judgments for the restarted epoch.',
66
+ 'next_mandatory_role': 'completion-stop-judge',
67
+ 'remaining_stop_judges': 2,
68
+ 'current_stop_wave_id': 2,
69
+ 'last_reground_at': '2026-05-24T00:00:00Z',
70
+ 'last_auditor_verdict': 'pass',
71
+ 'contract_status': 'stop_wave_pending_judgments',
72
+ 'latest_completed_slice': head,
73
+ 'latest_verified_slice': head,
74
+ }
75
+ startup_brief = {
76
+ 'schema_version': 1,
77
+ 'artifact_type': 'completion-startup-brief',
78
+ 'source': 'test',
79
+ 'confirmed': True,
80
+ 'confirmed_at': '2026-05-24T00:00:00Z',
81
+ 'mission': mission,
82
+ 'goal_text': f'Mission: {mission}',
83
+ 'scope': [],
84
+ 'constraints': [],
85
+ 'acceptance': [],
86
+ 'risks': [],
87
+ 'notes': ['stop-wave epoch test fixture'],
88
+ 'task_type': profile['task_type'],
89
+ 'evaluation_profile': profile['evaluation_profile'],
90
+ }
91
+ plan = {
92
+ 'schema_version': 1,
93
+ 'mission_anchor': mission,
94
+ 'task_type': profile['task_type'],
95
+ 'evaluation_profile': profile['evaluation_profile'],
96
+ 'last_reground_at': '2026-05-24T00:00:00Z',
97
+ 'plan_basis': 'stop_wave_epoch_fixture',
98
+ 'candidate_slices': [],
99
+ }
100
+ active = {
101
+ 'schema_version': 1,
102
+ 'mission_anchor': mission,
103
+ 'task_type': profile['task_type'],
104
+ 'evaluation_profile': profile['evaluation_profile'],
105
+ 'status': 'idle',
106
+ 'slice_id': None,
107
+ 'goal': None,
108
+ 'contract_ids': [],
109
+ 'acceptance_criteria': [],
110
+ 'priority': None,
111
+ 'why_now': None,
112
+ 'blocked_on': [],
113
+ 'locked_notes': [],
114
+ 'must_fix_findings': [],
115
+ 'implementation_surfaces': [],
116
+ 'verification_commands': [],
117
+ 'basis_commit': None,
118
+ 'remaining_contract_ids_before': [],
119
+ 'release_blocker_count_before': None,
120
+ 'high_value_gap_count_before': None,
121
+ }
122
+ evidence = {
123
+ 'schema_version': 1,
124
+ 'artifact_type': 'completion-verification-evidence',
125
+ 'subject_type': 'none',
126
+ 'slice_id': None,
127
+ 'goal': None,
128
+ 'contract_ids': [],
129
+ 'basis_commit': None,
130
+ 'head_sha': None,
131
+ 'verification_commands': [],
132
+ 'outcome': 'not_recorded',
133
+ 'recorded_at': None,
134
+ 'summary': 'No selected-slice verification evidence is required for the stop-wave epoch fixture.',
135
+ }
136
+ Path('.agent/state.json').write_text(json.dumps(state, indent=2) + '\n')
137
+ Path('.agent/startup-brief.json').write_text(json.dumps(startup_brief, indent=2) + '\n')
138
+ Path('.agent/plan.json').write_text(json.dumps(plan, indent=2) + '\n')
139
+ Path('.agent/active-slice.json').write_text(json.dumps(active, indent=2) + '\n')
140
+ Path('.agent/verification-evidence.json').write_text(json.dumps(evidence, indent=2) + '\n')
141
+ Path('.agent/stop-check-history.jsonl').write_text(json.dumps({
142
+ 'schema_version': 1,
143
+ 'type': 'judgment',
144
+ 'recorded_at': 1,
145
+ 'head_sha': head,
146
+ 'stop_wave_id': 1,
147
+ 'can_stop': False,
148
+ 'blocker_count': 1,
149
+ 'high_value_gap_count': 0,
150
+ }) + '\n')
151
+ Path('.agent/slice-history.jsonl').write_text('')
152
+ PY
153
+
154
+ HEAD_SHA="$HEAD_SHA" python3 - <<'PY'
155
+ import os, subprocess
156
+ combined = subprocess.run(['bash', '.agent/verify_completion_stop.sh'], text=True, capture_output=True)
157
+ text = combined.stdout + combined.stderr
158
+ assert combined.returncode != 0, 'expected stop verifier to fail before current stop-wave judgments are recorded'
159
+ assert f'Need 2 valid current-HEAD judgments for HEAD {os.environ["HEAD_SHA"]} in stop_wave_id 2; found 0.' in text, text
160
+ assert 'Current HEAD has a can_stop=no judgment' not in text, text
161
+ PY
162
+
163
+ HEAD_SHA="$HEAD_SHA" python3 - <<'PY'
164
+ import json, os
165
+ from pathlib import Path
166
+ head = os.environ['HEAD_SHA']
167
+ records = [
168
+ {
169
+ 'schema_version': 1,
170
+ 'type': 'judgment',
171
+ 'recorded_at': 2,
172
+ 'head_sha': head,
173
+ 'stop_wave_id': 2,
174
+ 'can_stop': True,
175
+ 'blocker_count': 0,
176
+ 'high_value_gap_count': 0,
177
+ },
178
+ {
179
+ 'schema_version': 1,
180
+ 'type': 'judgment',
181
+ 'recorded_at': 3,
182
+ 'head_sha': head,
183
+ 'stop_wave_id': 2,
184
+ 'can_stop': True,
185
+ 'blocker_count': 0,
186
+ 'high_value_gap_count': 0,
187
+ },
188
+ ]
189
+ with Path('.agent/stop-check-history.jsonl').open('a', encoding='utf8') as fh:
190
+ for record in records:
191
+ fh.write(json.dumps(record) + '\n')
192
+ PY
193
+
194
+ bash .agent/verify_completion_stop.sh >/dev/null
195
+
196
+ ROOT_PATH="$ROOT" node - <<'NODE'
197
+ const fs = require('node:fs');
198
+ const path = require('node:path');
199
+ const { parseReportFields, transcribeCanonicalRoleReport } = require(path.join(process.env.ROOT_PATH, 'extensions/completion/role-reporting.js'));
200
+ (async () => {
201
+ const report = `MISSION ANCHOR: epoch mission\nRemaining contract IDs: none\nRubric:\n- Contract coverage: pass - All implementation slices are accepted on HEAD.\n- Correctness risk: pass - No remaining blocker or high-value gap is evident.\n- Verification evidence: pass - Final verification passes for the current head.\n- Docs/state parity: pass - Docs, config, and canonical state match shipped behavior.\nCan the project stop now: yes\nExact remaining open top-level contract IDs: none\nBlocker count: 0\nHigh-value gap count: 0\nLatest completed slice commit: abcdef1234567890abcdef1234567890abcdef12\nDocs/config/runbooks match shipped behavior: yes\nTracked and unignored worktree is clean: yes\nBrief justification: Stop-wave epoch transcription should capture the active stop_wave_id.`;
202
+ const headSha = require('node:child_process').execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
203
+ const result = await transcribeCanonicalRoleReport({
204
+ role: 'completion-stop-judge',
205
+ output: report,
206
+ reportFields: parseReportFields(report),
207
+ snapshotFiles: {
208
+ statePath: path.join(process.cwd(), '.agent/state.json'),
209
+ stopHistoryPath: path.join(process.cwd(), '.agent/stop-check-history.jsonl'),
210
+ sliceHistoryPath: path.join(process.cwd(), '.agent/slice-history.jsonl'),
211
+ },
212
+ headSha,
213
+ recordedAt: 4,
214
+ });
215
+ if (result.errors.length > 0) throw new Error(result.errors.join(' | '));
216
+ const lines = fs.readFileSync('.agent/stop-check-history.jsonl', 'utf8').trim().split('\n').map((line) => JSON.parse(line));
217
+ const last = lines[lines.length - 1];
218
+ if (last.stop_wave_id !== 2) throw new Error('transcribed stop judgment must include current stop_wave_id 2');
219
+ })();
220
+ NODE
221
+
222
+ echo "stop-wave epoch test passed"
@@ -32,10 +32,11 @@ This skill defines shared protocol facts only. Role-specific behavior belongs in
32
32
  - Run exactly one implementation slice at a time.
33
33
  - A slice is not complete unless it lands as a new commit.
34
34
  - Before selecting or advancing to the next slice after a committed slice, the tracked and unignored worktree must be clean. If it is not clean, treat that dirty state as a blocker to next-slice progression and reopen or continue the latest slice for reconciliation.
35
+ - When that dirty tracked worktree contains changes unrelated to the latest slice or current reconciliation surfaces and those changes can be isolated safely, auto-preserve them with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note, continue the mandatory workflow step, and restore them before handing control back. Ask the user only when overlap, ownership ambiguity, or stash/restore conflicts make automatic isolation unsafe.
35
36
  - Docs, config, and runbooks must stay truthful to shipped behavior.
36
37
  - `.agent/verify_completion_stop.sh` is a generated repo-level baseline verifier. Onboarding should create a working version from current repo truth rather than an unconditional failing placeholder.
37
38
  - The packaged default stop policy is `required_stop_judges: 2` plus `stop_aggregation_policy: "unanimous-current-head-v1"` in `.agent/profile.json`.
38
- - Under `unanimous-current-head-v1`, only current-HEAD `judgment` records count, any current-HEAD `can_stop = no` fails closed, and repo-level stop verification must wait until the required current-HEAD judgments are recorded.
39
+ - Under `unanimous-current-head-v1`, only current-HEAD `judgment` records from the current stop-wave epoch count. Canonical `state.json current_stop_wave_id` tracks that epoch, may be incremented to restart stop evaluation on the same `HEAD`, and repo-level stop verification must wait until the required current-HEAD judgments for the current epoch are recorded.
39
40
  - Keep slice-specific proof in repo tests or deterministic checks. Refresh `.agent/verify_completion_stop.sh` only when the repo's top-level verification surfaces change or the verifier becomes stale.
40
41
  - The workflow topology is flat and primary-driven: the main pi session remains the workflow root and invokes at most one completion role at a time.
41
42
  - No completion role may invoke another completion role during the normal workflow.
@@ -85,7 +86,7 @@ If the workflow driver detects that the next mandatory action belongs to a compl
85
86
  6. If the latest committed slice lacks an audit result, invoke `completion-auditor`.
86
87
  7. If review or audit have returned and canonical reconciliation is needed, invoke `completion-regrounder`. `completion-regrounder` must not select or hand off a next slice while the latest committed slice leaves the tracked and unignored worktree dirty; instead it must reopen or continue that latest slice for reconciliation.
87
88
  8. If all planned slices are done and final closure is being evaluated, invoke the required `completion-stop-judge` sessions directly.
88
- 9. After each required current-HEAD `completion-stop-judge` result is faithfully recorded, rerun `bash .agent/verify_completion_stop.sh` and invoke `completion-regrounder` for final stop reconciliation.
89
+ 9. After each required current-HEAD `completion-stop-judge` result for the current `current_stop_wave_id` is faithfully recorded, rerun `bash .agent/verify_completion_stop.sh` and invoke `completion-regrounder` for final stop reconciliation.
89
90
 
90
91
  The workflow driver must not substitute itself for any mandatory dispatch target above.
91
92
 
@@ -87,6 +87,7 @@ Required fields:
87
87
  - `next_mandatory_action`
88
88
  - `next_mandatory_role`
89
89
  - `remaining_stop_judges`
90
+ - `current_stop_wave_id`
90
91
  - `last_reground_at`
91
92
  - `last_auditor_verdict`
92
93
  - `contract_status`
@@ -168,6 +169,7 @@ Rules:
168
169
  3. Done requires all satisfied. A slice may only transition to `done` when every acceptance criterion is satisfied and `evidence` contains the proof for each one.
169
170
  4. Re-ground validation. During re-ground, the current slice backlog must be revalidated against repo truth. A slice previously marked `done` whose criteria no longer hold must be reopened.
170
171
  5. Clean handoff before next slice. After a committed slice is reviewed and audited, the tracked and unignored worktree must be clean before the next slice is selected.
172
+ 6. Dirty-worktree auto-reconcile. If tracked worktree dirt is unrelated to the latest slice or current reconciliation surfaces and can be isolated safely, the workflow should auto-preserve it with a reversible mechanism such as a named git stash plus a `.agent/tmp/dirty-worktree-autostash.json` note, continue the mandatory workflow step, and restore it before handing control back. Ask the user only when overlap, ownership ambiguity, or stash/restore conflicts make automatic isolation unsafe.
171
173
 
172
174
  `active-slice.json` carries one current slice cursor.
173
175
 
@@ -249,6 +251,7 @@ Minimum record shape:
249
251
  - `type`
250
252
  - `recorded_at`
251
253
  - `head_sha`
254
+ - `stop_wave_id`
252
255
  - `can_stop`
253
256
  - `blocker_count`
254
257
  - `high_value_gap_count`
@@ -264,11 +267,12 @@ The packaged default stop policy is:
264
267
 
265
268
  Policy meaning:
266
269
 
267
- - count only `judgment` records whose `head_sha` matches the current `HEAD`
268
- - require at least two valid current-HEAD judgments before repo-level stop verification may run
269
- - fail closed if any current-HEAD judgment has `can_stop = false`
270
- - fail closed if a current-HEAD judgment is malformed or carries non-zero blocker/high-value-gap counts
271
- - rerun `bash .agent/verify_completion_stop.sh` only after the required current-HEAD judgments are faithfully recorded, then hand final reconciliation back to `completion-regrounder`
270
+ - `state.json current_stop_wave_id` is the current stop-wave epoch for the current mission and may be incremented to restart stop evaluation on the same `HEAD`
271
+ - count only `judgment` records whose `head_sha` matches the current `HEAD` and whose `stop_wave_id` matches `state.json current_stop_wave_id`
272
+ - require at least two valid current-HEAD judgments for the current stop-wave epoch before repo-level stop verification may run
273
+ - fail closed if any current-HEAD judgment in the current stop-wave epoch has `can_stop = false`
274
+ - fail closed if a current-HEAD judgment in the current stop-wave epoch is malformed or carries non-zero blocker/high-value-gap counts
275
+ - rerun `bash .agent/verify_completion_stop.sh` only after the required current-HEAD judgments for the current stop-wave epoch are faithfully recorded, then hand final reconciliation back to `completion-regrounder`
272
276
 
273
277
  ## Structured Evaluation Rubric Foundation
274
278