@@ -30,7 +30,7 @@ command="${1:-all}"
3030while [ " $# " -gt 0 ]; do
3131 case " $1 " in
3232 --root) root=" $( cd " $2 " && pwd -P) " ; shift 2 ;;
33- all|agents|skills|runtime|config|hooks|smoke|release|closeout|checkpoint|playtest|bug-lifecycle|handoff-review|resume-contract) command=" $1 " ; shift ;;
33+ all|agents|skills|runtime|config|hooks|smoke|release|closeout|checkpoint|playtest|bug-lifecycle|handoff-review|resume-contract|install-safety|coexistence|smoke-headless ) command=" $1 " ; shift ;;
3434 * ) shift ;;
3535 esac
3636done
@@ -651,6 +651,111 @@ run_install_safety() {
651651 return $problems
652652}
653653
654+ run_coexistence () {
655+ printf ' \n── Coexistence / Installer Matrix ───────────────────────────\n'
656+ printf ' (advisory — runs a real install/uninstall matrix in a temp dir)\n'
657+ local inst=" $root /.opencode/install.sh"
658+ local uninst=" $root /.opencode/uninstall.sh"
659+ [ -f " $inst " ] && [ -f " $uninst " ] || { fail " install.sh/uninstall.sh not found" ; return 1; }
660+
661+ local model=" "
662+ if command -v opencode > /dev/null 2>&1 ; then model=" $( opencode models 2> /dev/null | head -1) "
663+ elif [ -x " $HOME /.opencode/bin/opencode" ]; then model=" $( " $HOME /.opencode/bin/opencode" models 2> /dev/null | head -1) " ; fi
664+ [ -n " $model " ] || model=" local/test"
665+ local MF=" --tier-opus $model --tier-sonnet $model --tier-haiku $model --primary $model "
666+
667+ local base T out rc
668+ base=" $( mktemp -d " ${TMPDIR:-/ tmp} /ccgs-coex.XXXXXX" ) " || { fail " could not create temp dir" ; return 1; }
669+
670+ # S1 fresh install
671+ T=" $base /s1" ; mkdir -p " $T "
672+ rc=0; out=" $( bash " $inst " $MF " $T " 2>&1 ) " || rc=$?
673+ if [ " $rc " -eq 0 ] && [ -f " $T /.opencode/install-state.json" ]; then pass " S1 fresh install + state" ; else fail " S1 fresh install (rc=$rc )" ; fi
674+
675+ # S2 unowned collision -> preflight abort, no mutation
676+ T=" $base /s2" ; mkdir -p " $T /.github" ; printf ' USER\n' > " $T /.github/CODEOWNERS"
677+ rc=0; out=" $( printf ' y\n' | bash " $inst " $MF " $T " 2>&1 ) " || rc=$?
678+ if [ " $rc " -ne 0 ] && printf ' %s' " $out " | grep -qi ' Preflight conflict' ; then pass " S2 unowned collision abort" ; else fail " S2 collision abort (rc=$rc )" ; fi
679+ if [ -f " $T /.opencode/install-state.json" ]; then fail " S2 mutated target despite abort" ; else pass " S2 no mutation" ; fi
680+
681+ # S3 modified-file -> preflight abort, then S4 --replace-modified
682+ T=" $base /s3" ; mkdir -p " $T "
683+ bash " $inst " $MF " $T " > /dev/null 2>&1 || true
684+ printf ' USER-EDIT\n' > " $T /.opencode/VERSION"
685+ rc=0; out=" $( bash " $inst " $MF " $T " 2>&1 ) " || rc=$?
686+ if [ " $rc " -ne 0 ] && printf ' %s' " $out " | grep -qi ' Locally-modified' ; then pass " S3 modified-file abort" ; else fail " S3 modified abort (rc=$rc )" ; fi
687+ rc=0; out=" $( bash " $inst " $MF --replace-modified " $T " 2>&1 ) " || rc=$?
688+ if [ " $rc " -eq 0 ] && [ " $( cat " $T /.opencode/VERSION" 2> /dev/null) " = " $( cat " $root /.opencode/VERSION" ) " ]; then pass " S4 --replace-modified restore" ; else fail " S4 replace-modified (rc=$rc )" ; fi
689+
690+ # S5 uninstall missing-state -> abort, no removal
691+ T=" $base /s5" ; mkdir -p " $T "
692+ bash " $inst " $MF " $T " > /dev/null 2>&1 || true
693+ local agents_before
694+ agents_before=" $( ls " $T /.opencode/agents" /* .md 2> /dev/null | wc -l | tr -d ' ' ) "
695+ rm -f " $T /.opencode/install-state.json"
696+ rc=0; out=" $( bash " $uninst " " $T " 2>&1 ) " || rc=$?
697+ if [ " $rc " -ne 0 ] && printf ' %s' " $out " | grep -qi ' cannot uninstall safely' ; then pass " S5 uninstall missing-state abort" ; else fail " S5 missing-state abort (rc=$rc )" ; fi
698+ if [ " $( ls " $T /.opencode/agents" /* .md 2> /dev/null | wc -l | tr -d ' ' ) " = " $agents_before " ]; then pass " S5 no files removed" ; else fail " S5 files removed despite abort" ; fi
699+
700+ # S6 uninstall valid-state -> clean removal
701+ T=" $base /s6" ; mkdir -p " $T "
702+ bash " $inst " $MF " $T " > /dev/null 2>&1 || true
703+ rc=0; out=" $( bash " $uninst " " $T " 2>&1 ) " || rc=$?
704+ if [ " $rc " -eq 0 ] && [ ! -f " $T /.opencode/install-state.json" ] && [ ! -f " $T /.opencode/agents/creative-director.md" ]; then pass " S6 uninstall valid-state clean" ; else fail " S6 valid-state uninstall (rc=$rc )" ; fi
705+
706+ # S7 transactional rollback on forced mid-deploy failure
707+ T=" $base /s7" ; mkdir -p " $T "
708+ bash " $inst " $MF " $T " > /dev/null 2>&1 || true
709+ printf ' MY-MOD\n' > " $T /.opencode/.gitignore"
710+ rm -f " $T /.opencode/README.md"
711+ if chmod 444 " $T /.opencode/uninstall.sh" 2> /dev/null; then
712+ local a_before a_after
713+ a_before=" $( cat " $T /.opencode/.gitignore" ) "
714+ rc=0; out=" $( bash " $inst " $MF --replace-modified " $T " 2>&1 ) " || rc=$?
715+ chmod 644 " $T /.opencode/uninstall.sh" 2> /dev/null || true
716+ a_after=" $( cat " $T /.opencode/.gitignore" 2> /dev/null) "
717+ if [ " $rc " -ne 0 ] && printf ' %s' " $out " | grep -qi ' rolling back' && [ " $a_after " = " $a_before " ] && [ ! -f " $T /.opencode/README.md" ]; then
718+ pass " S7 transactional rollback (restore + remove)"
719+ else
720+ fail " S7 rollback (rc=$rc )"
721+ fi
722+ else
723+ pass " S7 rollback: SKIP (chmod 444 unsupported)"
724+ fi
725+
726+ rm -rf " $base " 2> /dev/null || true
727+ return 0
728+ }
729+
730+ run_smoke_headless () {
731+ printf ' \n── Headless Smoke (non-model) ──────────────────────────────\n'
732+ printf ' (advisory — command/skill graph integrity; model-driven smoke deferred)\n'
733+ local problems=0 cmds=0 broken=0
734+
735+ # command -> skill graph integrity
736+ local c ref skill_path
737+ for c in " $root " /.opencode/commands/* .md; do
738+ [ -f " $c " ] || continue
739+ cmds=$(( cmds + 1 ))
740+ ref=" $( grep -oE ' @\.opencode/skills/[^/]+/SKILL\.md' " $c " 2> /dev/null | head -1) "
741+ if [ -z " $ref " ]; then
742+ fail " $( basename " $c " .md) : no @skill reference"
743+ broken=$(( broken + 1 )) ; problems=1
744+ continue
745+ fi
746+ skill_path=" ${ref#@ } "
747+ if [ ! -f " $root /$skill_path " ]; then
748+ fail " $( basename " $c " .md) : skill $ref missing"
749+ broken=$(( broken + 1 )) ; problems=1
750+ fi
751+ done
752+ [ " $broken " -eq 0 ] && pass " $cmds commands resolve to an existing skill"
753+
754+ # model-driven boot is intentionally deferred (needs model + API access)
755+ pass " model-driven boot: DEFERRED (wire explicitly when a CI model runner exists)"
756+ return $problems
757+ }
758+
654759case " $command " in
655760 all)
656761 run_agents
@@ -678,10 +783,12 @@ case "$command" in
678783 runtime) run_runtime ;;
679784 config) run_config ;;
680785 install-safety) run_install_safety ;;
786+ coexistence) run_coexistence ;;
787+ smoke-headless) run_smoke_headless ;;
681788 hooks) run_hooks ;;
682789 smoke) run_smoke ;;
683790 release) run_release ;;
684- * ) printf ' Unknown command: %s\nAvailable: all, agents, skills, closeout, checkpoint, playtest, bug-lifecycle, handoff-review, resume-contract, runtime, config, install-safety, hooks, smoke, release\n' " $command " >&2 ; exit 2 ;;
791+ * ) printf ' Unknown command: %s\nAvailable: all, agents, skills, closeout, checkpoint, playtest, bug-lifecycle, handoff-review, resume-contract, runtime, config, install-safety, coexistence, smoke-headless, hooks, smoke, release\n' " $command " >&2 ; exit 2 ;;
685792esac
686793
687794printf ' \n── Result: %d error(s) ──\n' " $errors "
0 commit comments