@sienklogic/plan-build-run 2.21.0 → 2.21.1

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CLAUDE.md +2 -2
  3. package/package.json +1 -1
  4. package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +1 -1
  5. package/plugins/copilot-pbr/agents/debugger.agent.md +2 -0
  6. package/plugins/copilot-pbr/agents/executor.agent.md +3 -1
  7. package/plugins/copilot-pbr/agents/general.agent.md +2 -1
  8. package/plugins/copilot-pbr/agents/integration-checker.agent.md +2 -0
  9. package/plugins/copilot-pbr/agents/plan-checker.agent.md +1 -1
  10. package/plugins/copilot-pbr/agents/planner.agent.md +3 -1
  11. package/plugins/copilot-pbr/agents/researcher.agent.md +5 -3
  12. package/plugins/copilot-pbr/agents/synthesizer.agent.md +1 -1
  13. package/plugins/copilot-pbr/agents/verifier.agent.md +3 -3
  14. package/plugins/copilot-pbr/hooks/hooks.json +13 -13
  15. package/plugins/copilot-pbr/plugin.json +1 -1
  16. package/plugins/copilot-pbr/skills/audit/SKILL.md +1 -1
  17. package/plugins/copilot-pbr/skills/build/SKILL.md +4 -5
  18. package/plugins/copilot-pbr/skills/config/SKILL.md +1 -1
  19. package/plugins/copilot-pbr/skills/debug/SKILL.md +1 -1
  20. package/plugins/copilot-pbr/skills/import/SKILL.md +1 -1
  21. package/plugins/copilot-pbr/skills/plan/SKILL.md +1 -1
  22. package/plugins/copilot-pbr/skills/review/SKILL.md +4 -4
  23. package/plugins/copilot-pbr/skills/scan/SKILL.md +4 -4
  24. package/plugins/copilot-pbr/skills/shared/config-loading.md +1 -1
  25. package/plugins/copilot-pbr/skills/shared/context-budget.md +3 -3
  26. package/plugins/copilot-pbr/skills/shared/state-loading.md +1 -1
  27. package/plugins/copilot-pbr/skills/shared/state-update.md +12 -4
  28. package/plugins/copilot-pbr/skills/shared/universal-anti-patterns.md +1 -1
  29. package/plugins/copilot-pbr/templates/ROADMAP.md.tmpl +7 -0
  30. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  31. package/plugins/cursor-pbr/agents/codebase-mapper.md +1 -1
  32. package/plugins/cursor-pbr/agents/debugger.md +2 -0
  33. package/plugins/cursor-pbr/agents/executor.md +3 -1
  34. package/plugins/cursor-pbr/agents/general.md +2 -1
  35. package/plugins/cursor-pbr/agents/integration-checker.md +2 -0
  36. package/plugins/cursor-pbr/agents/plan-checker.md +1 -1
  37. package/plugins/cursor-pbr/agents/planner.md +3 -1
  38. package/plugins/cursor-pbr/agents/researcher.md +5 -3
  39. package/plugins/cursor-pbr/agents/synthesizer.md +1 -1
  40. package/plugins/cursor-pbr/agents/verifier.md +3 -3
  41. package/plugins/cursor-pbr/skills/audit/SKILL.md +1 -1
  42. package/plugins/cursor-pbr/skills/build/SKILL.md +4 -5
  43. package/plugins/cursor-pbr/skills/config/SKILL.md +1 -1
  44. package/plugins/cursor-pbr/skills/debug/SKILL.md +1 -1
  45. package/plugins/cursor-pbr/skills/import/SKILL.md +1 -1
  46. package/plugins/cursor-pbr/skills/plan/SKILL.md +1 -1
  47. package/plugins/cursor-pbr/skills/review/SKILL.md +4 -4
  48. package/plugins/cursor-pbr/skills/scan/SKILL.md +4 -4
  49. package/plugins/cursor-pbr/skills/shared/config-loading.md +1 -1
  50. package/plugins/cursor-pbr/skills/shared/context-budget.md +3 -3
  51. package/plugins/cursor-pbr/skills/shared/state-loading.md +1 -1
  52. package/plugins/cursor-pbr/skills/shared/state-update.md +12 -4
  53. package/plugins/cursor-pbr/skills/shared/universal-anti-patterns.md +1 -1
  54. package/plugins/cursor-pbr/templates/ROADMAP.md.tmpl +7 -0
  55. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  56. package/plugins/pbr/agents/codebase-mapper.md +1 -1
  57. package/plugins/pbr/agents/debugger.md +2 -0
  58. package/plugins/pbr/agents/executor.md +3 -1
  59. package/plugins/pbr/agents/general.md +2 -1
  60. package/plugins/pbr/agents/integration-checker.md +2 -0
  61. package/plugins/pbr/agents/plan-checker.md +0 -1
  62. package/plugins/pbr/agents/planner.md +2 -1
  63. package/plugins/pbr/agents/researcher.md +2 -0
  64. package/plugins/pbr/agents/synthesizer.md +0 -1
  65. package/plugins/pbr/agents/verifier.md +1 -1
  66. package/plugins/pbr/commands/do.md +5 -0
  67. package/plugins/pbr/scripts/check-phase-boundary.js +2 -8
  68. package/plugins/pbr/scripts/check-plan-format.js +78 -2
  69. package/plugins/pbr/scripts/check-skill-workflow.js +3 -11
  70. package/plugins/pbr/scripts/check-state-sync.js +18 -8
  71. package/plugins/pbr/scripts/check-subagent-output.js +78 -6
  72. package/plugins/pbr/scripts/log-tool-failure.js +1 -4
  73. package/plugins/pbr/scripts/pre-write-dispatch.js +0 -1
  74. package/plugins/pbr/scripts/status-line.js +44 -11
  75. package/plugins/pbr/scripts/validate-commit.js +8 -7
  76. package/plugins/pbr/scripts/validate-skill-args.js +2 -1
  77. package/plugins/pbr/scripts/validate-task.js +0 -5
  78. package/plugins/pbr/skills/build/SKILL.md +4 -5
  79. package/plugins/pbr/skills/health/SKILL.md +0 -2
  80. package/plugins/pbr/skills/plan/SKILL.md +1 -1
  81. package/plugins/pbr/skills/review/SKILL.md +4 -4
  82. package/plugins/pbr/skills/shared/state-update.md +10 -2
  83. package/plugins/pbr/templates/ROADMAP.md.tmpl +2 -0
@@ -152,21 +152,76 @@ const AGENT_OUTPUTS = {
152
152
  }
153
153
  };
154
154
 
155
+ /**
156
+ * Extract current phase number from STATE.md, preferring frontmatter over body.
157
+ * @param {string} stateContent - Full STATE.md content
158
+ * @returns {string|null} Phase number string or null
159
+ */
160
+ function getCurrentPhase(stateContent) {
161
+ // Prefer frontmatter (always up-to-date)
162
+ const fmMatch = stateContent.match(/^current_phase:\s*(\d+)/m);
163
+ if (fmMatch) return fmMatch[1];
164
+ // Fall back to body text
165
+ const bodyMatch = stateContent.match(/Phase:\s*(\d+)\s+of\s+\d+/);
166
+ return bodyMatch ? bodyMatch[1] : null;
167
+ }
168
+
169
+ /**
170
+ * Check if ROADMAP.md is stale after executor/verifier completion.
171
+ * Detects: (1) no Progress table for current milestone, (2) table exists but
172
+ * phase row is out of date vs. phase artifacts on disk.
173
+ *
174
+ * @param {string} planningDir - Path to .planning/
175
+ * @returns {string|null} Warning message or null if in sync
176
+ */
177
+ function checkRoadmapStaleness(planningDir) {
178
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
179
+ if (!fs.existsSync(roadmapPath)) return null;
180
+
181
+ try {
182
+ const content = fs.readFileSync(roadmapPath, 'utf8');
183
+
184
+ // Check if there's a Progress table at all
185
+ const hasProgressTable = /Plans\s*Complete/i.test(content);
186
+ if (!hasProgressTable) {
187
+ return 'ROADMAP.md has no Progress table for the current milestone. The orchestrator should add a Progress table with columns: Phase | Plans Complete | Status | Completed. See skills/shared/state-update.md for format.';
188
+ }
189
+
190
+ // If table exists, check if current phase row is present
191
+ const stateFile = path.join(planningDir, 'STATE.md');
192
+ if (fs.existsSync(stateFile)) {
193
+ const stateContent = fs.readFileSync(stateFile, 'utf8');
194
+ const currentPhase = getCurrentPhase(stateContent);
195
+ if (currentPhase) {
196
+ const paddedPhase = currentPhase.padStart(2, '0');
197
+ const phaseInTable = new RegExp(`\\|\\s*${paddedPhase}\\.`).test(content) ||
198
+ new RegExp(`\\|\\s*${parseInt(currentPhase, 10)}\\.`).test(content);
199
+ if (!phaseInTable) {
200
+ return `ROADMAP.md Progress table exists but has no row for Phase ${currentPhase}. Add a row for the current phase.`;
201
+ }
202
+ }
203
+ }
204
+ } catch (_e) {
205
+ // best-effort
206
+ }
207
+ return null;
208
+ }
209
+
155
210
  function findInPhaseDir(planningDir, pattern) {
156
211
  const matches = [];
157
212
  const phasesDir = path.join(planningDir, 'phases');
158
213
  if (!fs.existsSync(phasesDir)) return matches;
159
214
 
160
215
  try {
161
- // Find the active phase from STATE.md
216
+ // Find the active phase from STATE.md (prefer frontmatter over body)
162
217
  const stateFile = path.join(planningDir, 'STATE.md');
163
218
  if (!fs.existsSync(stateFile)) return matches;
164
219
 
165
220
  const stateContent = fs.readFileSync(stateFile, 'utf8');
166
- const phaseMatch = stateContent.match(/Phase:\s*(\d+)\s+of\s+\d+/);
167
- if (!phaseMatch) return matches;
221
+ const phaseNum = getCurrentPhase(stateContent);
222
+ if (!phaseNum) return matches;
168
223
 
169
- const currentPhase = phaseMatch[1].padStart(2, '0');
224
+ const currentPhase = phaseNum.padStart(2, '0');
170
225
  const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase));
171
226
  if (dirs.length === 0) return matches;
172
227
 
@@ -288,6 +343,23 @@ function main() {
288
343
  // Skill-specific post-completion validation
289
344
  const skillWarnings = [];
290
345
 
346
+ // ACTIVE-SKILL ENFORCEMENT: Warn when no .active-skill file exists.
347
+ // Skills are instructed (with CRITICAL markers) to write this file, but LLMs
348
+ // skip it under cognitive load. This warning reminds the orchestrator.
349
+ if (!activeSkill && agentType !== 'pbr:general' && agentType !== 'pbr:plan-checker' && agentType !== 'pbr:integration-checker') {
350
+ skillWarnings.push('.active-skill file is missing — the orchestrating skill never wrote it. This means skill-workflow guards were inactive for this entire operation. CRITICAL: Write the skill name to .planning/.active-skill BEFORE spawning agents.');
351
+ }
352
+
353
+ // ROADMAP.md SYNC: After executor or verifier completes, check if ROADMAP.md
354
+ // needs updating. Subagent writes (SUMMARY/VERIFICATION) trigger check-state-sync
355
+ // in the subagent context, but the main context ROADMAP.md may still be stale.
356
+ if (agentType === 'pbr:executor' || agentType === 'pbr:verifier') {
357
+ const roadmapWarning = checkRoadmapStaleness(planningDir);
358
+ if (roadmapWarning) {
359
+ skillWarnings.push(roadmapWarning);
360
+ }
361
+ }
362
+
291
363
  // Mtime-based recency check for researcher and synthesizer
292
364
  if (found._stale && (agentType === 'pbr:researcher' || agentType === 'pbr:synthesizer')) {
293
365
  const label = agentType === 'pbr:researcher' ? 'Researcher' : 'Synthesizer';
@@ -333,7 +405,7 @@ function main() {
333
405
  const verFiles = findInPhaseDir(planningDir, /^VERIFICATION\.md$/i);
334
406
  for (const vf of verFiles) {
335
407
  try {
336
- const content = fs.readFileSync(vf, 'utf8');
408
+ const content = fs.readFileSync(path.join(planningDir, vf), 'utf8');
337
409
  const statusMatch = content.match(/^status:\s*(\S+)/mi);
338
410
  if (statusMatch && statusMatch[1] === 'gaps_found') {
339
411
  skillWarnings.push('Review verifier: VERIFICATION.md has status "gaps_found" — ensure gaps are surfaced to the user.');
@@ -386,5 +458,5 @@ function main() {
386
458
  process.exit(0);
387
459
  }
388
460
 
389
- module.exports = { AGENT_OUTPUTS, findInPhaseDir, findInQuickDir, checkSummaryCommits, isRecent };
461
+ module.exports = { AGENT_OUTPUTS, findInPhaseDir, findInQuickDir, checkSummaryCommits, isRecent, getCurrentPhase, checkRoadmapStaleness };
390
462
  if (require.main === module || process.argv[1] === __filename) { main(); }
@@ -55,10 +55,7 @@ async function main() {
55
55
  // Provide recovery hints for Bash failures (most common actionable failure)
56
56
  if (toolName === 'Bash' && !isInterrupt) {
57
57
  const output = {
58
- hookSpecificOutput: {
59
- hookEventName: 'PostToolUseFailure',
60
- additionalContext: 'Bash command failed. If this is a recurring issue, consider using /pbr:debug for systematic investigation.'
61
- }
58
+ additionalContext: 'Bash command failed. If this is a recurring issue, consider using /pbr:debug for systematic investigation.'
62
59
  };
63
60
  process.stdout.write(JSON.stringify(output));
64
61
  }
@@ -115,7 +115,6 @@ function main() {
115
115
  const currentPhase = stateContent.match(/Phase:\s*(\d+)\s+of/);
116
116
  if (currentPhase && parseInt(phaseMatch[1], 10) !== parseInt(currentPhase[1], 10)) {
117
117
  process.stdout.write(JSON.stringify({
118
- decision: 'allow',
119
118
  additionalContext: `[pbr] Advisory: writing to phase ${phaseMatch[1]} but current phase is ${currentPhase[1]}. Ensure this cross-phase write is intentional.`
120
119
  }));
121
120
  process.exit(0);
@@ -197,6 +197,23 @@ function main() {
197
197
  process.exit(0);
198
198
  }
199
199
 
200
+ /**
201
+ * Parse YAML frontmatter from STATE.md content.
202
+ * Returns an object with frontmatter fields, or null if no frontmatter.
203
+ */
204
+ function parseFrontmatter(content) {
205
+ if (!content.startsWith('---')) return null;
206
+ const endIdx = content.indexOf('---', 3);
207
+ if (endIdx === -1) return null;
208
+ const fm = content.substring(3, endIdx);
209
+ const result = {};
210
+ for (const line of fm.split(/\r?\n/)) {
211
+ const m = line.match(/^(\w[\w_]*):\s*"?([^"]*)"?\s*$/);
212
+ if (m) result[m[1]] = m[2];
213
+ }
214
+ return result;
215
+ }
216
+
200
217
  function buildStatusLine(content, ctxPercent, cfg, stdinData) {
201
218
  const config = cfg || DEFAULTS;
202
219
  const sections = config.sections || DEFAULTS.sections;
@@ -205,15 +222,26 @@ function buildStatusLine(content, ctxPercent, cfg, stdinData) {
205
222
  const barCfg = config.context_bar || DEFAULTS.context_bar;
206
223
  const sd = stdinData || {};
207
224
 
225
+ // Prefer frontmatter (always up-to-date) over body text (may be stale)
226
+ const fm = parseFrontmatter(content);
227
+
208
228
  const parts = [];
209
229
 
210
230
  // Phase section (always includes brand text)
211
231
  if (sections.includes('phase')) {
232
+ const fmPhase = fm && fm.current_phase;
233
+ const fmTotal = fm && fm.total_phases;
234
+ const fmName = fm && fm.phase_name;
212
235
  const phaseMatch = content.match(/Phase:\s*(\d+)\s*of\s*(\d+)\s*(?:\(([^)]+)\))?/);
213
- if (phaseMatch) {
214
- parts.push(`${c.boldCyan}${brandText}${c.reset} ${c.bold}Phase ${phaseMatch[1]}/${phaseMatch[2]}${c.reset}`);
215
- if (phaseMatch[3]) {
216
- parts.push(`${c.magenta}${phaseMatch[3]}${c.reset}`);
236
+
237
+ const phaseNum = fmPhase || (phaseMatch && phaseMatch[1]);
238
+ const phaseTotal = fmTotal || (phaseMatch && phaseMatch[2]);
239
+ const phaseName = fmName || (phaseMatch && phaseMatch[3]);
240
+
241
+ if (phaseNum && phaseTotal) {
242
+ parts.push(`${c.boldCyan}${brandText}${c.reset} ${c.bold}Phase ${phaseNum}/${phaseTotal}${c.reset}`);
243
+ if (phaseName) {
244
+ parts.push(`${c.magenta}${phaseName}${c.reset}`);
217
245
  }
218
246
  } else {
219
247
  parts.push(`${c.boldCyan}${brandText}${c.reset}`);
@@ -222,10 +250,14 @@ function buildStatusLine(content, ctxPercent, cfg, stdinData) {
222
250
 
223
251
  // Plan section
224
252
  if (sections.includes('plan')) {
253
+ const fmComplete = fm && fm.plans_complete;
254
+ const fmTotal = fm && fm.plans_total;
225
255
  const planMatch = content.match(/Plan:\s*(\d+)\s*of\s*(\d+)/);
226
- if (planMatch) {
227
- const done = parseInt(planMatch[1], 10);
228
- const total = parseInt(planMatch[2], 10);
256
+
257
+ const done = fmComplete != null ? parseInt(fmComplete, 10) : (planMatch ? parseInt(planMatch[1], 10) : null);
258
+ const total = fmTotal != null ? parseInt(fmTotal, 10) : (planMatch ? parseInt(planMatch[2], 10) : null);
259
+
260
+ if (done != null && total != null && total > 0) {
229
261
  const planColor = done === total ? c.green : c.white;
230
262
  parts.push(`${planColor}Plan ${done}/${total}${c.reset}`);
231
263
  }
@@ -233,9 +265,10 @@ function buildStatusLine(content, ctxPercent, cfg, stdinData) {
233
265
 
234
266
  // Status section
235
267
  if (sections.includes('status')) {
236
- const statusMatch = content.match(/Status:\s*(.+)/);
237
- if (statusMatch) {
238
- const text = statusMatch[1].trim();
268
+ const fmStatus = fm && fm.status;
269
+ const statusMatch = content.match(/^Status:\s*(.+)/m);
270
+ const text = fmStatus || (statusMatch && statusMatch[1].trim());
271
+ if (text) {
239
272
  const short = text.length > maxLen ? text.slice(0, maxLen - 3) + '...' : text;
240
273
  parts.push(`${statusColor(text)}${short}${c.reset}`);
241
274
  }
@@ -285,4 +318,4 @@ function buildStatusLine(content, ctxPercent, cfg, stdinData) {
285
318
  }
286
319
 
287
320
  if (require.main === module || process.argv[1] === __filename) { main(); }
288
- module.exports = { buildStatusLine, buildContextBar, getContextPercent, getGitInfo, formatDuration, loadStatusLineConfig, DEFAULTS };
321
+ module.exports = { buildStatusLine, buildContextBar, getContextPercent, getGitInfo, formatDuration, loadStatusLineConfig, parseFrontmatter, DEFAULTS };
@@ -178,6 +178,14 @@ function isGitCommit(command) {
178
178
  }
179
179
 
180
180
  function extractCommitMessage(command) {
181
+ // Try heredoc first: -m "$(cat <<'EOF'\n...\nEOF\n)" or -m "$(cat <<EOF\n...\nEOF\n)"
182
+ // Must check before generic -m patterns to avoid capturing heredoc syntax as the message
183
+ const heredocMatch = command.match(/<<'?EOF'?\s*\n([\s\S]*?)\nEOF/);
184
+ if (heredocMatch) {
185
+ // First line of heredoc is the commit message
186
+ return heredocMatch[1].trim().split('\n')[0].trim();
187
+ }
188
+
181
189
  // Try -m "message" or -m 'message'
182
190
  const mFlagMatch = command.match(/-m\s+["']([^"']+)["']/);
183
191
  if (mFlagMatch) return mFlagMatch[1];
@@ -186,13 +194,6 @@ function extractCommitMessage(command) {
186
194
  const mFlagMatch2 = command.match(/-m\s+"([^"]+)"/);
187
195
  if (mFlagMatch2) return mFlagMatch2[1];
188
196
 
189
- // Try heredoc: -m "$(cat <<'EOF'\n...\nEOF\n)"
190
- const heredocMatch = command.match(/<<'?EOF'?\s*\n([\s\S]*?)\nEOF/);
191
- if (heredocMatch) {
192
- // First line of heredoc is the commit message
193
- return heredocMatch[1].trim().split('\n')[0].trim();
194
- }
195
-
196
197
  return null;
197
198
  }
198
199
 
@@ -101,7 +101,8 @@ function checkSkillArgs(data) {
101
101
 
102
102
  return {
103
103
  output: {
104
- additionalContext: [
104
+ decision: 'block',
105
+ reason: [
105
106
  'BLOCKED: /pbr:plan received freeform text instead of a phase number.',
106
107
  '',
107
108
  'The arguments "' + args.substring(0, 80) + (args.length > 80 ? '...' : '') + '" do not match any valid pattern.',
@@ -567,11 +567,6 @@ function checkBuildDependencyGate(data) {
567
567
  return null;
568
568
  }
569
569
 
570
- /**
571
- * Advisory check: when active skill is "build" and an executor is being
572
- * spawned, warn if .checkpoint-manifest.json is missing in the phase dir.
573
- * Returns a warning string or null.
574
- */
575
570
  /**
576
571
  * Parse VERIFICATION.md frontmatter to extract status field.
577
572
  * Returns the status string or 'unknown' if not parseable.
@@ -720,11 +720,10 @@ These return `{ success, old_status, new_status }` or `{ success, old_plans, new
720
720
 
721
721
  **CRITICAL: Update STATE.md NOW with phase completion status. Do NOT skip this step.**
722
722
 
723
- **8b. Update STATE.md:**
724
- - Phase status: {final_status from Step 8-pre}
725
- - Plan completion count
726
- - Last activity timestamp
727
- - Progress bar
723
+ **8b. Update STATE.md (CRITICAL — update BOTH frontmatter AND body):**
724
+ - Frontmatter: `status`, `plans_complete`, `last_activity`, `progress_percent`, `last_command`
725
+ - Body `## Current Position`: `Phase:` line, `Plan:` line, `Status:` line, `Last activity:` line, `Progress:` bar
726
+ - These MUST stay in sync — the status line reads frontmatter, humans read the body
728
727
 
729
728
  **8c. Commit planning docs (if configured):**
730
729
  Reference: `skills/shared/commit-planning-docs.md` for the standard commit pattern.
@@ -209,8 +209,6 @@ This ensures the user can recover the original STATE.md if the fix produces inco
209
209
 
210
210
  4. If "Skip": Do nothing, continue to the rest of the output.
211
211
 
212
- **Note:** When auto-fix is active, the health skill is no longer strictly read-only. The `allowed-tools` frontmatter must include `Write` and `AskUserQuestion` for auto-fix to work. Update the frontmatter accordingly.
213
-
214
212
  ---
215
213
 
216
214
  ## Bonus: Recent Decisions
@@ -492,7 +492,7 @@ Use AskUserQuestion (pattern: approve-revise-abort from `skills/shared/gate-prom
492
492
  4. Update the `Plans Complete` column to `0/{N}` where N = number of plan files just created
493
493
  5. Update the `Status` column to `planned`
494
494
  6. Save the file — do NOT skip this step
495
- - Update STATE.md: set current phase plan status to "planned"
495
+ - Update STATE.md **(CRITICAL update BOTH frontmatter AND body)**: set `status: "planned"`, `plans_total`, `last_command` in frontmatter AND update `Status:`, `Plan:` lines in body `## Current Position`
496
496
  - **If `features.auto_advance` is `true` AND `mode` is `autonomous`:** Chain directly to build: `Skill({ skill: "pbr:build", args: "{N}" })`. This continues the build→review→plan→build cycle automatically.
497
497
  - **Otherwise:** Suggest next action: `/pbr:build {N}`
498
498
 
@@ -331,10 +331,10 @@ If all automated checks and UAT items passed:
331
331
  4. Update the `Status` column to `verified`
332
332
  5. Update the `Completed` column to the current date (YYYY-MM-DD)
333
333
  6. Save the file — do NOT skip this step
334
- 2. Update `.planning/STATE.md`:
335
- - Phase status: "verified"
336
- - Progress updated
337
- - Last activity timestamp
334
+ 2. Update `.planning/STATE.md` **(CRITICAL — update BOTH frontmatter AND body):**
335
+ - Frontmatter: `status: "verified"`, `progress_percent`, `last_activity`, `last_command`
336
+ - Body `## Current Position`: `Status:` line, `Last activity:` line, `Progress:` bar
337
+ - These MUST stay in sync — see `skills/shared/state-update.md`
338
338
  - **STATE.md size limit:** Follow size limit enforcement rules in `skills/shared/state-update.md` (150 lines max).
339
339
  3. Update VERIFICATION.md with UAT results (append UAT section)
340
340
  3. Present completion:
@@ -4,12 +4,17 @@ Standard pattern for updating `.planning/STATE.md`. Include this fragment in ski
4
4
 
5
5
  ---
6
6
 
7
+ **CRITICAL: STATE.md has TWO representations — YAML frontmatter AND markdown body. You MUST update BOTH when changing state. The status line reads frontmatter; humans and hooks read the body. If you only update frontmatter, the body goes stale and the status line shows wrong data. Do NOT skip body updates under any circumstances.**
8
+
9
+ ---
10
+
7
11
  ## When to Update STATE.md
8
12
 
9
13
  | Event | What to Update |
10
14
  |-------|---------------|
11
- | Phase status changes (planned, building, verified) | Current Position section |
12
- | Plan completes or fails | Plan counter, status, last activity |
15
+ | Phase status changes (planned, building, verified) | Frontmatter fields AND Current Position section |
16
+ | Plan completes or fails | Frontmatter fields AND Plan counter, status, last activity |
17
+ | Phase advances to next phase | Frontmatter fields AND Phase line, Status, Last activity, Progress bar |
13
18
  | New decision made | Accumulated Context > Decisions |
14
19
  | Blocker discovered or resolved | Accumulated Context > Blockers/Concerns |
15
20
  | Session starts or ends | Session Continuity section |
@@ -31,6 +36,9 @@ See: .planning/PROJECT.md (updated {date})
31
36
  Update `Current focus` when phase changes.
32
37
 
33
38
  ### 2. Current Position (lines 9-14)
39
+
40
+ **CRITICAL: This section MUST match the frontmatter fields above it. When you update `current_phase` or `status` in frontmatter, you MUST also update the corresponding lines below. A hook will auto-fix drift, but do not rely on it.**
41
+
34
42
  ```
35
43
  ## Current Position
36
44
  Phase: {N} of {total} ({Phase name})
@@ -28,6 +28,8 @@ Phase 01 ──→ Phase 02 ──→ Phase 04
28
28
  **Goal:** {overall project goal from requirements}
29
29
  **Phases:** 1 - {n}
30
30
 
31
+ ## Phase Details
32
+
31
33
  ### Phase 01: Project Setup
32
34
 
33
35
  **Goal**: {goal statement}