1111# closeout Check closeout-routing contract on completion skills
1212# checkpoint Check active.md silent-checkpoint contract on skills/agents
1313# playtest Check playtest-focus contract on root/continuity/skill surfaces
14+ # bug-lifecycle Check bug lifecycle consolidation contract on bug-report/triage
15+ # handoff-review Check handoff two-round review-gate contract on handoff/AGENTS.md
16+ # resume-contract Check resume lane-selection boundary on resume-from-handoff
1417# runtime Check for stale references
1518# config Validate opencode.json
1619# hooks Test hook scripts
@@ -27,7 +30,7 @@ command="${1:-all}"
2730while [ " $# " -gt 0 ]; do
2831 case " $1 " in
2932 --root) root=" $( cd " $2 " && pwd -P) " ; shift 2 ;;
30- all|agents|skills|runtime|config|hooks|smoke|release|closeout|checkpoint|playtest) command=" $1 " ; shift ;;
33+ all|agents|skills|runtime|config|hooks|smoke|release|closeout|checkpoint|playtest|bug-lifecycle|handoff-review|resume-contract ) command=" $1 " ; shift ;;
3134 * ) shift ;;
3235 esac
3336done
@@ -257,6 +260,208 @@ run_playtest_focus() {
257260 printf ' %d playtest-focus surfaces checked\n' " $checked "
258261}
259262
263+ run_bug_lifecycle () {
264+ printf ' \n── Bug Lifecycle Contract ────────────────────────────────\n'
265+ local checked=0
266+
267+ # --- bug-report required phrases ---
268+ local report=" $root /.opencode/skills/bug-report/SKILL.md"
269+ checked=$(( checked + 1 ))
270+ if [ ! -f " $report " ]; then fail " bug-report/SKILL.md (missing)" ; else
271+ local report_norm; report_norm=$( tr -s ' [:space:]' ' ' < " $report " )
272+ local -a req=(
273+ " treat verification, closure, stale triage"
274+ " one deterministic bug lifecycle operation"
275+ " Do not stop after VERIFIED FIXED to offer"
276+ " refresh stale triage metadata under the same approval"
277+ " zero-open-bugs refresh"
278+ " derived checkpoint"
279+ ' Do not ask a separate "May I write?" for'
280+ " Do not bundle and stop for user decision if triage would require assigning"
281+ )
282+ local missing=() p
283+ for p in " ${req[@]} " ; do
284+ grep -qiF " $p " <<< " $report_norm" 2> /dev/null || missing+=(" $p " )
285+ done
286+ # Forbidden old-language fragments
287+ local -a forbidden=(
288+ " is referenced in the triage report"
289+ " write the closure record and update status"
290+ " remove it from the active list"
291+ )
292+ local bad=()
293+ for p in " ${forbidden[@]} " ; do
294+ grep -qiF " $p " <<< " $report_norm" 2> /dev/null && bad+=(" $p " )
295+ done
296+ if [ " ${# missing[@]} " -eq 0 ] && [ " ${# bad[@]} " -eq 0 ]; then
297+ pass " bug-report/SKILL.md (lifecycle contract)"
298+ else
299+ local d=" "
300+ [ " ${# missing[@]} " -gt 0 ] && d+=" missing: ${missing[*]} "
301+ [ " ${# bad[@]} " -gt 0 ] && d+=" forbidden-present: ${bad[*]} "
302+ fail " bug-report/SKILL.md (lifecycle contract)$d "
303+ fi
304+ fi
305+
306+ # --- bug-triage required phrases ---
307+ local triage=" $root /.opencode/skills/bug-triage/SKILL.md"
308+ checked=$(( checked + 1 ))
309+ if [ ! -f " $triage " ]; then fail " bug-triage/SKILL.md (missing)" ; else
310+ local triage_norm; triage_norm=$( tr -s ' [:space:]' ' ' < " $triage " )
311+ local -a req2=(
312+ " zero-open-bugs closure refresh"
313+ " Treat it as metadata cleanup"
314+ " non-blocking follow-up"
315+ " Exception for bundled bug lifecycle cleanup"
316+ " deterministic metadata cleanup"
317+ " It must be explicitly marked non-blocking if it cannot be completed safely"
318+ " Do not bundle if the triage work would require assigning priorities"
319+ )
320+ local missing2=() p2
321+ for p2 in " ${req2[@]} " ; do
322+ grep -qiF " $p2 " <<< " $triage_norm" 2> /dev/null || missing2+=(" $p2 " )
323+ done
324+ if [ " ${# missing2[@]} " -eq 0 ]; then
325+ pass " bug-triage/SKILL.md (lifecycle contract)"
326+ else
327+ fail " bug-triage/SKILL.md missing: ${missing2[*]} "
328+ fi
329+ fi
330+ printf ' %d bug-lifecycle surfaces checked\n' " $checked "
331+ }
332+
333+ run_handoff_review () {
334+ printf ' \n── Handoff Review Gate Contract ──────────────────────────\n'
335+ local checked=0
336+
337+ # --- handoff SKILL.md required phrases ---
338+ local skill=" $root /.opencode/skills/handoff/SKILL.md"
339+ checked=$(( checked + 1 ))
340+ if [ ! -f " $skill " ]; then fail " handoff/SKILL.md (missing)" ; else
341+ local skill_norm; skill_norm=$( tr -s ' [:space:]' ' ' < " $skill " )
342+ local -a req=(
343+ " ## Round 1"
344+ " ## Round 2"
345+ " \` STANDARD\` "
346+ " \` ADVERSARIAL\` "
347+ " Foundation ADR cluster closure"
348+ " pure design/process-document"
349+ " self-review is sufficient and the native cross-check is skipped"
350+ " Mixed code-and-document changes are not exempt"
351+ " distinct native review pass"
352+ " current OpenCode session"
353+ " \` HIGH\` , \` MEDIUM\` , or \` LOW\` "
354+ " \` CLEAN\` "
355+ " \` path:line\` "
356+ " If uncertain whether the work meets a major trigger, use \` STANDARD\` "
357+ " quoted verbatim"
358+ " stop before Phase 1"
359+ " second native cross-check"
360+ " \` HIGH\` finding"
361+ " cross-cutting executable behavior"
362+ " Trivial and confidently intent-preserving only"
363+ " Any non-trivial fix"
364+ " Do not run a third pass"
365+ " three native review passes"
366+ " fourth native review pass"
367+ " active reported context percentage"
368+ " review audit trail"
369+ " every finding"
370+ " Only then proceed to Phase 1"
371+ )
372+ local missing=() p
373+ for p in " ${req[@]} " ; do
374+ grep -qF " $p " <<< " $skill_norm" 2> /dev/null || missing+=(" $p " )
375+ done
376+ if [ " ${# missing[@]} " -eq 0 ]; then
377+ pass " handoff/SKILL.md (review gate contract)"
378+ else
379+ fail " handoff/SKILL.md missing: ${missing[*]} "
380+ fi
381+ fi
382+
383+ # --- AGENTS.md required phrases ---
384+ local agents=" $root /AGENTS.md"
385+ checked=$(( checked + 1 ))
386+ if [ ! -f " $agents " ]; then fail " AGENTS.md (missing)" ; else
387+ local agents_norm; agents_norm=$( tr -s ' [:space:]' ' ' < " $agents " )
388+ local -a req2=(
389+ " files already created or materially modified during the session"
390+ " intent-preserving review fixes"
391+ " active OpenCode session"
392+ " Round-two non-trivial findings"
393+ " external data-egress approval"
394+ " new intent, architecture, game-feel, balance, or scope decisions"
395+ )
396+ local missing2=() p2
397+ for p2 in " ${req2[@]} " ; do
398+ grep -qF " $p2 " <<< " $agents_norm" 2> /dev/null || missing2+=(" $p2 " )
399+ done
400+ if [ " ${# missing2[@]} " -eq 0 ]; then
401+ pass " AGENTS.md (handoff review exception)"
402+ else
403+ fail " AGENTS.md missing: ${missing2[*]} "
404+ fi
405+ fi
406+ printf ' %d handoff-review surfaces checked\n' " $checked "
407+ }
408+
409+ run_resume_contract () {
410+ printf ' \n── Resume Lane-Selection Contract ────────────────────────\n'
411+ if python3 - " $root " << 'PY '
412+ import re, sys, os
413+ root = sys.argv[1]
414+ rel = ".opencode/skills/resume-from-handoff/SKILL.md"
415+ path = os.path.join(root, rel)
416+ if not os.path.isfile(path):
417+ print(f" ! {rel}: missing file")
418+ sys.exit(1)
419+ text = open(path, encoding="utf-8").read()
420+ norm = re.sub(r"\s+", " ", text)
421+
422+ required = (
423+ "A focus argument biases ranking; it does not select a lane.",
424+ "Never start an unselected lane.",
425+ "recommendation as the first option",
426+ "wait for the user to reply `1`",
427+ "Resume selection authorizes entering only the selected workflow",
428+ "FIRST verification cannot be waived by choosing another lane",
429+ "Follow-up fork",
430+ "`question` tool",
431+ "Playable/Slice State Source",
432+ "production/stage.txt",
433+ ".opencode/docs/workflow-catalog.yaml",
434+ "production/session-state/active.md",
435+ )
436+ missing = [p for p in required if p not in norm]
437+ if missing:
438+ print(f" ! {rel}: missing phrase(s): " + ", ".join(missing))
439+
440+ fails = len(missing)
441+ for i, line in enumerate(text.splitlines(), start=1):
442+ lower = line.lower()
443+ starts_lane = re.search(r"\b(?:start|begin|enter)\b", lower)
444+ bypasses = any(w in lower for w in (
445+ "automatically", "immediately", "without waiting", "without selection"
446+ ))
447+ forbidden = any(w in lower for w in (
448+ "do not", "don't", "never", "must not", "cannot"
449+ ))
450+ if starts_lane and bypasses and not forbidden:
451+ print(f" ! {rel}:{i}: automatic lane startup forbidden; "
452+ "pause for selection boundary")
453+ fails += 1
454+
455+ print(f" resume-from-handoff checked, {fails} violation(s)")
456+ sys.exit(1 if fails else 0)
457+ PY
458+ then
459+ pass " resume selection contract satisfied"
460+ else
461+ fail " resume selection contract violations"
462+ fi
463+ }
464+
260465run_runtime () {
261466 printf ' \n── Runtime References ──────────────────────────────────────\n'
262467 # Check for stale Claude references
@@ -404,6 +609,9 @@ case "$command" in
404609 run_closeout
405610 run_checkpoint
406611 run_playtest_focus
612+ run_bug_lifecycle
613+ run_handoff_review
614+ run_resume_contract
407615 run_runtime
408616 run_config
409617 run_hooks
@@ -414,12 +622,15 @@ case "$command" in
414622 closeout) run_closeout ;;
415623 checkpoint) run_checkpoint ;;
416624 playtest) run_playtest_focus ;;
625+ bug-lifecycle) run_bug_lifecycle ;;
626+ handoff-review) run_handoff_review ;;
627+ resume-contract) run_resume_contract ;;
417628 runtime) run_runtime ;;
418629 config) run_config ;;
419630 hooks) run_hooks ;;
420631 smoke) run_smoke ;;
421632 release) run_release ;;
422- * ) printf ' Unknown command: %s\nAvailable: all, agents, skills, closeout, checkpoint, playtest, runtime, config, hooks, smoke, release\n' " $command " >&2 ; exit 2 ;;
633+ * ) printf ' Unknown command: %s\nAvailable: all, agents, skills, closeout, checkpoint, playtest, bug-lifecycle, handoff-review, resume-contract, runtime, config, hooks, smoke, release\n' " $command " >&2 ; exit 2 ;;
423634esac
424635
425636printf ' \n── Result: %d error(s) ──\n' " $errors "
0 commit comments