diff --git a/.claude/backup_README_corrupted_20260504_233240.md b/.claude/backup_README_corrupted_20260504_233240.md new file mode 100644 index 0000000..937a886 --- /dev/null +++ b/.claude/backup_README_corrupted_20260504_233240.md @@ -0,0 +1,808 @@ +# Claude Code Python + +濞戞挴鍋撳☉鎿冧邯濞间即宕ラ幋婊冩暕闁活喕绀佹导鎰媴濠婂懏鐣?AI CLI / REPL / MCP 濡炪倕婀卞ú浼存晬鐏炴儳绲瑰〒?Claude Code 濡炲瀛╅悧鎼佹儍閸曨亝鍞夊ù婊勫竾閳ь兛绀佹导鎰板礂閻ゎ垳娈堕柣顫妸閳ь兛妞掗幑銏ゅ礉闄囩换宥囨偘鐏炵偓顦ч柕鍡曠gent闁靛棔绀侀ˇ?Provider 闁规亽鍎遍崣鍡涙晬鐏為棿绨伴柛娆忥梗缁斿瓨绺藉Δ浣哥槷缂備緡鍘界槐銊︽交濞戞鍘柣銊ュ娴兼劗绮欑€n亜顕ч柛鈺勬椤㈠懐鎷嬮悙顒佺參闁? + +闁?README 闁哄嫷鍨扮紞瀣礈瀹ュ嫮娉㈤幖瀛樻尵濞堟垹绱掗悢鍓侇伇闁稿繈鍎辫ぐ娑㈠Υ閸屾哎浜eù锝嗘崌閸g儤绋夐幘娲绘毌闁哄倸娲﹂妴鍌滅磼瑜忛悽缁樼┍濠靛牊娈岄柛?`docs/`闁挎稑鐭佺换鏍煂鐏炶棄娑уǎ鍥ㄧ箘閺嗏偓闊洤鎳撻々锕傚箺濡娲i柕鍡曠劍濡叉垹娑甸纰卞殼闁煎壊浜滈幏浼村触鎼达絿鏁剧紓浣哥摠婵垻鐥敃鈧悾楣冩晬瀹€鍕級闁稿繐绉甸弸鍐浖閿濆懎鏅欐繛鍡忊偓鐐戒杭闁硅矇浣插亾? + +## 濡炪倕婀卞ú鎵偓瑙勭煯缂? + +- 閻犲浂鍙€閳诲牊绋夋惔銈囩閻炴稑鏈鍌炴晬濮濆巸thon 3.10+闁靛棔姊梐syncio` +- 濞戞捁顕ч崣鍡涘矗閿濆繒绐梎python -m claude_code.main` 闁?`python run.py` +- 濞存嚎鍊撶花鏉课熼垾宕囩闁挎稒鐭锔界閹烘垹纭€ REPL闁靛棔绀佸畷鐔封枎?query闁靛棔鑳堕鎼佹焼閹捐尐浣割嚕韫囧鍋撴稉鐡筆 闁哄牆绉存慨鐔煎闯閵娢翠礁顕? +- 婵☆垪鈧磭鈧兘骞掗妷銉ュ汲闁挎稒鐡猘nthropic`闁靛棔姊梠penai`闁靛棔姊梠llama`闁靛棔姊梫llm`闁靛棔姊梔eepseek`闁靛棔姊梑edrock`闁靛棔姊梫ertex` +- 闁稿繑濞婇弫顓犫偓娑欏姉闁绱掗悤鍌滅獥`QueryEngine`闁靛棔姊桾oolRegistry`闁靛棔鐒﹀鍫ユ⒔閹邦噣鍏囩紓浣哄枂閳ь兛妞掗幑銏ゅ礉闄囩换宥囨偘鐏炵偓顦ч柕鍡曠gent闁靛棔搴淐P闁靛棔鐒︽俊褔鎳楅悾宀勫厙缂備胶鍠嗛埀顑跨劍濠€鍥礉閳ュ磭婀? + +鐟滅増鎸告晶鐘崇閿濆洨鍨冲☉?`azure` 閻犱警鍨扮欢鐐哄嫉婢跺鈻旂€?fail-fast 闁圭粯鍔楅妵姘舵晬鐏炶姤绀堟慨婵勫€涚换鏍煂鐏炶偐鐟濋柟璺猴工閻g姵鎷呭鈧拹鐔汇亹閹惧啿顤呴柟鎭掑姀瀹曟ɑ娼婚幇顖f斀閻犱警鍨扮欢鐐哄Υ? + +## 鐟滅増鎸告晶鐘诲极鐎靛憡鍊炵紓浣规崄椤? + +闁规惌浜i崵?`2026-04-17`闁挎稑鏈﹢鐗堢閹惧磭姘ㄩ柣?Markdown 闁哄倸娲﹂妴鍌炲矗椤栨瑤绨伴柡鈧懜鍨異濞戞捁妗ㄦ禍鎺撶▔鐎n剛娉㈤柡瀣缁? + +1. `README.md` 闁告瑯浜g粈瀣嫻閿濆洨鍩犲☉鎾亾闁稿繈鍎辫ぐ娑㈡晬鐏炶偐鐟濋柛鎰Т閻栥垽鎯嶅畝鍐㈤梻鈧幐搴㈡畬缂佸顑呴幏鎵偓鍦仧楠炲洭寮稿顐㈩洭闁? +2. 闁炽儲绮岀紞瀣礈瀹ュ洤笑闁诡兛璁查埀顒佺箑缁鳖參宕楅崼銏$畽閻㈩垽闄勫Λ鈺呭嫉閻旂儤鐣遍悹鍥у閸?闁圭瑳鍡╂斀闁哄倸娲﹂妴鍌炴晬瀹€鍐e亾鐏炶偐鐟濋柡鍕靛灡濞插潡寮埡鍐╃暠閻犲洤瀚鎼佹嚒婢跺矈鐒鹃柕? +3. `docs/current/reference/AGENT.MD` 濞?`docs/current/reference/MCP.md` 濞达絾鎹囬崳鐑樻綇閸愩劊浜i柨娑樺缁绘岸鎮惧▎鎴濐仾缂佹柨顑嗛弸鍐浖閿濆繒绀夊☉鎾崇Т閸熲偓闂佹彃绉撮ˇ鏌ュ箹椤掑啰绠?README闁? +4. 闁哄鍩栭悗顖炲Υ娴e厜鍋撹閸忔﹢濡存担鐤幀闂傚倻绻濆▎銏$▔婢跺钂嬬紒鎯х仢閸ㄥ酣宕氶锛勭闁伙絾鐟㈤埀顒佺矊缂嶅宕滃鍥╂尝閻犱浇銆€閳ь剚绻€缁楀矂鍨惧鍐ㄥ潑闁告瑨灏欑划蹇涙嚍閸屾せ鍋撳┑鍛扳拡閻忕偛鍋婄槐婵囩▔瀹ュ懎鏅欐鐐舵硾閸亪宕奸崱妤€缍屽鑸电煯闁叉粓宕ュ畝鈧鍥礂閵夈儱缍撻柕? + +闁哄牜鍓氶鍏硷紣濠靛棭妯嗛柡宥囶焾椤曨喚绱掗幘瀵镐函闁? + +- 婵炴潙顑堥惁顖涘緞瀹ュ棛澹嬮柨娑欑摢pytest -q -c pytest.ini` 闂侇偅淇虹换鍐晬瀹€鈧划銊╁几濠娾偓鐠?`90 passed` +- 闁哄鍩栭悗顖炲嫉閳ь剟寮弶鎸庡渐闁绘挆宥囩獥`2026-04-16` 闁汇劌瀚惁搴∶圭€n剛娉㈤悹浣芥〃鐠愮喖鍨惧鍐х矗缂佸顑呰ぐ鏌ユ偨閵娿儳鍞ㄧ紒鎯х仢瑜版煡骞掗妸銊х闁炽儲绻愮槐婵堟嫚閸曨偄鐎?`8.4 / 10` +- 濞戞搩鍙冨Λ鎸庣閼哥數宸濋弶鈺傜☉閹烩晠鎮¤缁辩櫗Phase 0` 鐎瑰憡褰冮悾顒勫箣閹板墎绀塦Phase 1` 闁归晲鑳堕悽濠氬箳閵娿劎绠? +- 闁诡儸鍡楀幋闊浂鍋嗛崣搴ㄦ晬濮濇aseline V1` 閻犱焦婢樼紞宥夊触椤栨艾袟闁肩増顨嗗鍌滅棯?`1109.38 ms` + +## 闊浂鍋婇埀顒傚枎缁辨垶鎱? + +### 1. 閻庣懓顦抽ˉ濠冪瑹濠靛﹦顩? + +```bash +python -m pip install -r requirements.txt +``` + +濠碘€冲€归悘澶嬫媴閻樻彃娑ч柡鍕靛灟缁姴鈹冮幇顔惧灣閺夆晜鍔橀、鎴︽晬鐏炵偓鎷卞ù鐘虫尭缁ㄨ精銇愰幘鍐差枀鐎点倝缂氶鍛?`requirements.txt` 闁?`pytest.ini` 濞戞挸鎼崳顖炲Υ? + +### 2. 闂侇偄顦扮€?Provider + +#### Anthropic + +```bash +export CLAUDE_API_PROVIDER=anthropic +export ANTHROPIC_API_KEY=your-key +export CLAUDE_MODEL=claude-sonnet-4-20250514 +``` + +#### Ollama / 闁哄牜鍓欏﹢?OpenAI 闁稿繒鍘ч鎰熼垾宕団偓? + +```bash +export CLAUDE_API_PROVIDER=ollama +export OPENAI_BASE_URL=http://localhost:11434/v1 +export CLAUDE_MODEL=qwen2.5:14b +``` + +閻犲洤鐡ㄥΣ鎴︽晬? + +- `openai` / `deepseek` 闂傚洠鍋撻悷?`OPENAI_API_KEY` +- `ollama` / `vllm` 闁稿繋娴囬蹇涘嫉椤掆偓濠€鏉懳熼垾宕団偓椋庢崉椤栨氨绐為柨娑樺缁楀顕i崫鍕厬闁活亞鍠庨悿?API Key +- PowerShell 閻犲洤鍢查惃?`export` 闁衡偓闁稖绀?`$env:闁告瑦锕㈤崳娲触?"闁?` + +### Windows 闁规亽鍔忓畷姗€宕ラ姘楅柨娑樼墦娴尖晠宕?WindowsApps stub闁? + +Windows 濞戞挸锕g粭澶屾啺娴i璐╅悹?`C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe` 閺夆晜鐟х悮?Store alias stub闁? + +鐎点倝缂氶鍛濡搫甯ュù锝堟硶閺併倝鏁? + +```powershell +py -3 -m claude_code.main --doctor +py -3 -m claude_code.main +``` + +闁瑰瓨鐗楀Ο澶婎嚕韫囧骸鈻忛柣顫姀濞呭嫰骞忛悢鍝勭畾濠⒀冨暢琚欓梺鎻掞工濞呮帡鏁? + +```powershell +.\.venv\Scripts\python.exe -m claude_code.main --doctor +.\.venv\Scripts\python.exe -m claude_code.main +``` + +`--doctor` 濞村吋淇虹欢顓㈠礄妤﹀晝鎺楁煂婵犲倹鐝ら柡澶堝劜缁喗绋夋惔顭戞闂傚嫧鏅滆ぐ浣虹矆閻氬骞㈤柤姹囧劚閹斥剝绋?WindowsApps stub闁挎稑濂旂槐鎵磼濞嗗繐姣夐柡鍕捣閳ユ鎷冮敃鈧幉鈩冪▔鎼淬垺绂屽ù鐙呯到閹酣宕濋妸銉﹀殥濞寸姰鍊戦埀? + +### 3. 閻㈩垰鎽滈弫銈夊触椤栨艾袟闁哄倻鎳撶槐? + +```bash +# 闁哄被鍎冲﹢鍛暜椤旂厧袠 +python -m claude_code.main --help + +# 闁绘鐗婂﹢? +python -m claude_code.main --version + +# 闁稿鍎遍幃宥呂涢埀顒勫蓟? +python -m claude_code.main --doctor + +# 濞存嚎鍊撶花鏉款嚕?REPL +python -m claude_code.main + +# 闁告娲橀濂稿箵閹扮増锛? +python -m claude_code.main "閻熸瑱缍侀崳纾嬨亹閹惧啿顤呴柣鈺婂枛缂嶅秵绋夌€n剚鐣卞銈呮贡濞叉壆绱掗幘瀵糕偓? + +# 缂佺媴绻濇禍鎯熼垾宕囩 +echo "閻㈩垼鍠楅崹婊堝箑閼姐倗娉㈤弶鈺傜懄椤斿本绂掗敐鍥╁灣" | python -m claude_code.main --pipe + +# 濞达絾绮堢拹?MCP 闁哄牆绉存慨鐔煎闯閵娿儲鍎欓柛? +python -m claude_code.main --mcp-serve + +# 闁告凹鍨版慨鈺呭嫉椤掆偓濠€?daemon / API 闁硅矇鍐ㄧ厬闂傚牜婢佺槐姗?-01闁? +python -m claude_code.main --daemon-serve --daemon-host 127.0.0.1 --daemon-port 8787 + +# CLI 濞?thin-client 閻?daemon闁挎稑婀?-06闁? +python -m claude_code.main --daemon-client "explain current repo status" +echo "summarize changed files" | python -m claude_code.main --pipe --daemon-client +``` + +## 闁哄秶顭堢缓楣冩嚄閽樺顫旀慨鎺戝€介~? + +- 濞存嚎鍊撶花鎵沪閸岋妇绐桼EPL闁靛棔绀佸畷鐔告姜?query闁靛棔鑳堕鎼佹焼閹捐尐浣割嚕韫囧鍋撴担鍛婂殥濞寸姰鍊楅柈瀵哥磼? +- 鐎规悶鍎遍崣璺ㄤ沪閸岋妇绐楅柡鍌氭矗濞嗐垻鎷犵拠鎻掓櫢闁靛棔鐒﹂幃宕囨椤兘鍋撴稉绯積ll/PowerShell闁靛棔绀佹导鎰媴濠婂嫮銈﹂柕鍡曟閹广垽宕濋埄鍐ㄤ粯闁告帟缈伴埀顑跨劍婵⊙囨嚄婵劏鍋撴担鍦偦閻熸瑥鐗嗗▍鎺楀Υ娑撱彃P闁靛棔搴淐P闁靛棔绠榦rktree闁靛棔绔紃on闁靛棔绠慹am +- 閺夆晜鍔橀、鎴﹀籍鐠佸湱绐梎QueryEngine` 缂傚倹鐗楃敮鎾澄熼垾宕団偓椋庘偓鐢殿攰閻︿粙濡存担闀愮矗闁稿繒鏌夐惃鐔兼偨閵婏絺鍋撴担瑙勭秬闂傚嫭鍔曢崹鐣屸偓瑙勮壘閹风増绋夋繝浣虹憮闁哄倸娲﹂悗顖氼嚈? +- 濞寸姾顕ф慨鐔告媴閹惧爼鍏囬柨娑欑摢TaskManager`闁靛棔姊桾askRepository`闁靛棔姊桾askQueue` 闁衡偓椤栨稒瀚奸柛姘瑜板瓨绂掔拠鎻掝潳闁告粌濂旈幑銏ゅ礉閳ュ磭鎽犻柛灞诲妽缁便劍娼? +- 闁哄牆绉存慨鐔轰沪閸岋妇绐楃紓鍌涙尭閻°劑濡存笟鈧埀顒傚枔瀹稿ジ姊介幇顒€鐓戦柕鍡曠窔娴犳潙霉鐎c劉鍋撴担绋垮潑闁告瑨灏欓鎼佹偠閸℃劏鍋撴担绛嬪敹闊洤妫庨埀顑跨婵偟鈧潧妫庨埀顑垮紦ooks闁靛棔澶焗utdown闁靛棔杈渙ken estimation +- 闁圭鏅涢惈宥夋嚄閽樺顫旈柨娑欑搲CP server/client闁靛棔绔竒ent闁靛棔鐒︽俊褔鎳楅悾灞剧獥鐟滅増娲忛埀顑跨窔閵嗗秹鎯勯楣冪崜 `.claude/` 閺夆晜鍔橀、鎴﹀极閻楀牆绁? + +濠碘€冲€归悘澶嬫媴閻樹警娲i柣顏勵儏閻n剟寮弶鎴滅矗闁稿繘鏀辩粩濠氬础閺囶亞绀夊☉鎾崇Т閸熲偓濞?README 闁哄被鍎叉竟姗€鏁嶅畝鈧ú鍧楀箳閵壯勭畽 `docs/current/reference/MCP.md`闁? + +## 濞寸姵鎸哥花杈╃磼閹惧鈧? + +```text +claude-code-python/ +闁宠澹曢弨銏ゅ煘閳?claude_code/ # 濞戞捁顔婇崬顒勬儘? +闁? 闁宠澹曢弨銏ゅ煘閳?main.py # CLI 闁稿繈鍎辫ぐ? +闁? 闁宠澹曢弨銏ゅ煘閳?engine/ # QueryEngine / context / session / permissions +闁? 闁宠澹曢弨銏ゅ煘閳?tools/ # 鐎规悶鍎遍崣璺ㄥ寲閼姐倗鍩? +闁? 闁宠澹曢弨銏ゅ煘閳?commands/ # 闁告稒鍨濋幎銈囧寲閼姐倗鍩? +闁? 闁宠澹曢弨銏ゅ煘閳?tasks/ # 濞寸姾顕ф慨鐔告交閹邦垼鏀介柡鍐堪閳ь兛妞掔划銊╁磼閵婏絺鍋撴笟鈧Σ锕傚礆? +闁? 闁宠澹曢弨銏ゅ煘閳?services/ # 缂傚倹鎸搁悺銊╁Υ娓氣偓娴犳潙霉鐎c劉鍋撴担绋垮潑闁告瑥鐪伴埀顑垮紦ooks闁靛棔澶焗utdown 缂? +闁? 闁宠澹曢弨銏ゅ煘閳?mcp/ # MCP 闁哄牆绉存慨鐔虹博椤栨氨鏉介柣? +闁? 闁宠澹曢弨銏ゅ煘閳?agents/ # 闁告劕鎳愰悿?Agent 閻庤鐭粻? +闁? 闁宠澹曢弨銏ゅ煘閳?skills/ # 闁瑰灈鍋撻柤宕囨櫕闁绱? +闁? 闁宠鏌¢弨銏ゅ煘閳?state/ # 闁绘鍩栭埀顑跨劍鑶╅柛? +闁宠澹曢弨銏ゅ煘閳?docs/ # 濞戞挻鎹囬。浠嬪棘閸ャ劊鈧? +闁宠澹曢弨銏ゅ煘閳?examples/ # 缂佲偓鏉炴壆浼愰柤瀛樼濠€? +闁宠澹曢弨銏ゅ煘閳?scripts/ # 閺夊牆鎳庢慨顏堟嚇濮橆厽鎷卞☉鎾冲閳ь儸鍡楀幋闁煎瓨纰嶅﹢? +闁宠澹曢弨銏ゅ煘閳?tests/ # 婵炴潙顑堥惁? +闁宠澹曢弨銏ゅ煘閳?run.py # 婵犙勫姉閻栨粍娼婚幇顖f斀闁稿繈鍎辫ぐ? +闁宠澹曢弨銏ゅ煘閳?requirements.txt # 濞撴碍绻嗙粋鍡椼€掗崨顓炵 +闁宠鏌¢弨銏ゅ煘閳?pytest.ini # pytest 闂佹澘绉堕悿? +``` + +## 闁哄倸娲﹂妴鍌炲捶閺夋寧绂? + +濞戞挸顑夊浼村及椤栨稒娈婚柣鐐叉閹鎯冮崟顒€鑵归柤鑺ュ姍濡插嫮鎷犳繝姘モ偓搴㈡償韫囧鍋? + +| 闁哄倸娲﹂妴?| 閻熸瑦甯熸竟?| 濞寸姭鍋撳☉鏂跨墛濡炲倿宕愬▎鎴炵畽 | 鐟滅増鎸告晶鐘碘偓瑙勭煯缂?| +|---|---|---|---| +| `README.md` | 闁诡剝顕ч崣鍡涘矗?| 缂佹鍏涚粩鏉戔枎闄囩换姗€宕楅妷銈囨尝閹?| 闁哥儐鍨粩瀵糕偓浣冨閸╁懘宕楅妷銉ョ稉 | +| `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 闁哄鍩栭悗顖炴偝閹殿喖笑闊浂鍋嗛崣?| 闁诡垰纾悡锟犳焼閹炬眹鈧秹鎯勯鎯х疀闁革负鍔嶉弳锝嗘媴閹捐泛笑闁?| 闁哄鍩栭悗顖涚▔鐠囨彃寮抽柛?| +| `docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` | 濞存籂鍐╂儌闁瑰瓨顭囬弳鎰▔鎼淬倗孝閺夌儑绠掗惌鍓х棯閸喗绂?| 闁诡垰纾悡锟犳焼閹炬眹鈧秹鎯勯纰辨搐濞达絾娲戠划鐘诲灳濠婂啫璁查柣顫妼缁ㄦ娊鎯勫Ο灏佸亾濠靛﹨娉查柛鎺撳閳ь剚绮忕粔瀵告惥?Claude Code闁?| 闁瑰瓨顭囬弳鎰▔鐠囨彃寮抽柛?| +| `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` | 闁圭瑳鍡╂斀閻犱讲鈧啿鐏婂☉鎾崇凹閹广垽宕濋垾鍐插耿闁?| 闁诡垰纾ú鍧楀箳閵夈儳纾荤€规悶鍎遍懟鐔煎及鎼达絺鈧ê袙韫囨挾鐐婇柛妤嬬磿濞堟垿鎯嶉弬鍨岛闁肩厧鍟ú鍧楀Υ娴e摜鏆氶柟瀛樺姈閻栵綁宕欓崱鎰ㄥ亾娴e湱銈撮悹鍥ㄦ礃缁讳焦鎯?| 闁圭瑳鍡╂斀濞戞捁顕ч崣鍡涘矗?| +| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 濞寸姾顕ф慨?濞戞搩鍙冨Λ鎸庣閹増鍎伴柛锔藉椤斿洩銇?| 闁诡垰纾悡锟犳焼閹惧弶宕查柛鏃撶磿闁绱掗悢宄板殥闁衡偓閻熸澘鐓傞柛婵愪簷缁旀潙顫?| 濞戞搩鍙冨Λ鎸庣閸撲礁绠涢柣妯绘构鐎靛矂宕楅妷銉ョ稉 | +| `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 闁诡儸鍡楀幋濞戞挴鍋撳銈囨暩閻?| 闁诡垰鍟块幓鈺呮焻閻旂儤绠欓柟顑棗鍘撮柣妯垮煐閳?| 闁诡儸鍡楀幋濞戞捁顕ч崣鍡涘矗?| +| `docs/current/reference/MCP.md` | MCP 濞达綀娉曢弫銈夊箥鐎n亜鏂€ | 闁诡垵娅曟俊鍛婄閹惧磭姘ㄥù锝嗙矆鐠?MCP server 闁规亽鍎遍崣?| MCP 濞戞捁顕ч崣鍡涘矗?| +| `docs/current/reference/AGENT.MD` | Agent 闁稿繈鍔戦崳娲箥鐎n亜鏂€ | 闁诡垰纾幃濠勬喆?Agent闁靛棔绀佺槐鎾愁潰閵夈儳鎽嶅ù鐘侯嚙婵喖濡存担鍓叉▼闁哄懘缂氶崗妯绘媴?| Agent 濞戞捁顕ч崣鍡涘矗?| +| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 濡絾鐗為悿鍡涙⒒椤曗偓椤d粙鎯勫Ο鐓庝化 | 闁诡垰鐤囬幏宄扳攦椤栨稒浠橀柛鎺撶箓瑜板倿鎮虫0浣哄晩闁告繍浜欑花娲⒒椤曗偓椤?| 闁哄鍩栭悗顖炲储閸℃钑夐柡鍌氭处閵?| +| `docs/history/architecture/FIX_PROPOSALS.md` | 濞e浂鍠栭ˇ鏌ュ棘鐟欏嫷鏀抽幖?| 闁诡垰纾﹢鍛村储閸℃钑夊ǎ鍥跺枛椤﹁尙鎷嬮幑鎰靛悁濞戞挸楠搁ˇ顒勬焻婢跺鐓欐俊?| 闁哄鍩栭悗顖炲储閸℃钑夐柡鍌氭处閵?| +| `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 濞戞搩鍙冨Λ鎸庣閼哥數宸濋弶鈺傜閻﹀孩瀵?| 闁诡垰鐤囬惁搴㈠?Redis/MySQL/妤犵偛鍟胯ぐ鎾礌閺嶃劍笑闁告熬闄勫﹢浣界疀閸涢偊娲?| 濞戞搩鍙冨Λ鎸庣鐠哄搫鏋€缂佹稒鐗楅弸鍐浖?| +| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 闁诡儸鍡楀幋閻犱警鍨抽崵搴ㄥ炊?| 闁诡垰纾﹢鍛存⒓閼告鍞介柣鈺婂枟閻栵綁宕仦鎯р挃閻炴稑鐭侀鎼佸礆?| 闁诡儸鍡楀幋閻犱警鍨抽崵搴ㄥ炊?| +| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 闁诡儸鍡楀幋濞村吋锚鐎垫煡寮版惔锝囩煄 | 闁诡垰鐤囬幏鐑界叒椤忓嫬寰斿ù锝嗘尰閺佸吋绂嶉崱妤佹喛濞存粍绋撻崕鍦崉椤栨氨绐?| 闁诡儸鍡楀幋缂備礁妫滄俊顓㈠棘閸ャ劊鈧?| +| `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 闁诡儸鍡楀幋閺夆晜绋戠€瑰磭鎷嬮弶璺ㄧЭ | 闁诡垰纾﹢鍛存⒓閼告鍞介柟顑棛绠婚悘?| 闁诡儸鍡楀幋闁告ê妫楄ぐ鍫曞棘閸ャ劊鈧?| +| `docs/current/performance/BASELINE_V1.md` | 闁诡儸鍡楀幋闁糕晞娅i崵搴ㄥ箺濡娲?| 闁诡垰纾﹢鍛村春閾忕懓娈犵紓浣规崄椤?| 闁诡儸鍡楀幋闁轰胶澧楀畵渚€骞楀Ο娆炬矗 | +| `docs/current/performance/baseline_v1.json` | 闁告鍠庨~鎰板春閾忕懓娈犻柡浣哄瀹?| 闁诡垰鍟挎禒娑氣偓浣冾潐閻︻噣骞嬮弽顒€娈伴柛鏂诲妼鐎靛弶寰勯崟顓熷€?| 闁诡儸鍡楀幋闁告鍠庨~鎰板极閻楀牆绁?| + +## 闁告艾瀚€靛本锛愬Ο鍏肩暠闁哄鍟埢澶愬礂閵夈儱缍? + +濞戞挻妞芥导鈺呭礂瀹ヤ讲鍋撳鍐╁€卞☉鎾亾濞戞挸顭烽。鑺ュ緞濮橆偊鍤嬮柛蹇嬪劚瑜版盯宕ョ仦鐐槯闁哄牆顦伴弲銉╁灳濠垫挾绀夐柛姘捣閻㈣崵绱掗悢鍓侇伇闁圭顦粭鍛存閵忋垺鐣卞☉鎾诡嚙閸欏棝宕i敐鍥ㄥ€為悷娆欑秶缁? + +- 闁哄鍩栭悗顖炴偝閹殿喖笑闁挎稒鐡猟ocs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` +- 濞存籂鍐╂儌闁瑰瓨顭囬弳?/ 閻℃帒鎳撶粔?Claude Code 閻犱警鍨抽崵搴ㄥ炊閹惧懐绐梎docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` +- 闁圭瑳鍡╂斀閻犱讲鈧啿鐏?/ 濞寸姾顕ф慨鐔煎础閳ュ啿绀嬮柨娑欑摢docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 闁哄鍩栭悗顖炲储閸℃钑夐梻鍌ゅ櫍椤h姤绋夋惔婵囧弿濠㈣泛绉撮ˇ顒勬焻婢舵稓绐梎docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md` +- 濞寸姾顕ф慨?濞戞搩鍙冨Λ鎸庣鐠鸿櫣绉奸柛鎾崇Х閹倝宕烽幍顔夹﹂柟顑跨筏缁辩櫗docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` +- 濞寸姾顕ф慨?濞戞搩鍙冨Λ鎸庣閼哥數宸濋弶鈺傜☉閸犲懐绮甸弽锔捐穿闁硅鍣槐鐧璬ocs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` +- 闁诡儸鍡楀幋鐟滅増鎸告晶鐘绘偐閼哥鍋撴笟濠勭獥`docs/current/performance/PERFORMANCE_ONE_PAGER.md` +- 闁诡儸鍡楀幋闁糕晞娅i崵搴ㄥ极閻楀牆绁﹂柨娑欑摢docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json` +- MCP 濞达綀娉曢弫銈夋晬濮濇瓰ocs/current/reference/MCP.md` +- Agent 濞达綀娉曢弫銈夋晬濮濇瓰ocs/current/reference/AGENT.MD` + +## 闁哄鍩栭悗顖炲Υ娴e厜鍋撹閸忔﹢濡存担鐤幀闂傚倻绻濆▎銏ゆ儍閸曨剚鍠呴悷? + +### 闁哄鍩栭悗? + +缂備胶鍘ч幃?`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 濞戞挸楠稿濠氬矗閼煎墎妲戦悗鍏夊墲閺嬪啫顩奸敐蹇曠闁烩晩鍠栨晶鐘诲矗椤栨瑤绨伴悘蹇撴閻忥箓寮搁崟顓犳尝閻犱胶鍎ら弫褰掑极濞戞绀嬮柨? + +- 闁哄倻鎳撻幃婊勭▔婵犲啯笑闁告艾鐗忛幃濠囨儍閸曨儫渚€宕稿Δ鈧€垫煡宕¢弴姘辩Ъ +- 闁稿繑濞婇弫顓熺▔婵犳碍鎳犻悹渚灡濞存﹢宕欓搹鐟扮疀閺夆晛娲ゆ导鎰板礂闁垮鐦归梻鍥х湴閳ь兛鐒﹀鍫ユ⒔閹邦喒鈧牠姊婚幖鐐╁亾娑撳埀ent 鐎殿喖鍊归鐐哄Υ娑撶ovider 闂侇偄鍊块崢銈囩驳婢跺鐒介柣? +- 闁规惌浜i崵?`2026-04-16` 闁汇劌瀚惁搴∶圭€n偅鐎俊妤嬬秶缁辨繃娼诲▎搴f槀闁稿繑濞婇弫顓㈠棘椤撶姴浠€瑰憡褰冮悾顒勫箣閹邦亞顏遍弶鐑嗗櫍濡挳鎮抽娆愬弿濠㈣泛绋勭槐婵喢圭€n厾妲搁悹浣规緲缂嶅秵绋?`68 passed` +- 闁哄洤鐡ㄥΛ顓㈡儍?`docs/history/architecture/ARCHITECTURE_REVIEW.md` 闁?`docs/history/architecture/FIX_PROPOSALS.md` 濞戞挻妲掗々锕傛偨閵娿倗鑹鹃弶鈺勫Г閸戜粙鍨惧鈧拹鐔哥閳ь剚绋婇崼婊呯獥閺夆晜鐟ょ粻鐐哄绩閸忓鍋撳┑鎾剁濞戞挸绉撮崯鈧ù鐙呯秬閵嗗啴寮甸埀顒勫棘閹殿喖笑闁? + +### 濞戞搩鍙冨Λ鎸庣?/ 濞寸姾顕ф慨鐔哄寲閼姐倗鍩? + +缂備胶鍘ч幃搴㈢▔閵堝嫬鏁滃☉鎿冨弮濡寧绂掗懜鍨€俊妤嬬秶缁辨繆銇愰幘鍐差枀缂備焦鎹侀鎴﹀及椤栥倗绐? + +- 濞寸姵鎸哥花閬嶅储閻斿娼楁俊顖椻偓宕団偓鐑藉及椤栨せ鍋撳鍡欑缂佸顑呴崬鎾⒓閻斿嘲鐏?+ 闁哄倸娲e▎銏ゅ箰娴i鐣介柛鏍ㄧ墤閳ь剚绻愮槐婵囩▔瀹ュ棙笑闁轰胶澧楀畵浣规償閹剧粯袝闁告帗顨嗛悘锕傚几? +- 閻庣數鎳撳畷鐔煎嫉?CLI 闁告艾鐗忛幃濠囨晬鐏炶偐绋婚悗鐢垫嚀闁解晠宕i弶鍨嚙闁靛棔绀侀ˇ璺ㄢ偓鍦仒缁躲儵濡存担绋胯闁诡厹鍨归ˇ鏌ュ捶閻戞ɑ鐝瀣叄濞呮挻娼忛崘顔惧蒋 +- 鐟滅増鎸告晶鐘绘媰閽樺鍕鹃梺鎻掔Ф閸嬶絽顔忛煫顓犵煠闁炽儲绮嶅Σ鎼佸触閿斿墽褰岄柛鎺曨唺缁?Redis/MySQL闁炽儲绻嗗ù鍡樼▔鐞涒檧鍋撳鍐ㄥ弗闁硅泛锕i幑銏ゅ礉閳╁叀渚€宕圭€c劉鍋撴担椋庢尝闁稿被鍔婇埀顑跨窔濡诧箓宕氬Δ浣糕枙閻犵偐鍓濋弫褰掑极濞戞巻鍋? +- 鐎瑰憡褰冮悾顒勫箣閹邦喗鐣辩€规悶鍎扮紞鏃堝礌閸涱喖顏柨娑欑摢tasks.json` schema 缂備胶鍠嶇粩鎾Υ娴h鐎ù鐘茬埣閺€锝夊Υ娴e摜鏆旈柛蹇嬪妼閸熸捇宕楅妷锝傚亾娑斺暠rable 閻犲浂鍘虹粻鐔哥┍椤旂⒈妲婚柕鍡曟TaskRepository` / `TaskQueue` 闁硅泛鈧喕鏉介柟鎭掑劚閸? + +### 闁诡儸鍡楀幋 + +缂備胶鍘ч幃搴ㄥ箑瑜戦崗姗€寮崶銊b偓鍌炴晬鐏炵晫绉奸柛鎾崇Ф缁劎鎷嬮悜妯恍﹂柨? + +- 鐎瑰憡褰冪紓鎾剁博鐎n収鍤旀繛澶嬫礋濡剛绮嬫担纰樺亾娴e厜鍋撹閸?smoke闁靛棔绀侀悢鈧紒鎹愬劵閸撳ジ寮甸璺ㄧ憦濞寸姾娉涢悢鈧痪顓涘亾閻犱胶鍋撻弻? +- 鐟滅増鎸告晶鐘诲春閾忕懓娈犲☉鎾剁帛閺嗙喖骞戦娆炬綄 `docs/current/performance/baseline_v1.json` +- 闁告娲熼妴澶愬箺濡娲eù鍏济崢娑㈡儑?`docs/current/performance/PERFORMANCE_ONE_PAGER.md` +- 閻犲浄濡囩划蹇斿濡搫顕у銈夆偓娑氬枠闁稿繐鐗忓﹢?`docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` + +## 鐎殿喒鍋撻柛娆愬灣缁楀本顨ュ畝鍐 + +```bash +# 閺夆晜鍔橀、鎴澝圭€n厾妲? +python -m pytest -q -c pytest.ini + +# 閺夆晜鍔橀、?Phase 0 濞戞挸顭烽幗濂稿炊閻愯尙绉洪梻鍌樺妿椤╋箓鏁嶉崼鐔风倒濞存嚎鍊曟晶?闁告艾鐗嗛懟鐔煎礈瀹ュ繒绀? +python scripts/run_phase0_gate.py + +# 閺夆晜鍔橀、?Phase 1 闁硅矇鍐ㄧ厬闂傚牄鍨瑰ú鏍亹閹烘锛岀紒鍌欑筏缁辨┊1-01闁? +python scripts/run_p1_control_plane_gate.py + +# 閺夆晜鍔橀、?Phase 1 濞存粌顑勫▎銏ゅ籍閵夈儳绠堕柛銉у仜缂嶅﹪姊婚妸褜娲i柨娑樻恭1-02闁? +python scripts/run_p1_event_journal_gate.py + +# 閺夆晜鍔橀、?Phase 1 SQLite 闁绘鍩栭埀顑跨閹绮╅姘鐟滅増甯″Λ顒傜矉娓氬﹦绀凱1-03闁? +python scripts/run_p1_sqlite_state_backend_gate.py + +# 閺夆晜鍔橀、?Phase 1 Active Memory 闁搞儳鍋涚紞濠囨⒒閵娧屾矗闁挎稑婀?-04闁? +python scripts/run_p1_active_memory_gate.py + +# 閺夆晜鍔橀、?Phase 1 Hook/Permission/Audit 闁衡偓閼稿灚娈愰梻鍌樺妿椤╋箓鏁嶉崷?-05闁? +python scripts/run_p1_hook_permission_audit_gate.py + +# 閺夆晜鍔橀、?Phase 1 CLI thin-client 閺夆晙鑳朵簺闂傚倶鍔庨々锕傛晬閸?-06闁? +python scripts/run_p1_cli_thin_client_gate.py + +# 閺夆晜鍔橀、?Phase 2 濠?Agent Supervisor 闂傚倶鍔庨々锕傛晬閸?-01闁? +python scripts/run_p2_multi_agent_supervisor_gate.py + +# 閺夆晜鍔橀、?Phase 2 Artifact Bus 闂傚倶鍔庨々锕傛晬閸?-02闁? +python scripts/run_p2_artifact_bus_gate.py + +# 閺夆晜鍔橀、?Phase 2 IDE 闂傚棗妫欓崹姘舵⒒閵娧屾矗闁挎稑婀?-03闁? +python scripts/run_p2_ide_integration_gate.py + +# 閺夆晜鍔橀、?Phase 2 GitHub/CI workflow 闂傚倶鍔庨々锕傛晬閸?-04闁? +python scripts/run_p2_github_ci_workflow_gate.py + +# 閺夆晜鍔橀、?Phase 2 缂備礁瀚划鎰驳閺嶎偅娈e☉鎾抽椤撳摜鎷嬮敓鐘筹紝缂佸倷绶ょ槐姗?-05闁? +python scripts/run_p2_org_policy_audit_gate.py + +# 閺夆晜鍔橀、?Phase 2 Agent 閺夆晜鍔橀、鎴﹀籍閺堢數顏遍柤宄扮摠閳ь儸鍥紝缂佸倷绶ょ槐姗?-07闁? +python scripts/run_p2_agent_runtime_parity_gate.py + +# 閺夆晜鍔橀、?Phase 2 闁煎浜滈悾鐐▕?Agent 闁烩晩鍠栫紞宥夊礉閻樼儤绁伴梻鍌樺妿椤╋箓鏁嶉崷?-08闁? +python scripts/run_p2_custom_agents_loader_gate.py + +# 閺夆晜鍔橀、?Phase 2 JetBrains IDE 闂傚棗妫欓崹姘舵⒒閵娧屾矗闁挎稑婀?-10闁? +python scripts/run_p2_jetbrains_ide_integration_gate.py + +# 閺夆晜鍔橀、?Phase 0-2 Linux 缂備胶鍠嶇粩瀛橆殽鐏炶姤顢嶉梻鍌樺妿椤╋箓鏁嶉崷?-06闁? +python scripts/run_linux_unified_gate.py + +# 閺夆晜鍔橀、?Phase 2 Linux 缂備胶鍠嶇粩瀛橆殽鐏炶姤顢嶉柟绗涘棭鏀介梻鍌樺妿椤╋箓鏁嶉崷?-09闁挎稑顒歩nux 闁圭瑳鍡╂斀闁? +python scripts/run_p2_linux_unified_execution_gate.py --dry-run +python scripts/run_p2_linux_unified_execution_gate.py --continue-on-failure +# 闁告帒妫涙晶鏍嵁閹偊鏀界紒鈧潪鎵紣闁? 濞?job 濞戞搩鍘惧▓鎴犵箔?2 闁绘娴勭槐? +python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2 --continue-on-failure + +# 闁艰鲸鑹鹃幃搴㈠緞濮橆剙鐎婚柣?summary 妤犵偛澧庣划浼村礄閸濆嫬寮块悘鐐╁亾闂侇偅淇虹换?濠㈡儼绮剧憴锕傚礆閵堝懐鏆伴柨娑樻恭2-12闁挎稑顒歩nux 闁搞儳鍋為弫褰掓⒓閼告鍞介柟绗涘棭鏀介柨? +python scripts/run_p2_linux_shard_aggregation_gate.py --artifacts-dir .claude/reports +python scripts/run_p2_linux_shard_aggregation_gate.py --summary-glob ".claude/reports/**/summary.json" + +# 濞?merged summary 闁告瑦鍨电粩鐑藉嫉閳ь剛绱掗崼鐔沸撻柛娑橆煭缁辨┊2-13闁挎稑顒歩nux 闁搞儳鍋為弫褰掓⒓閼告鍞介柟绗涘棭鏀介柨? +python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json +python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md + +# 濞戞挴鍋撻梺娆惧枙鐟曞棝鎳曢弮鈧晶鐣屾偘?>婵懓娲﹂埀?>闁告瑦鍨电粩鐑藉礂閵娾晜鎳犻梻鍌樺妿椤╋箓鏁嶉崷?-14闁挎稑顒歩nux 缂傚倹鐗楃敮鎾诲礂閵夈儱缍撻柨? +python scripts/run_p2_linux_unified_pipeline_gate.py --dry-run +python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure --fail-fast +# 濞寸姴鎳庢禒娑樞ч崶銊㈠亾?闁告瑦鍨电粩鐑芥晬閸絿鍎查弶鈺佹处婢х晫鎮板畝鍕枆婵炲牏顣槐? +python scripts/run_p2_linux_unified_pipeline_gate.py --skip-execution --summary-glob ".claude/reports/**/summary.json" + +# 闁汇垻鍠愰崹?Linux 闁告帒妫涙晶鏍箥瑜戦、鎴犳媼閳ュ啿鐏婇柨娑樻恭2-15闁挎稑濂旂欢?CI fan-out job 闁烩晛鐡ㄧ敮鏉戔槈閸絽鐎柨? +python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --dry-run --print-commands +python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --output .claude/reports/linux_unified_gate/shard_plan.json --continue-on-failure -- -k runtime + +# 闁汇垻鍠愰崹?Linux CI matrix 濞存籂鍛挅闁挎稑婀?-16闁挎稑濂旂欢?CI matrix 闁烩晛鐡ㄧ敮鏉戔槈閸絽鐎柨? +python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --dry-run --print-matrix +python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --output .claude/reports/linux_unified_gate/ci_matrix.json --skip-empty-shards + +# 闁汇垻鍠愰崹?Linux CI workflow 閻犱讲鈧啿鐏婂ù婧犲懎鈷栭柨娑樻恭2-17闁挎稑濂旂欢?fan-out/fan-in job 闁烩晛鐡ㄧ敮鏉戔槈閸絽鐎柨? +python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --dry-run --print-plan +python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --output .claude/reports/linux_unified_gate/ci_workflow_plan.json + +# 閻?workflow 閻犱讲鈧啿鐏婃繛鎾冲级閻撳绋?GitHub Actions YAML闁挎稑婀?-18闁挎稑濂旂欢?CI 闁烩晛鐡ㄧ敮鎾箥瑜戦、鎴︽晬? +python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-yaml +python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --output-workflow .github/workflows/linux_unified_gate.yml --output-metadata .claude/reports/linux_unified_gate/ci_workflow_render.json + +# 闁哄稄绻濋悰?闁告艾鏈?workflow 婵炴挸寮堕悡瀣瑜忔晶鍨煶閸屾簽鈺呮晬閸?-19闁挎稑鐭傚Σ璇差潰?plan->yaml 婵犳洖鍊讳簺闁? +python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --print-diff +python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --write + +# 闁哄稄绻濋悰?閻熸瑥瀚€垫牠宕?workflow 閻犱讲鈧啿鐏婂☉鎿冨幘濞堟垿宕ㄩ幋鎺撳Б濠靛倹鍨圭€规娊鏁嶉崷?-20闁挎稑鐭傚Σ璇差潰?command/parts/閻犱警鍨扮欢?婵犳洖鍊讳簺闁? +python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --write --output .claude/reports/linux_unified_gate/ci_workflow_command_guard.json + +# 婵懓娲﹂埀?workflow 婵犳洖鍊讳簺 + 闁告稒鍨濋幎?+ 闁稿繐鍟弳鐔煎箲椤旀鏀ㄧ紓鍌浬戞稉宥夋偠閸☆厾绀凱2-21闁挎稑鐬奸弫鎾诲箣閹邦剙绀嬪☉鎾亾婵炲矁宕甸幃濠勭磼閹捐鍟堥柨? +python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --output .claude/reports/linux_unified_gate/ci_workflow_governance.json + +# 闁告瑦鍨电粩宄扳柦閼姐倖鍊炵紓浣规崄椤旀垶绋?CI 闁告瑯鍨辩粔椋庢嫻閻熸澘鏋€缂佹稒鐗槐姗?-22闁挎稑鐬奸弫鎾诲箣?should_execute_workflow 濞戞挸楠歌ぐ鍌滄暜閸愨晛袚闁告稑顭槐? +python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --output-json .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_governance_publish.md + +# 閻忓繐妫欐稉宥夋偠閸℃绲洪悽顖氬暟缁劑寮稿鍕毆闁轰焦绋愮拹鐔煎箥瑜戦、鎴﹀礃瀹曞洨鎽滈梻鍌涙倐濡剟鏁嶉崷?-23闁挎稑鐬肩划鐑樼▔閳?execute/blocked 闁告帇鍊曢悾鐐▔鎼淬劉鍋撻埀顒勫礄閾忓湱鎽滈柣锝冨劵缁? +python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --on-block skip --output-json .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_execution_decision.md + +# 閻忓繐妫欐晶鐣屾偘鐏炶棄鏋€缂佹稒鐗楅弫褰掑极濞戞绀嬮柛娆樺灥閻ㄧ喐鎯旈敃鈧幊鈩冪閵堝拋娈╃紒鎾呰缁辨┊2-24闁挎稑鐬肩划鐑樼▔閳?dispatch-ready/blocked 濞?gh workflow run 闁告稒鍨濋幎銈夋晬? +python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch.md + +# 闁圭瑳鍡╂斀閻犲鍟€硅櫕绺介幋鐘差唺妤犵偞鍎肩欢顓㈠礄閻戞ɑ浠樼紓浣哥墛婢х晫鎮扮仦鑺ョ闁圭瑳宥囩P2-25闁挎稑鐬肩划鐑樼▔閳?dispatched/blocked/fail 闁汇劌瀚晶鐣屾偘瀹€鈧划銊╁几濠婃劗绀? +python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md + +# 閻?dispatch 闁圭瑳鍡╂斀缂備焦鎸婚悘澶愬绩閼稿灚娈愬☉?run trace 濠靛倹鍨圭€规娊鏁嶉崷?-27闁挎稑鐬肩划鐑樼▔閳?run_id/url/poll 闁告稒鍨濋幎銈嗙▔鎼达絽笑闁诡兛绶ょ槐? +python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md + +# 缂佹稑顦欢?dispatch run 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱掗崼婵堟殮闁瑰瓨鍔曢崹鐣屸偓瑙勭啲缁辨┊2-28闁挎稑鐬肩划鐑樼▔閳?completed/in_progress/timeout/failure 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md + +# 闁告瑦鍨电粩?dispatch completion 闁汇劌瀚划鎾朵沪閳ь剛绱掗幘璇″晥闁挎稑婀?-29闁挎稑鐬肩划鐑樼▔閳?promote/hold 闁告瑦鍨电粩椋庢嫚椤撴繄鐤呴柨? +python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md + +# 閻?terminal publish 闁衡偓閼稿灚娈愬☉?release handoff 濠靛倹鍨圭€规娊鏁嶉崷?-30闁挎稑鐬肩划鐑樼▔閳?release job 閻熸瑱绠戣ぐ鍌滄嫚椤撴繄鐤呴柨? +python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --target-environment production --release-channel stable --output-json .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_handoff.md + +# 闁圭瑳鍡╂斀 release 闁哄牃鍋撶紓浣哥墣琚濋柛娆愬灴濡剛绮嬫笟濠勭P2-31闁挎稑鐬肩划鐑樼▔閳?release dispatch 闁圭瑳鍡╂斀閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trigger.md + +# 閺夆晛鈧喖鍤?release trigger 闁圭瑳鍡╂斀闁告艾娴峰▓?run 閻犲浂鍘虹粻鐔兼晬閸?-32闁挎稑鐬肩划鐑樼▔閳?release run trace/poll 闁绘鍩栭埀顑跨筏缁? +python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trace.md + +# 缂佹稑顦欢?release run 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱掗崼婵堟殮闁瑰瓨鍔曢崹鐣屸偓瑙勭啲缁辨┊2-33闁挎稑鐬肩划鐑樼▔閳?release 閻庣懓鏈崹姘跺箑娴g瓔鍤斿☉鏂款檧缁? +python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_completion.md + +# 闁告瑦鍨电粩?release completion 闁汇劌瀚划鎾朵沪閳ь剛绱掗幘璇″晥闁挎稑婀?-34闁挎稑鐬肩划鐑樼▔閳?release finalize/hold 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md + +# 閻?release terminal publish 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱?finalization 闁告劕纾悺銉╂晬閸?-35闁挎稑鐬肩划鐑樼▔閳?finalize/hold/abort 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --on-hold pass --output-json .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_finalization.md + +# 閻?release finalization 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱?closure 闁告瑦鍨电粩閿嬬附閹寸姴顔婇柨娑樻恭2-36闁挎稑鐬肩划鐑樼▔閳?close/notify/rollback 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_closure.md + +# 閻?release closure 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱掗崼锝囨闁硅鍠栫紞濠傤浖閿濆拋娈╃紒鎾呰缁辨┊2-37闁挎稑鐬肩划鐑樼▔閳?archive/publish/block 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_archive.md + +# 閻?release archive 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱掗崼婵嗙岛閻㈩垰鍟崹鐣屾嫚瀹ュ拋娈╃紒鎾呰缁辨┊2-38闁挎稑鐬肩划鐑樼▔閳?ship/hold/block 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_verdict.md + +# 閻?release verdict 闁衡偓閼稿灚娈愬☉?incident dispatch 闁圭瑳鍡╂斀濠靛倹鍨圭€规娊鏁嶉崷?-39闁挎稑鐬肩划鐑樼▔閳ь剟宕ㄦ繝鍜佸妳閻熸瑱绠戣ぐ鍌滄嫚椤撴繄鐤呴柨? +python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --incident-repo acme/demo --incident-label release-incident --output-json .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_incident.md + +# 閻?release incident 闁衡偓閼稿灚娈愬☉鎾规缁挾浠﹂埀?terminal verdict 濠靛倹鍨圭€规娊鏁嶉崷?-40闁挎稑鐬肩划鐑樼▔閳?ship/hold/escalate/block 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md + +# 閻?release terminal verdict 闁衡偓閼稿灚娈愬☉鎾跺劋濞撳墎绱?delivery 濠靛倹鍨圭€规娊鏁嶉崷?-41闁挎稑鐬肩划鐑樼▔閳?deliver/hold/escalate/block 閻犲浂鍘虹粻鐔兼晬? +python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery.md + +# P2-42: converge release delivery to terminal publish contract (announce_release/announce_hold/announce_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md + +# P2-43: converge release delivery terminal publish to final verdict contract (close_release/open_follow_up/escalate_blocker/abort_close). +python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md + + +# P2-44: converge release delivery final verdict to follow-up dispatch contract (no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch). +python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md + +# P2-45: converge release follow-up dispatch to follow-up closure contract (no_action/queue_follow_up/queue_escalation/abort_queue). +python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --follow-up-repo acme/demo --follow-up-label release-follow-up --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md + +# P2-46: converge release follow-up closure to terminal publish contract (announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md + +# P2-47: converge release follow-up terminal publish to final verdict contract (close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close). +python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md + +# P2-48: converge delivery/follow-up final verdicts to one release final outcome contract (ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome). +python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md + +# P2-49: converge release final outcome to terminal publish contract (announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md + +# P2-50: converge release final terminal publish to final handoff contract (handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff). +python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md + +# P2-51: converge release final handoff to final closure contract (close_release/close_with_follow_up/close_blocker/abort_close). +python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md + +# P2-52: publish release final closure contract (announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md + +# P2-53: archive release final closure publish contract (archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive). +python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md + +# P2-54: converge release final archive to final verdict contract (ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict). +python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md + +# P2-55: publish release final verdict contract (announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md + +# P2-56: archive release final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive). +python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md + +# P2-57: close gate-script/runtime-test/manifest drift contract. +python scripts/run_p2_linux_gate_manifest_drift_gate.py --dry-run --print-report +python scripts/run_p2_linux_gate_manifest_drift_gate.py --output-json .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-markdown .claude/reports/linux_unified_gate/linux_gate_manifest_drift.md + +# P2-59: converge P2-56+P2-57 to one Linux terminal verdict contract. +python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md + +# P2-60: dispatch Linux validation command from P2-59 terminal verdict contract. +python scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --linux-validation-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md + +# P2-61: converge Linux validation dispatch into one final Linux validation verdict contract. +python scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --dry-run --print-report +python scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md + +# P2-62: publish Linux validation verdict contract (announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish). +python scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --dry-run --print-report +python scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md + +# P2-63: terminal publish Linux validation verdict publish contract (announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish). +python scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --dry-run --print-report +python scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md + +# P2-64: converge Linux validation terminal publish contract to final verdict (accept_linux_validation_terminal/accept_linux_validation_terminal_with_follow_up/escalate_linux_validation_terminal_blocker/abort_linux_validation_terminal_verdict). +python scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --dry-run --print-report +python scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md + +# P2-65: publish Linux validation final verdict contract (announce_linux_validation_final_validated/announce_linux_validation_final_validated_with_follow_up/announce_linux_validation_final_blocker/abort_publish). +python scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --dry-run --print-report +python scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md + +# P2-66: archive Linux validation final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive). +python scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --dry-run --print-report +python scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md + +# P2-67: converge Linux validation final publish archive contract to terminal verdict (proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation). +python scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --dry-run --print-report +python scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md + +# P2-68: publish Linux validation terminal verdict contract (announce_linux_validation_terminal_ready/announce_linux_validation_terminal_ready_with_follow_up/announce_linux_validation_terminal_blocker/abort_publish). +python scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md + +# P2-69: converge Linux validation terminal verdict publish to terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch). +python scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --dry-run --print-report +python scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md + +# P2-70: execute Linux validation terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch). +python scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --dry-run --print-report +python scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --linux-validation-terminal-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md + +# P2-71: trace Linux validation terminal dispatch execution run reference (run_tracking_ready/run_tracking_missing/run_poll_failed/run_in_progress/run_completed_success/run_completed_failure). +python scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --dry-run --print-report +python scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --poll-timeout-seconds 600 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md + +# P2-72: await Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report). +python scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --dry-run --print-report +python scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md + +# P2-73: terminal publish Linux validation terminal dispatch completion contract (announce_linux_validation_terminal_dispatch_completed/announce_linux_validation_terminal_dispatch_completed_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failed/abort_linux_validation_terminal_dispatch_terminal_publish). +python scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --dry-run --print-report +python scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md + +# P2-74: final verdict Linux validation terminal dispatch terminal publish contract (accept_linux_validation_terminal_dispatch/accept_linux_validation_terminal_dispatch_with_follow_up/keep_linux_validation_terminal_dispatch_in_progress/escalate_linux_validation_terminal_dispatch_blocker/escalate_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict). +python scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --dry-run --print-report +python scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md + +# P2-75: publish Linux validation terminal dispatch final verdict contract (announce_linux_validation_terminal_dispatch_validated/announce_linux_validation_terminal_dispatch_validated_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict_publish). +python scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --dry-run --print-report +python scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md + +# P2-76: archive Linux validation terminal dispatch final verdict publish contract (archive_linux_validation_terminal_dispatch_validated/archive_linux_validation_terminal_dispatch_validated_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_publish_archive). +python scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --dry-run --print-report +python scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md + +# P2-77: terminal verdict Linux validation terminal dispatch final publish archive contract (proceed_linux_validation_terminal_dispatch/proceed_linux_validation_terminal_dispatch_with_follow_up/hold_linux_validation_terminal_dispatch_in_progress/halt_linux_validation_terminal_dispatch_blocker/halt_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict). +python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --dry-run --print-report +python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md + +# P2-78: publish Linux validation terminal dispatch terminal verdict contract (announce_linux_validation_terminal_dispatch_ready/announce_linux_validation_terminal_dispatch_ready_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish). +python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md + +# P2-79: archive Linux validation terminal dispatch terminal verdict publish contract (archive_linux_validation_terminal_dispatch_ready/archive_linux_validation_terminal_dispatch_ready_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive). +python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --dry-run --print-report +python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md + +# P2-80: converge Linux validation terminal dispatch terminal verdict closure contract (proceed_linux_validation_terminal_dispatch/proceed_linux_validation_terminal_dispatch_with_follow_up/hold_linux_validation_terminal_dispatch_in_progress/halt_linux_validation_terminal_dispatch_blocker/halt_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_closure). +# P2-81: publish Linux validation terminal dispatch terminal verdict closure contract (announce_linux_validation_terminal_dispatch_ready/announce_linux_validation_terminal_dispatch_ready_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish). +# P2-82: archive Linux validation terminal dispatch terminal verdict closure publish contract (archive_linux_validation_terminal_dispatch_ready/archive_linux_validation_terminal_dispatch_ready_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive). +# P2-83: validate pipeline closure to P2-82 (pipeline_closure_to_p2_82_verified/hold_pipeline_closure_manual_action). +# P2-84: validate closure-chain continuity to P2-83 (pipeline_closure_to_p2_83_verified/hold_pipeline_closure_p2_83_manual_action). +# P2-85: validate closure-chain continuity to P2-84 (pipeline_closure_to_p2_84_verified/hold_pipeline_closure_p2_84_manual_action). +# P2-86: validate closure-chain continuity to P2-85 (pipeline_closure_to_p2_85_verified/hold_pipeline_closure_p2_85_manual_action). +# P2-87: validate closure-chain continuity to P2-86 (pipeline_closure_to_p2_86_verified/hold_pipeline_closure_p2_86_manual_action). +# P2-88: validate closure-chain continuity to P2-87 (pipeline_closure_to_p2_87_verified/hold_pipeline_closure_p2_87_manual_action). +# P2-89: validate closure-chain continuity to P2-88 (pipeline_closure_to_p2_88_verified/hold_pipeline_closure_p2_88_manual_action). + +# P2-90: validate closure-chain continuity to P2-89 (pipeline_closure_to_p2_89_verified/hold_pipeline_closure_p2_89_manual_action). + +# P2-91: validate closure-chain continuity to P2-90 (pipeline_closure_to_p2_90_verified/hold_pipeline_closure_p2_90_manual_action). +python scripts/run_p2_lv_td_tverdict_closure_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-report +python scripts/run_p2_lv_td_tverdict_closure_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md +python scripts/run_p2_lv_td_tverdict_closure_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json --dry-run --print-report +python scripts/run_p2_lv_td_tverdict_closure_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md +python scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json --dry-run --print-report +python scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-gate-script scripts/run_p2_linux_ci_workflow_pipeline_gate.py --closure-publish-archive-gate-script scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-gate-script scripts/run_p2_linux_ci_workflow_pipeline_gate.py --closure-publish-archive-gate-script scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-82-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-closure-p2-82-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-82-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-closure-p2-82-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-83-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-83-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-83-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-83-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-84-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-84-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-84-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-84-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-85-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-85-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-85-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-85-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-86-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-86-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-86-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-86-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-87-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-87-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-87-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-87-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.md + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-88-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-88-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py --dry-run --print-report + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-88-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-88-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.md + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py --pipeline-closure-p2-89-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-89-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py --dry-run --print-report + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py --pipeline-closure-p2-89-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-89-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.md + +# 濞戞挴鍋撻梺娆惧枙鐟曞棝鎳?P2-17 -> P2-89 CI workflow 闁稿繈鍔戦幗濂告⒒閵娧屾矗闁挎稑婀?-26闁挎稑顒歩nux CI 闁诡剝顕ч崣鍡涘矗閿濆繒绀? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --dry-run +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --fail-fast --sync-write --command-guard-write --dispatch-trace-poll-now --completion-poll-interval-seconds 20 --completion-max-polls 15 --target-environment production --release-channel stable --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --release-trace-poll-now --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --on-release-hold pass --follow-up-repo acme/demo --follow-up-label release-follow-up +# 濞寸姴鎳忔晶鐣屾偘鐏炶姤鍊甸柛妤€锕幗濂告晬閸粎鐭ゆ繛宀冨吹閹﹪宕i幋婵堫伌鐎殿喒鍋撳┑顔碱儜缁? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance +# 濞寸姴鎳忔晶鐣屾偘?P2-29 缂備礁鐗嗛惇顒勫矗閹存繄顏撮柨娑樼墕椤︽煡鎮介妸銉ュ殥濞存籂鍐ㄦ瘔闁?completion 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion +# 濞寸姴鎳忔晶鐣屾偘?P2-30 release handoff闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?terminal publish 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --target-environment staging --release-channel canary --skip-release-trigger --skip-release-trace --skip-release-completion +# 濞寸姴鎳忔晶鐣屾偘?P2-31 release trigger闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release handoff 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --skip-release-trace --skip-release-completion +# 濞寸姴鎳忔晶鐣屾偘?P2-32 release trace闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release trigger 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --release-trace-poll-now --skip-release-completion +# 濞寸姴鎳忔晶鐣屾偘?P2-33 release completion闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release trace 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --skip-release-terminal-publish +# 濞寸姴鎳忔晶鐣屾偘?P2-34 release terminal publish闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release completion 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-finalization --skip-release-closure --skip-release-archive +# 濞寸姴鎳忔晶鐣屾偘?P2-35 release finalization闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release terminal publish 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --on-release-hold fail --skip-release-closure --skip-release-archive +# 濞寸姴鎳忔晶鐣屾偘?P2-36 release closure闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release finalization 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-archive +# 濞寸姴鎳忔晶鐣屾偘?P2-37 release archive闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release closure 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-verdict --skip-release-incident +# 濞寸姴鎳忔晶鐣屾偘?P2-38 release verdict闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release archive 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-incident +# 濞寸姴鎳忔晶鐣屾偘?P2-39 release incident闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release verdict 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-terminal-verdict --incident-repo acme/demo --incident-label release-incident +# 濞寸姴鎳忔晶鐣屾偘?P2-40 release terminal verdict闁挎稑鐗嗛ˇ鏌ユ偨閵娿儱鍤掑ù婧犲啫姣夐柣?release incident 闁硅翰鍎遍幉锟犳晬? +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch +# P2-42 only: run release delivery terminal publish (reuse release delivery report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch +# P2-43 only: run release delivery final verdict (reuse release delivery terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-follow-up-dispatch +# P2-44 only: run release follow-up dispatch (reuse release delivery final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict +# P2-45 only: run release follow-up closure (reuse release follow-up dispatch report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up + +# P2-46 only: run release follow-up terminal publish (reuse release follow-up closure report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up + +# P2-47 only: run release follow-up final verdict (reuse release follow-up terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --follow-up-repo acme/demo --follow-up-label release-follow-up +# P2-48 only: run release final outcome (reuse P2-43 + P2-47 final verdict reports). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict +# P2-49 only: run release final terminal publish (reuse P2-48 final outcome report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish +# P2-50 only: run release final handoff (reuse P2-49 final terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-closure --skip-release-final-closure-publish +# P2-51 only: run release final closure (reuse P2-50 final handoff report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure-publish +# P2-52 only: run release final closure publish (reuse P2-51 final closure report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-archive + +# P2-53 only: run release final archive (reuse P2-52 final closure publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish +# P2-54 only: run release final verdict (reuse P2-53 final archive report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive +# P2-55 only: run release final verdict publish (reuse P2-54 final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict +# P2-56 only: run release final publish archive (reuse P2-55 final verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish +# P2-57 only: run gate manifest drift closure (independent closure check over scripts/tests/manifest). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive +# P2-59 only: run terminal verdict closure (reuse P2-56 + P2-57 reports). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift +# P2-60 only: run Linux validation dispatch (reuse P2-59 terminal verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --linux-validation-timeout-seconds 7200 +# P2-61 only: run Linux validation verdict (reuse P2-60 Linux validation dispatch report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch +# P2-62 only: run Linux validation verdict publish (reuse P2-61 Linux validation verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict +# P2-63 only: run Linux validation terminal publish (reuse P2-62 Linux validation verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish +# P2-64 only: run Linux validation final verdict (reuse P2-63 Linux validation terminal publish report). +# P2-65 only: run Linux validation final verdict publish (reuse P2-64 Linux validation final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict +# P2-66 only: run Linux validation final publish archive (reuse P2-65 Linux validation final verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish +# P2-67 only: run Linux validation terminal verdict (reuse P2-66 Linux validation final publish archive report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive +# P2-68 only: run Linux validation terminal verdict publish (reuse P2-67 Linux validation terminal verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict +# P2-69 only: run Linux validation terminal dispatch (reuse P2-68 Linux validation terminal verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish +# P2-70 only: run Linux validation terminal dispatch execution (reuse P2-69 Linux validation terminal dispatch report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch +# P2-71 only: run Linux validation terminal dispatch trace (reuse P2-70 Linux validation terminal dispatch execution report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion +# P2-72 only: run Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-terminal-publish +# P2-73 only: run Linux validation terminal dispatch terminal publish (reuse P2-72 Linux validation terminal dispatch completion report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-final-verdict +# P2-74 only: run Linux validation terminal dispatch final verdict (reuse P2-73 Linux validation terminal dispatch terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish +# P2-75 only: run Linux validation terminal dispatch final verdict publish (reuse P2-74 Linux validation terminal dispatch final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-final-publish-archive +# P2-76 only: run Linux validation terminal dispatch final publish archive (reuse P2-75 Linux validation terminal dispatch final verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-terminal-verdict +# P2-77 only: run Linux validation terminal dispatch terminal verdict (reuse P2-76 Linux validation terminal dispatch final publish archive report). +python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md +# P2-78 only: run Linux validation terminal dispatch terminal verdict publish (reuse P2-77 Linux validation terminal dispatch terminal verdict report). +python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md +# P2-79 only: run Linux validation terminal dispatch terminal verdict publish archive (reuse P2-78 Linux validation terminal dispatch terminal verdict publish report). +python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md +# P2-80 only: run Linux validation terminal dispatch terminal verdict closure (reuse P2-79 publish-archive report + P2-57 manifest drift report). +python scripts/run_p2_lv_td_tverdict_closure_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md +# P2-81 only: run Linux validation terminal dispatch terminal verdict closure publish (reuse P2-80 closure report). +python scripts/run_p2_lv_td_tverdict_closure_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md +# P2-82 only: run Linux validation terminal dispatch terminal verdict closure publish archive (reuse P2-81 closure publish report). +python scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md +# P2-83 only: validate pipeline closure to P2-82 (checks terminal closure publish archive stage wiring in P2-26 pipeline gate). +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-gate-script scripts/run_p2_linux_ci_workflow_pipeline_gate.py --closure-publish-archive-gate-script scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.md +# P2-84 only: validate pipeline closure to P2-83 (checks closure-continuity contract for P2-82 closure verifier). +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-82-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-closure-p2-82-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-82-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py --pipeline-closure-p2-82-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.md +# P2-85 only: validate pipeline closure to P2-84 (checks closure-continuity contract for P2-83 closure verifier). +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-83-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-83-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-83-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py --pipeline-closure-p2-83-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.md +# P2-86 only: validate pipeline closure to P2-85 (checks closure-continuity contract for P2-84 closure verifier). +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-84-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-84-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-84-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py --pipeline-closure-p2-84-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.md +# P2-87 only: validate pipeline closure to P2-86 (checks closure-continuity contract for P2-85 closure verifier). +# P2-88 only: validate pipeline closure to P2-87 (checks closure-continuity contract for P2-86 closure verifier). +# P2-89 only: validate pipeline closure to P2-88 (checks closure-continuity contract for P2-87 closure verifier). + +# P2-90 only: validate pipeline closure to P2-89 (checks closure-continuity contract for P2-88 closure verifier). + +# P2-91 only: validate pipeline closure to P2-90 (checks closure-continuity contract for P2-89 closure verifier). +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-85-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-85-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-85-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py --pipeline-closure-p2-85-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-86-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-86-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-86-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py --pipeline-closure-p2-86-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.md +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-87-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-87-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-87-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py --pipeline-closure-p2-87-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.md + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-88-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-88-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py --dry-run --print-report + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-88-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py --pipeline-closure-p2-88-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.md + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py --pipeline-closure-p2-89-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-89-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py --dry-run --print-report + +python scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py --pipeline-closure-p2-89-gate-script scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py --pipeline-closure-p2-89-runtime-test tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py --output-json .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.md + +# 閻犲浂鍘界涵璺何涢埀顒勫蓟? +python scripts/check_syntax.py --root claude_code + +# 閺夆晜鍔橀、鎴﹀箑瑜戦崗?smoke +python -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py + +# 闁汇垻鍠愰崹姘跺箑瑜戦崗姗€宕洪搹鐟版疇 +python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json +``` + +闁哄牜鍓氶濂稿极鐎靛憡鍊為柡鍐硾閻ゅ嫰姊介崨顓фЩ闁哄秵鐡曠换鍐晬? + +```bash +python -m pytest -q -c pytest.ini +``` + +缂備焦鎸婚悘澶嬬▔?`90 passed`闁? + +## 闂佹澘绉堕悿鍡樼▔鎼淬倗绠ラ悶娑樻湰濡炲倿寮崶锔筋偨 + +鐟滅増鎸告晶鐘崇閿濆洨鍨抽梺鎻掕嫰閻栧墎鎲存担鐑樼暠闂佹澘绉堕悿鍡樼▔鎼淬倗绠ラ悶娑樻湰濡炲倿寮崶锔筋偨闁告帒妫楃粩閿嬩繆閸屾瑧鐟撻柨? + +| 濞达絽绉堕悿?| 闁活潿鍔戦埀?| +|---|---| +| `~/.claude-code-python/base.json` | 闁稿繈鍔岄惇顒勫春閾忚鏀ㄩ梺鏉跨Ф閻?| +| `~/.claude-code-python/{env}.json` | 闁圭顦遍獮鍡樻櫠閸愩劌鐎婚悘鐐插€块崢銈囩磾?| +| `~/.claude-code-python/config.json` | 闁活潿鍔嶉崺娑㈠箥鐎n亙绱i悷鏇炴濞插﹪鏌婂鍥╂瀭 | +| `.claude-code-python.json` | 濡炪倕婀卞ú浼村嫉椤掆偓濠€瀵告媼閸撗呮瀭 | +| `.claude/` | 濡炪倕婀卞ú鐗堟交閹邦垼鏀介柡浣哄瀹撲線鎯勯鑲╃Э闁挎稑鑻悥鍓佹喆娴g鏁堕悗鍦嚀鐎垫﹢骞忛鎸庡床闁告枀鎵冲亾娴g瓔鍚€闁告帗甯囬埀顑跨劍婵⊙囨嚄婵劏鍋撴稊鐠穙ks闁靛棔杈渙do闁靛棔杈渆am闁靛棔澶焎heduled tasks | +| `.mcp.json` | MCP 闁烩晝顭堥崣褔鏌婂鍥╂瀭 | +| `CLAUDE.md` / `CLAUDE.local.md` | 濡炪倕婀卞ú浼村箰閸パ屽殼濞e洠鍓濇导鍛存晬瀹€鈧弫?`/init` 闁告帗绻傞~鎰板礌?| + +濠碘€冲€归悘澶嬫媴閻樹警娲i柟鐑樺笚閻撯剝绂掔拠鎻掝潳缂侇垵宕电划娲晬鐏炶偐鍠橀柛蹇撶墕閸櫻冣枖椤帞绐? + +- `.claude/tasks.json` +- `.claude/runtime_tasks.json` +- `.claude/scheduled_tasks.json` + +## 缂佲偓鏉炴壆浼愬☉鎾抽閸欏棝宕i敐鍡樼€ù? + +- `examples/basic_usage.py`闁挎稒鑹鹃悢鈧痪顓涘亾閻犲鍟伴弫銈囩矆鏉炴壆浼? +- `examples/configuration.py`闁挎稒宀搁崢銈囩磾椤斾勘浠涘〒? +- `examples/custom_tools.py`闁挎稒淇洪崵婊呪偓瑙勭煯缁犵喎顔忛妷銉ュ緮缂佲偓鏉炴壆浼? +- `examples/mcp_integration.py`闁挎稒鐡塁P 闂傚棗妫欓崹姘辩矆鏉炴壆浼? +- `examples/session_management.py`闁挎稒鐭槐鎵嫚濠靛牜鍚€闁荤偛妫涢妵姘瑹? + +## 闁告艾娴烽悽濠氬棘閸ャ劊鈧倻绱掔€涙ê袘閻熸瑥瀚崹? + +濞戞捁妗ㄧ花锟犳焼閸喖甯抽柡鍌氭处閵嗗倿宕樺鍡╁仹闁艰翰鍔忛崕澶愭晬鐏炶姤鍊电紓渚囧幖缂傛挾鎷嬮鈧导鎺斺偓鐟扮墔娴滄帗绋夌€n収娼愰柛鎺撶懕缁? + +1. README 闁告瑯浜滄禒娑㈠礂閵夈儱缍撻柕鍡曠劍閹插磭鎲版担鍛婂閻庝絻澹堥崺鍛存晬鐏炶偐鐟濋柛鎰Х閹风兘宕濋悩灞備海闁搞劌顑勭粭鎾达紣濡鍤涢柡鍕昂閳? +2. 闁告艾濂旂粩瀛樼▔婵犳凹鏆柡鍫氬亾濠㈣埖鐭换姘舵偩濞嗗海顏卞☉鎿冧簴閳ь剚绮岀紞瀣礈瀹ュ嫬鐦滈柡鍌氭处閵嗗倿鍨惧┑鍡樺濞戞挴鍋撳☉鎿冧簴閳ь剚绮屽濠氬矗閼肩澶愬礂閸涱喗鐎俊妤嬬岛閳ь剚绺块埀? +3. 闁哄倹澹嗗▓鎴犳嫚閸曨亜寮?闁诡剝宕电划銊╁棘閸ャ劊鈧倽绠涢崨娣偓蹇曟暜閿旇姤锛夐柡鍫㈠櫐缁辨繈鐛懜鍨潠缁绢収鍠涢崵婊冾啅鏉堛劍笑闁告熬绠戣ぐ鍥ㄧ閿濆棙锛嬮柡鍌氭处閵嗗倿濡? +4. 閻℃帒鎳撶换鍐╃▔閳ь剚銇勯悽鍨暠濞戞挻鎹囬。浠嬪礃閸涱収鍟囬柣鈺佺摠鐢挳寮ㄩ幆褍鐓?`docs/`闁挎稑顒‥ADME 闁告瑯浜滈崯鎾诲箺濡娲i柛婊冪焸閹藉ジ骞掗妷锝傚亾? +5. 濠碘€冲€归悘澶愬蓟閹邦亪鍤嬪☉鎾存崌椤h棄顔忛懠顒傜梾闁哄牆顦€靛矂寮崶銊b偓鍌炴晬鐏炶姤鍊电紓渚囧弨钘熼柛蹇撴噸缁鳖參宕楅崼鐔哥函闁哄倹婢樼敮顐﹀棘閸ャ劊鈧倿鏁嶅畝鍐e亾鐏炶偐鐟濋柡鍕靛灡閺屽﹤顕欓崫鍕尙閻炴稑鏈弸鍐浖閿濆啠鍋? + +## 缂備焦鎹侀? + +閺夆晜鐟﹂濂稿极鐎靛憡鍊為柛姘嚱缁辨紮README.md` 濞戞挸绉撮崯鈧柟鍨婵挳鍨惧鍕暡闁哄牆顦遍悡锛勬嫚閸℃稑鍘撮柛鎰懃濠€顏呯▔閳ь剚寰勯崟銊㈠亾濠靛牊鐣遍柤鍗炵焷閻鏁嶅畝鍐e亾鐏炵偓笑闁瑰瓨鍔掔拹鐔哥閹惧磭姘ㄩ柣銊ュ鑿欓悗瑙勮壘閸欏棝宕i敐蹇曠獥 + +- 闁诡垰鐤囩换宥囨偘瀹€鍕┾偓宥夋儎椤曞棛绀夐柛蹇撶墢濠€鍛村灳濠婂啯褰ラ梺顐ゅ枎缁辨垶鎱ㄧ€Q€鍋? +- 闁诡垰纾﹢鍛般亹閹惧啿顤呴柡瀣煐閻?闁诡儸鍡楀幋/濞戞搩鍙冨Λ鎸庣閸撲礁笑闁诡兛绶ょ槐婵嬫儎鐎涙ê澶嶉柣顏勵儏椤曨喗鎯旈弬鎯х槣闁哄倸娲﹂妴? +- 闁诡垵娅曠换渚€宕?Agent 闁?MCP闁挎稑鑻崯鈧弶鈺傜☉閸欏棙寰勮閺嬪啫顩? +- 闁诡垰鐤囬幏宄扳攦椤栨艾鍧婇柛娆愮叀濡埖锛愬Ο鐑樺濞e浂鍠栭ˇ鏌ュ棘鐟欏嫷鏀抽柨娑樿嫰閸熲偓闁活亜顑呭濠氬矗閸欏鐎俊? + +閺夆晜鐟ュ銊╁及椤栨氨绉奸柛鎾崇С缁劍鎯旈幘瀛樼€俊妤嬬悼濞堟垿骞掗妸銊ョ闂傚啫鎳撻鐗堛亜閸濆嫮纰嶉柛婊冪灱濞n噣骞庨妶鍫㈢彾闁伙絽琚埀? + + + + + + + diff --git a/.claude/blob_65cf5acbb1de8197ea42e74e03fb9c2505f2808c.txt b/.claude/blob_65cf5acbb1de8197ea42e74e03fb9c2505f2808c.txt new file mode 100644 index 0000000..65cf5ac --- /dev/null +++ b/.claude/blob_65cf5acbb1de8197ea42e74e03fb9c2505f2808c.txt @@ -0,0 +1,1898 @@ +# Claude Code Python 执行计划与任务卡单(2026-04-29) + +## 1. 文档定位 + +- 文档性质:执行计划 + 任务卡单 +- 上游文档:[CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md](D:/Download/gaming/new_program/claude-code-python/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md) +- 使用方式: + - 产品、架构、研发、测试都以本文件作为近期实施清单 + - 每张任务卡必须能独立建分支、独立开发、独立验证、独立合并 + - 若一张卡无法在一个明确边界内完成,则继续向下拆 + +## 2. 当前拆解原则 + +### 2.1 拆卡原则 + +每张卡必须同时回答 5 个问题: + +1. 解决什么问题? +2. 要做什么研发功能? +3. 要做到什么程度才算完成? +4. 测试代码要补到什么程度? +5. 有什么前置依赖和明确非目标? + +### 2.2 完成定义(Definition of Done) + +每张卡默认完成标准: + +- 代码主链可运行 +- 新增或修改测试通过 +- 对应文档同步更新 +- 不引入新的未解释脏状态 +- 至少完成一轮本地手工验收 + +### 2.3 测试分层标准 + +为避免“写了点测试”但没有验收价值,统一采用以下深度分层: + +| 层级 | 名称 | 目的 | 最低要求 | +|---|---|---|---| +| T1 | Unit | 纯函数、状态机、输入输出边界 | 正向 + 负向 + 边界 | +| T2 | Component | 单模块编排、带 fake/mocks 的行为验证 | 至少覆盖 1 条主链 + 1 条异常链 | +| T3 | Integration | 跨模块联动,验证真实依赖边界 | 至少覆盖 1 条端到端业务链 | +| T4 | Regression | 复现历史缺陷,防止回归 | 每个已知 bug 至少 1 条回归用例 | +| T5 | Manual Smoke | 人工快速验收 | 命令级/入口级实际运行 | + +### 2.4 测试产物要求 + +每张代码卡至少满足: + +- 新增或修改 `T1/T2` 测试 +- 若变更影响 CLI、session、task、MCP、hooks、agent 主链,则必须补 `T3` +- 若任务卡源于已知缺陷,则必须补 `T4` +- 输出 1 份最小手工验收命令清单 + +## 3. 实施阶段总览 + +## 3.1 Phase 0:止血与收敛 + +目标: + +- 修掉“看起来有、实际没打通”的假闭环 +- 建立唯一主运行时入口 +- 建立稳定的本地开发/测试施工面 + +## 3.2 Phase 1:平台化 + +目标: + +- 从单进程 CLI 演进为可复用的执行后端 +- 补齐 event journal、state backend、active memory + +## 3.3 Phase 2:超车 + +目标: + +- 建立多 Agent 控制面、IDE 客户端、CI 工作流、组织级治理能力 + +## 4. 依赖顺序图 + +```text +P0-01 Runtime Backbone + -> P0-02 Session Resume + -> P0-03 Auth/Config + -> P0-04 Feature Flags + -> P0-05 Review Command + -> P0-06 Session/History/Memory Boundaries + -> P0-07 Hooks Runtime + -> P0-08 Windows Bootstrap + -> P0-09 Temp Artifact Hygiene + -> P0-10 Phase0 Regression Gate + +P1-01 Daemon/API Control Plane + -> P1-02 Event Journal + -> P1-03 SQLite State Backend + -> P1-04 Active Memory Runtime + -> P1-05 Hook Policy/Audit Convergence + -> P1-06 CLI Thin Client Migration + +P2-01 Agent Supervisor + -> P2-02 Artifact Bus + -> P2-03 IDE Integration + -> P2-04 GitHub/CI Workflow + -> P2-05 Org Policy & Audit + -> P2-06 Linux Unified Verification Gate + -> P2-07 Agent Runtime Parity + -> P2-08 Custom Agents Directory Loader + -> P2-09 Linux Unified Execution Gate + -> P2-10 JetBrains IDE Integration + -> P2-11 Linux Sharded Execution Gate + -> P2-12 Linux Shard Aggregation Gate + -> P2-13 Linux Final Report Publish Gate + -> P2-14 Linux Unified Pipeline Orchestration Gate + -> P2-15 Linux Shard Plan Generation Gate + -> P2-16 Linux CI Matrix Export Gate + -> P2-17 Linux CI Workflow Plan Gate + -> P2-18 Linux CI Workflow YAML Render Gate + -> P2-19 Linux CI Workflow Drift Sync Gate + -> P2-20 Linux CI Workflow Command Guard Gate + -> P2-21 Linux CI Workflow Governance Convergence Gate + -> P2-22 Linux CI Workflow Governance Publish Gate + -> P2-23 Linux CI Workflow Execution Decision Gate + -> P2-24 Linux CI Workflow Dispatch Readiness Gate + -> P2-25 Linux CI Workflow Dispatch Execution Gate + -> P2-26 Linux CI Workflow Full Pipeline Gate +``` + +## 5. Phase 0 任务卡单 + +## P0-01 统一主运行时骨架 + +- 卡号:`P0-01` +- 优先级:`P0` +- 解决问题: + - `main.py`、`app.py`、session/task/hooks/memory 初始化链条分散 + - 子系统存在但未形成唯一运行时闭环 +- 研发功能: + - 建立统一 runtime bootstrap + - 明确 `main -> Application -> QueryEngine -> Session -> TaskManager -> Hooks -> Memory` + - 消除重复初始化路径与隐式全局状态依赖 +- 实施范围: + - `claude_code/main.py` + - `claude_code/app.py` + - `claude_code/engine/query.py` + - 视需要新增 `claude_code/runtime/` 或等价模块 +- 完成程度: + - REPL、single query、pipe、MCP serve 共用同一 bootstrap 流 + - runtime 关键对象有唯一构建路径 + - 后续卡单不再依赖“各自偷偷初始化” +- 测试代码要求: + - `T1`:bootstrap 参数解析、runtime object 构建边界 + - `T2`:不同运行模式下 runtime 组件装配一致性 + - `T3`:REPL / pipe / single query 至少各有 1 条主链 smoke + - `T4`:覆盖“某模式漏初始化某服务”的历史断点 +- 建议测试文件: + - 新增 `tests/test_runtime_bootstrap.py` + - 扩展 `tests/test_main_runtime.py` +- 手工验收: + - `python -m claude_code.main --help` + - `python -m claude_code.main --doctor` + - `echo hi | python -m claude_code.main --pipe` +- 非目标: + - 本卡不引入 daemon/API + - 本卡不处理组织级策略 +- 前置依赖:无 + +## P0-02 /resume 真闭环 + +- 卡号:`P0-02` +- 优先级:`P0` +- 解决问题: + - `QueryEngine.resume_session()` 已存在,但 `/resume` 命令没有真正调用恢复链路 + - session 恢复是“文案功能”,不是“运行时功能” +- 研发功能: + - 将 `/resume [session_id]` 真正接入 `QueryEngine.resume_session()` + - 无参数时列最近 session;有参数时恢复指定 session 并接续对话 + - 明确恢复后的 session id、message list、working directory 行为 +- 实施范围: + - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/repl/__init__.py` +- 完成程度: + - `/resume` 不再只返回说明文字 + - 恢复后下一轮 query 能接着历史消息运行 + - 恢复失败时有可诊断错误信息 +- 测试代码要求: + - `T1`:session list / load 边界 + - `T2`:command -> engine 恢复链路 + - `T3`:创建 session、持久化、恢复、继续问答的一条完整链 + - `T4`:无 session / session_id 不存在 / 会话文件损坏 +- 建议测试文件: + - 扩展 `tests/test_query_engine_runtime.py` + - 扩展 `tests/test_main_runtime.py` + - 新增 `tests/test_resume_runtime.py` +- 手工验收: + - 启动一次会话并生成持久化 + - 新进程执行 `/resume` + - 确认恢复后继续对话 +- 非目标: + - 不引入跨机器恢复 + - 不引入 session 搜索 UI +- 前置依赖:`P0-01` + +## P0-03 auth/config 主链修复与统一 + +- 卡号:`P0-03` +- 优先级:`P0` +- 解决问题: + - `logout` / `auth` 使用未导入的 `ConfigManager` + - `Config` 与 `ConfigManager` 双轨并存,认证状态判定不稳定 +- 研发功能: + - 选定 canonical config 读写路径 + - 修复 `/login`、`/logout`、`/auth` + - 统一 env/config/key masking 规则 +- 实施范围: + - `claude_code/commands/auth/__init__.py` + - `claude_code/config.py` + - `claude_code/utils/config_manager.py` + - 视需要调整 `claude_code/main.py` +- 完成程度: + - `auth` 链路无 `NameError` + - 认证状态输出与实际 env/config 一致 + - logout 能清理 runtime 可见的 key 来源 +- 测试代码要求: + - `T1`:env > config > default 优先级 + - `T2`:auth commands 在不同 key 来源下的行为 + - `T3`:命令级状态变化验证 + - `T4`:无 key / config 缺失 / key mask 输出 +- 建议测试文件: + - 新增 `tests/test_commands_auth_runtime.py` + - 扩展 `tests/test_main_runtime.py` +- 手工验收: + - 设置 env key + - 运行 `/auth` + - 运行 `/logout` + - 再次运行 `/auth` +- 非目标: + - 不引入 OAuth 真登录流 + - 不接第三方 secrets manager +- 前置依赖:`P0-01` + +## P0-04 feature flag 运行时修复 + +- 卡号:`P0-04` +- 优先级:`P0` +- 解决问题: + - `FeatureConfig` 为 frozen dataclass,但 `enable()/disable()` 试图原地修改 + - 当前 flag 机制对未来大功能切换不可靠 +- 研发功能: + - 重构 feature registry 的内部状态表达 + - 保持 env fallback,同时允许程序内安全切换 + - 明确 feature 读取与写入语义 +- 实施范围: + - `claude_code/features.py` +- 完成程度: + - `enable()` / `disable()` 不抛 `FrozenInstanceError` + - registry 内外行为一致 + - flags 可作为后续大卡的 gated rollout 机制 +- 测试代码要求: + - `T1`:enable / disable / env override / unknown feature + - `T2`:registry 实例在多次调用中的状态一致性 + - `T4`:复现当前 `FrozenInstanceError` 的回归用例 +- 建议测试文件: + - 新增 `tests/test_features_runtime.py` +- 手工验收: + - 本地脚本切换 feature 并读取 +- 非目标: + - 不引入远程 flag service +- 前置依赖:无 + +## P0-05 review 命令产品化 + +- 卡号:`P0-05` +- 优先级:`P0` +- 解决问题: + - `/review` 只是引导文案,不执行真实审查 + - 用户无法依赖该命令检查 diff / 文件风险 +- 研发功能: + - 支持 review 当前 git diff + - 支持 review 指定文件 + - 统一 findings 输出格式:问题、位置、严重度、建议 + - 明确“无发现”输出 +- 实施范围: + - `claude_code/commands/review/__init__.py` + - 视需要新增 `claude_code/services/review_service.py` + - 可接入 `git diff` / read-only analyzer +- 完成程度: + - `/review` 不再返回占位 prompt + - 能对当前改动或输入文件给出结构化结果 + - 输出可作为后续 PR/CI review 的雏形 +- 测试代码要求: + - `T1`:参数解析、无输入默认行为 + - `T2`:service 对 fake diff 的 findings 生成 + - `T3`:临时 git repo 中的 review 主链 + - `T4`:无 diff / 文件不存在 / 空结果 +- 建议测试文件: + - 新增 `tests/test_review_command_runtime.py` +- 手工验收: + - 修改一个文件后执行 `/review` + - 指定文件执行 `/review path` +- 非目标: + - 不做组织级 code review dashboard + - 不强行接 GitHub API +- 前置依赖:`P0-01` + +## P0-06 session/history/memory 职责收敛 + +- 卡号:`P0-06` +- 优先级:`P0` +- 解决问题: + - `Session`、`HistoryManager`、`SessionMemory` 职责交叉 + - 短期对话、长期记忆、归档历史边界不清 +- 研发功能: + - 明确三层职责: + - `Session`:当前会话事实 + - `History`:已结束/已保存的消息归档 + - `Memory`:长期结构化知识 + - 消除重复保存与不一致加载路径 + - 为 P1 active memory 铺底 +- 实施范围: + - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` +- 完成程度: + - 三者职责与调用边界写清 + - 当前主链只保留一个 message persistence source of truth + - memory 不再隐式承担 session/history 角色 +- 测试代码要求: + - `T1`:save/load/export/search 边界 + - `T2`:session -> persisted session 行为 + - `T3`:context builder 读取 memory/session 的联动 + - `T4`:防止重复消息、重复持久化、错误恢复 +- 建议测试文件: + - 新增 `tests/test_session_history_memory_runtime.py` + - 扩展 `tests/test_context_builder_runtime.py` +- 手工验收: + - 生成会话 + - 重启加载 + - 验证 memory 不被误写为聊天历史 +- 非目标: + - 不做向量数据库 + - 不做长期检索增强 +- 前置依赖:`P0-01` + +## P0-07 hooks 接入主运行时 + +- 卡号:`P0-07` +- 优先级:`P0` +- 解决问题: + - hooks command、hooks manager、tool/command 执行链没有真正联动 + - hooks 目前更像静态配置,而不是运行时能力 +- 研发功能: + - 在 tool execution 前后触发 hooks + - 在 command 执行前后触发 hooks + - 在错误路径触发 `on_error` + - 约定 hook context payload +- 实施范围: + - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - 视需要接入 command dispatch 层 +- 完成程度: + - hooks 配置能真实生效 + - disabled hook、超时 hook、失败 hook 行为清晰 + - 对主链失败不产生不可控副作用 +- 测试代码要求: + - `T1`:hook load/save/parse + - `T2`:trigger before_tool / after_tool / on_error + - `T3`:真实 tool 调用链触发 hook + - `T4`:hook timeout / hook command failure / disabled hook +- 建议测试文件: + - 新增 `tests/test_hooks_runtime.py` +- 手工验收: + - 配置一个简单 echo hook + - 执行 read / grep / bash + - 确认 hook 被触发 +- 非目标: + - 不做 org-wide hook registry +- 前置依赖:`P0-01` + +## P0-08 Windows 启动与解释器探测稳定化 + +- 卡号:`P0-08` +- 优先级:`P1` +- 解决问题: + - 系统 `python` 指向 WindowsApps stub,用户容易跑到假解释器 + - Windows 用户启动链脆弱 +- 研发功能: + - 在 `--doctor` 中识别解释器来源 + - 对 WindowsApps stub 给出明确警告 + - 统一推荐启动器 / wrapper 行为 + - 文档同步明确 Windows 推荐运行法 +- 实施范围: + - `claude_code/main.py` + - `README.md` + - 可选新增 `scripts/` 下辅助启动脚本 +- 完成程度: + - doctor 输出能诊断解释器风险 + - Windows 用户不会误以为 stub 可用 + - 项目入口文档清楚标注推荐解释器 +- 测试代码要求: + - `T1`:解释器路径识别逻辑 + - `T2`:doctor 输出分支 + - `T4`:WindowsApps stub 回归用例 +- 建议测试文件: + - 新增 `tests/test_doctor_runtime.py` +- 手工验收: + - 模拟 / 检查 doctor 输出 +- 非目标: + - 不做完整安装器 +- 前置依赖:`P0-01` + +## P0-09 测试临时产物清理与隔离 + +- 卡号:`P0-09` +- 优先级:`P1` +- 解决问题: + - `tests/.tmp_plugins/` 存在残留目录 + - 测试隔离性与仓库整洁性不足 +- 研发功能: + - 修复 plugin 测试的清理逻辑 + - 增补 `.gitignore` 或测试 teardown + - 确保 pytest 结束后不新增脏目录 +- 实施范围: + - `tests/test_plugins_runtime.py` + - `.gitignore` + - 视需要调整 plugin fixture +- 完成程度: + - 本地完整 pytest 后不再残留新增 `.tmp_*` / `.tmp_plugins/*` + - 测试互不污染 +- 测试代码要求: + - `T2`:fixture cleanup 断言 + - `T4`:重复运行 pytest 不累计残留 +- 建议测试文件: + - 扩展 `tests/test_plugins_runtime.py` +- 手工验收: + - 连续跑两次 pytest + - 检查工作区脏文件 +- 非目标: + - 不清理用户真实运行数据 +- 前置依赖:无 + +## P0-10 Phase 0 回归门禁 + +- 卡号:`P0-10` +- 优先级:`P0` +- 解决问题: + - 现有测试分布较散,没有围绕 Phase 0 主链建立明确门禁 +- 研发功能: + - 定义一组必须通过的 runtime regression tests + - 增加按主链分组的 pytest marker 或脚本入口 + - 明确提交前必跑命令 +- 实施范围: + - `pytest.ini` + - `scripts/` 下校验脚本(如需要) + - 测试文件标记整理 +- 完成程度: + - 能一条命令执行 P0 主链回归 + - 团队知道“开工前/合并前必须跑什么” +- 测试代码要求: + - 本卡本质上是测试组织卡 + - 需要补 1 个测试入口脚本或 marker 验证 +- 建议测试文件: + - 如需新增:`tests/test_phase0_regression_contract.py` +- 手工验收: + - 执行一条 phase0 gate 命令并验证通过 +- 非目标: + - 不要求此时接 CI SaaS +- 前置依赖: + - `P0-02` ~ `P0-09` + +## 6. Phase 1 任务卡单 + +## P1-01 daemon / API 控制面 + +- 卡号:`P1-01` +- 优先级:`P1` +- 解决问题: + - 当前 CLI 是厚客户端,IDE/desktop/web/CI 无法共用执行后端 +- 研发功能: + - 提供本地 daemon/API 层 + - 抽离 session、task、tool execution、artifact 访问接口 + - CLI 改为 daemon client 的上层入口 +- 实施范围: + - 新增 `claude_code/server/` 或等价控制面模块 + - 调整 `main.py` + - 复用 `app.py` +- 完成程度: + - 单机可启动 daemon + - CLI 与 daemon 可共享一套执行核心 +- 测试代码要求: + - `T1`:API schema / request validation + - `T2`:server service 层行为 + - `T3`:client -> server -> runtime 一条完整链 + - `T4`:daemon 未启动 / 超时 / 会话不存在 +- 前置依赖:`P0-01`、`P0-10` + +## P1-02 event journal 事件日志 + +- 卡号:`P1-02` +- 优先级:`P1` +- 解决问题: + - prompt、tool、task、permission 决策不可回放,不可审计 +- 研发功能: + - 定义统一事件模型 + - 写入 prompt/message/tool/task/permission/artifact 事件 + - 提供查询与 replay 基础能力 +- 实施范围: + - 新增 `claude_code/services/event_journal.py` 或等价模块 + - 接入 `QueryEngine`、`TaskManager` +- 完成程度: + - 所有关键主链动作可产生结构化事件 + - 支持按 session 查询事件流 +- 测试代码要求: + - `T1`:事件 schema、序列化、versioning + - `T2`:writer/reader correctness + - `T3`:query -> tool -> task 事件链 + - `T4`:部分写入失败、乱序恢复 +- 前置依赖:`P1-01` + +## P1-03 SQLite runtime state backend + +- 卡号:`P1-03` +- 优先级:`P1` +- 解决问题: + - file/memory backend 对恢复、检索、多实例前准备不足 +- 研发功能: + - 为 session/task/event 提供 SQLite backend + - 保留 file backend 作为兼容层或 fallback + - 明确 migration 与 schema version +- 实施范围: + - `claude_code/tasks/factory.py` + - `claude_code/tasks/repository.py` + - session/event 相关存储模块 +- 完成程度: + - 至少可用 SQLite 持久化 task/session/event + - 单机重启后可恢复核心运行态 +- 测试代码要求: + - `T1`:schema init / migration + - `T2`:repository CRUD contract + - `T3`:重启恢复 + - `T4`:数据库损坏、锁冲突、重复初始化 +- 前置依赖:`P1-02` + +## P1-04 active memory 接入运行时 + +- 卡号:`P1-04` +- 优先级:`P1` +- 解决问题: + - `SessionMemory` 现在是孤立 KV,不影响主对话上下文 +- 研发功能: + - 让 memory 与 context builder/runtime 联动 + - 定义 memory scope:user/project/local + - 支持显式写入、按需注入、命中统计 +- 实施范围: + - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` + - `claude_code/agents/builtin.py` +- 完成程度: + - memory 成为运行时真实输入,而非旁路存储 + - agent / main query 可选择不同 memory scope +- 测试代码要求: + - `T1`:namespace / key / search 行为 + - `T2`:context builder memory 注入 + - `T3`:query 执行时命中 memory + - `T4`:空 memory / 损坏 memory / scope 隔离 +- 前置依赖:`P0-06`、`P1-02` + +## P1-05 hooks / permission / audit 收敛 + +- 卡号:`P1-05` +- 优先级:`P1` +- 解决问题: + - hooks、permissions、event journal 仍是分立能力 +- 研发功能: + - 统一 hook 事件、permission decision、audit event + - 形成一次执行的完整审计链 + - 为未来 org policy 铺底 +- 实施范围: + - `claude_code/permissions.py` + - `claude_code/services/hooks_manager.py` + - event journal 相关模块 +- 完成程度: + - 每次 tool call 至少具备 request -> decision -> hook -> result 的闭环记录 +- 测试代码要求: + - `T2`:permission + hook + event 联动 + - `T3`:真实 tool 执行审计链 + - `T4`:deny / timeout / partial failure +- 前置依赖:`P0-07`、`P1-02` + +## P1-06 CLI thin client 迁移 + +- 卡号:`P1-06` +- 优先级:`P1` +- 解决问题: + - CLI 与控制面耦合过深,不利于 IDE/CI 共用 +- 研发功能: + - CLI 发请求给本地 runtime server/daemon + - 保留单进程 fallback 模式 + - 统一输出与错误语义 +- 实施范围: + - `claude_code/main.py` + - CLI/REPL client adapter +- 完成程度: + - CLI 作为 runtime 前端存在,而非执行内核本身 +- 测试代码要求: + - `T2`:client adapter 行为 + - `T3`:CLI -> daemon 完整主链 + - `T4`:daemon 不可用 fallback +- 前置依赖:`P1-01` + +## 7. Phase 2 任务卡单 + +## P2-01 多 Agent supervisor + +- 卡号:`P2-01` +- 优先级:`P2` +- 解决问题: + - 当前 agent 更像“单次子任务执行器”,不是控制面 +- 研发功能: + - agent DAG + - budget / timeout / retry / ownership + - parent-child artifact passing +- 完成程度: + - 至少支持并行 agent + 汇总收口 +- 测试代码要求: + - `T1`:graph / budget / timeout 状态机 + - `T2`:supervisor 调度 + - `T3`:并行 agent 执行 + - `T4`:子 agent 失败恢复 +- 前置依赖:`P1-01`、`P1-02` + +## P2-02 artifact bus 与冲突收敛 + +- 卡号:`P2-02` +- 优先级:`P2` +- 解决问题: + - 多 agent 输出缺少结构化 artifact 交换层 +- 研发功能: + - 统一 artifact schema + - patch / note / diff / report / finding 流转 + - 冲突检测与合并策略 +- 测试代码要求: + - `T1`:artifact schema + - `T2`:producer/consumer + - `T3`:多 agent artifact 汇流 + - `T4`:冲突合并失败 +- 前置依赖:`P2-01` + +## P2-03 IDE 集成(VS Code first) + +- 卡号:`P2-03` +- 优先级:`P2` +- 解决问题: + - 当前仓库缺少真正可用的 IDE workflow +- 研发功能: + - VS Code client + - daemon 连接 + - diff、task、session、findings 展示 +- 测试代码要求: + - `T2`:client protocol adapter + - `T3`:IDE -> daemon 核心链 + - `T5`:人工交互验收为主 +- 前置依赖:`P1-06` + +## P2-04 GitHub / CI 工作流 agent + +- 卡号:`P2-04` +- 优先级:`P2` +- 解决问题: + - 当前能力主要停留在本地 CLI,无法进入团队协作闭环 +- 研发功能: + - issue -> plan -> code -> test -> review -> PR 工作流 + - CI 环境的 headless execution 模式 +- 测试代码要求: + - `T2`:workflow state machine + - `T3`:fake repo / fake PR 主链 + - `T4`:权限不足 / repo 状态不干净 / 网络失败 +- 前置依赖:`P1-01`、`P1-02` + +## P2-05 组织级策略与审计面板 + +- 卡号:`P2-05` +- 优先级:`P2` +- 解决问题: + - 团队场景下缺乏治理、审批、审计、安全边界 +- 研发功能: + - policy model + - approval queue + - secret redaction + - audit query / reporting +- 测试代码要求: + - `T1`:policy matcher / redaction rules + - `T2`:approval state transitions + - `T3`:执行审计链闭环 + - `T4`:策略冲突 / 未审批执行阻断 +- 前置依赖:`P1-05` + +## P2-06 Linux 统一验尸门禁 + +- 卡号:`P2-06` +- 优先级:`P2` +- 解决问题: + - 当前每张卡各有 gate 脚本,但缺少一次性统一执行清单,不利于 Linux 阶段全量验尸 +- 研发功能: + - 聚合 `P0-01` 到 `P2-05` 全链路关键测试脚本清单 + - 提供 Linux 统一执行入口脚本 + - 建立 unified gate manifest 的 contract test,避免清单漂移 +- 测试代码要求: + - `T1`:manifest 存在性与文件映射校验 + - `T2`:命令生成顺序与格式契约 + - `T3`:Linux 阶段按清单逐条执行(本地不执行) + - `T4`:新增/删减测试文件时契约回归提示 +- 前置依赖:`P2-05` + +## P2-07 Agent 运行时一致性(sync/background parity) + +- 卡号:`P2-07` +- 优先级:`P2` +- 解决问题: + - `AgentTool` 的同步与后台路径在 runtime context 继承上存在漂移风险 + - 后台 agent 任务缺少 session/conversation 等审计元数据,不利于回放与治理 +- 研发功能: + - 对齐 sync/background 子 agent 在 `working_directory`、`tool_registry`、`permission_mode`、`provider/model`、`memory_scope` 的行为 + - 将 `session_id`、`conversation_id`、runtime context 字段写入后台任务 metadata + - 让后台路径优先复用上层 runtime `TaskManager`,避免全局单例漂移 +- 实施范围: + - `claude_code/tools/base.py` + - `claude_code/engine/query.py` + - `claude_code/tools/agent/__init__.py` + - `claude_code/tasks/manager.py` +- 测试代码要求: + - `T2`:sync/background context 继承一致性 + - `T3`:后台任务 metadata 审计字段完整性 + - `T4`:provider/model 继承回归(本地 provider 与 Claude model alias 差异) +- 前置依赖:`P2-01`、`P2-06` + +## P2-08 自定义 Agent 目录加载闭环(.claude/agents) + +- 卡号:`P2-08` +- 优先级:`P2` +- 解决问题: + - Python 版本 `AgentTool` 只支持内置 `BUILTIN_AGENTS`,未形成 `.claude/agents/*.md` 的加载闭环 + - `subagent_type` 找不到时可观测性不足,无法同时看到 custom + builtin 可用类型 +- 研发功能: + - 新增 `.claude/agents/*.md` loader,支持 YAML frontmatter + prompt body 解析 + - 支持从当前目录向上搜索到 git 根的多级 `.claude/agents`,并按“近路径覆盖远路径”合并 + - 在 `AgentTool` 中接入 custom agent lazy-load,优先级 `custom > builtin` + - `Unknown agent type` 错误提示输出联合可用 agent 列表 +- 实施范围: + - `claude_code/agents/loader.py` + - `claude_code/tools/agent/__init__.py` + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` +- 测试代码要求: + - `T1`:frontmatter 解析与字段归一化(tools/disallowedTools/memory/maxTurns) + - `T2`:多级目录覆盖策略(root -> nested) + - `T3`:AgentTool custom 覆盖 builtin 解析链 + - `T4`:invalid YAML / 缺字段容错 + unknown type 联合可用清单 +- 前置依赖:`P2-07` + +## P2-09 Linux 统一验尸执行门禁 +- 卡号:`P2-09` +- 优先级:`P2` +- 解决问题: + - `P2-06` 当前只输出命令清单,不直接提供 Linux 阶段的一键执行、报告沉淀与失败策略控制。 + - 统一验尸链缺少标准化产物(junit xml + summary json),不利于 CI 汇总与回放。 +- 研发功能: + - 新增 Linux 执行门禁脚本,复用 `run_linux_unified_gate.py` manifest。 + - 提供 `--dry-run` / `--print-commands` / `--continue-on-failure` / `--report-dir` 能力。 + - 顺序执行每条 pytest 命令并产出每条用例的 `--junitxml`,最终写入统一 `summary.json`。 +- 实施范围: + - `scripts/run_p2_linux_unified_execution_gate.py` + - `scripts/run_linux_unified_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` +- 测试代码要求: + - `T1`:执行门禁命令生成严格跟随 unified manifest 顺序。 + - `T2`:额外 pytest 参数透传契约与 junit 路径契约。 + - `T3`:Linux 阶段执行入口可 dry-run,不在本地运行测试。 + - `T4`:manifest 变更后执行门禁契约测试可及时拦截漂移。 +- 前置依赖:`P2-06`、`P2-08` + +## P2-10 JetBrains IDE 集成(IntelliJ / PyCharm) +- 卡号:`P2-10` +- 优先级:`P2` +- 解决问题: + - `P2-03` 仅覆盖 `VS Code first` 适配,JetBrains 生态缺少标准化快照适配层。 + - 现有 IDE payload 虽可复用,但缺少 JetBrains 侧可直接消费的 inspection 结构映射。 +- 研发功能: + - 在 `ide_adapter` 增加 `JetBrainsClientAdapter`。 + - 将 daemon `workspace.findings` 映射为 JetBrains inspection 结构(severity -> highlight)。 + - 保持 daemon API 不变,适配层完成“同 payload,多 IDE 语义”收敛。 +- 实施范围: + - `claude_code/server/ide_adapter.py` + - `claude_code/server/__init__.py` + - `tests/test_p2_jetbrains_ide_integration_runtime.py` + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` +- 测试代码要求: + - `T1`:JetBrains adapter 协议归一化(project_root/vcs/sessions/tasks)。 + - `T2`:findings -> inspections 字段映射与 severity/highlight 契约。 + - `T3`:保持 daemon `ide/workspace` payload 兼容,不新增路由。 + - `T4`:非法 line/severity 输入容错。 +- 前置依赖:`P2-03`、`P2-09` + +## P2-11 Linux 分片并行验尸门禁 +- 卡号:`P2-11` +- 优先级:`P2` +- 解决问题: + - `P2-09` 仅支持单进程顺序执行,Linux 阶段无法直接按 CI job 分片并行,整体回归耗时高。 + - 缺少分片级 summary 元数据,不利于多 job 回收阶段做统一聚合。 +- 研发功能: + - 在 `run_p2_linux_unified_execution_gate.py` 增加 `--shard-total` / `--shard-index`。 + - 按 unified manifest 顺序做确定性分片选择(同一输入始终稳定切片)。 + - summary 增加 `shard_total` / `shard_index` / `manifest_total_tests` 字段。 + - 保持 `--dry-run` / `--print-commands` 路径可见当前分片覆盖范围。 +- 实施范围: + - `scripts/run_p2_linux_unified_execution_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:分片选择与 manifest 顺序契约(deterministic slicing)。 + - `T2`:非法分片参数拒绝契约(`shard_total < 1` / `shard_index` 越界)。 + - `T3`:summary 分片元数据契约。 + - `T4`:额外 pytest 参数透传、junit 路径契约继续保持。 +- 前置依赖:`P2-09` + +## P2-12 Linux 分片验尸汇总门禁 +- 卡号:`P2-12` +- 优先级:`P2` +- 解决问题: + - `P2-11` 已支持分片并行执行,但缺少 Linux 回收阶段的统一汇总入口,无法给出全局通过/失败判定。 + - 多个 shard 的 `summary.json` 缺少一致性校验(缺片/重复片/manifest 漂移),容易让 CI 误判。 +- 研发功能: + - 新增分片汇总门禁脚本,支持 `--summary` / `--summary-glob` / `--artifacts-dir` 自动发现 summary 文件。 + - 合并 shard 结果并输出 `merged_summary.json`,沉淀 `overall_status`、`structural_issues`、`shards_missing` 与全局 totals。 + - 对关键结构做契约校验:字段完整性、`shard_index` 范围、`passed+failed<=total_tests`、manifest/分片总数一致性。 +- 实施范围: + - `scripts/run_p2_linux_shard_aggregation_gate.py` + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:summary 发现策略去重契约(direct + glob + artifacts-dir)。 + - `T2`:summary 基础字段校验与类型约束契约。 + - `T3`:全分片覆盖时的聚合 totals 与 passed 状态契约。 + - `T4`:缺片/总数漂移时的 structural issue 与 failed 状态契约。 +- 前置依赖:`P2-11` + +## P2-13 Linux 最终报告发布门禁 +- 卡号:`P2-13` +- 优先级:`P2` +- 解决问题: + - `P2-12` 仅输出 `merged_summary.json`,缺少 Linux 回收阶段可直接归档与展示的最终报告产物。 + - 对 `overall_status` 缺少“再计算校验”与发布契约,CI 侧难以稳定消费最终结论。 +- 研发功能: + - 新增最终报告发布门禁脚本,读取 `merged_summary.json` 并做字段契约校验。 + - 输出 `final_report.json` 与 `final_report.md`(状态、分片、totals、structural issues、notes)。 + - 对 `overall_status` 执行 computed-vs-reported 一致性复核,不一致时强制失败。 +- 实施范围: + - `scripts/run_p2_linux_report_publish_gate.py` + - `tests/test_p2_linux_report_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:merged summary 必填字段与类型契约校验。 + - `T2`:happy path 下最终状态与 totals 发布契约。 + - `T3`:reported status 与 computed status 不一致时失败契约。 + - `T4`:Markdown 报告结构契约(Totals/Shards/Structural Issues/Notes)。 +- 前置依赖:`P2-12` + +## P2-14 Linux 统一验尸全链编排门禁 +- 卡号:`P2-14` +- 优先级:`P2` +- 解决问题: + - `P2-09` / `P2-12` / `P2-13` 已拆分为多阶段脚本,但缺少一条 Linux 一键串联执行入口。 + - 多阶段手工拼装命令易漂移,失败策略与参数透传在 CI 侧难统一。 +- 研发功能: + - 新增 pipeline gate,按 `execution -> aggregation -> publish` 编排执行。 + - 支持 `--skip-execution` / `--skip-aggregation` / `--skip-publish`,覆盖单机与多 job 回收场景。 + - 支持 `--print-commands` / `--dry-run` 产出可审计命令清单,支持 `--fail-fast` 失败控制。 + - 支持 execution 阶段 pytest 额外参数透传(`--` 转发到 `P2-09`)。 +- 实施范围: + - `scripts/run_p2_linux_unified_pipeline_gate.py` + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:默认三阶段命令生成契约(execution summary 自动注入 aggregation)。 + - `T2`:`skip-execution` 下外部 summary/glob/artifacts 参数透传契约。 + - `T3`:全跳过模式返回空 pipeline 契约。 + - `T4`:pytest 额外参数透传契约。 +- 前置依赖:`P2-13` + +## P2-15 Linux 分片执行计划生成门禁 +- 卡号:`P2-15` +- 优先级:`P2` +- 解决问题: + - `P2-11`/`P2-14` 已支持分片执行与全链编排,但 Linux CI fan-out job 仍需手工拼接 `shard_index` 命令,容易漂移。 + - 缺少“执行前”计划产物,CI 回收阶段难以对照校验分片覆盖是否完整。 +- 研发功能: + - 新增 shard plan gate,基于 unified manifest 生成确定性 `shard_plan.json`。 + - 每个分片输出:`test_files`、`report_dir`、`summary_path`、`execution command`、`command_parts`。 + - 支持 `--print-commands`/`--dry-run` 用于 CI 编排探测;支持 pytest 额外参数透传(`--`)。 + - 对 `shard_total` 与 manifest 文件存在性做前置契约校验,非法输入直接失败。 +- 实施范围: + - `scripts/run_p2_linux_shard_plan_gate.py` + - `tests/test_p2_linux_shard_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:分片切片确定性契约(与 `P2-11` slicing 一致)。 + - `T2`:分片执行命令与 pytest 参数透传契约。 + - `T3`:非法 `shard_total` 拒绝契约。 + - `T4`:输出 `summary_path/report_dir` 字段契约。 +- 前置依赖:`P2-14` + +## P2-16 Linux CI Matrix 导出门禁 +- 卡号:`P2-16` +- 优先级:`P2` +- 解决问题: + - `P2-15` 已产出 `shard_plan.json`,但 Linux CI 仍需手工把分片计划转为 matrix 变量,易产生编排漂移。 + - fan-out 与 fan-in 之间缺少统一的 `summary_paths` 回收清单,导致回收阶段参数拼装不稳定。 +- 研发功能: + - 新增 CI matrix gate,从 `shard_plan.json` 生成可直接消费的 `ci_matrix.json`。 + - 输出标准 `matrix.include` 条目(`shard_index/shard_total/command/command_parts/report_dir/summary_path`)。 + - 输出 `summary_paths` 回收清单,供 `P2-12` 聚合阶段直接使用。 + - 支持 `--github-output` 写入 GitHub Actions output 文件;支持 `--skip-empty-shards` 过滤空分片。 +- 实施范围: + - `scripts/run_p2_linux_ci_matrix_gate.py` + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:matrix include 顺序与字段契约。 + - `T2`:`skip-empty-shards` 过滤契约。 + - `T3`:GitHub output 字段生成契约(matrix/summary_paths/selected_shards)。 + - `T4`:`manifest_total_tests` 与分片 totals 不一致时拒绝契约。 +- 前置依赖:`P2-15` + +## P2-17 Linux CI Workflow 计划门禁 +- 卡号:`P2-17` +- 优先级:`P2` +- 解决问题: + - `P2-16` 已产出 `ci_matrix.json`,但 fan-out/fan-in CI job 仍需人工拼接回收命令,发布链存在手工漂移风险。 + - 缺少统一 workflow 计划产物,CI 编排侧无法直接消费“分片执行 + 汇总 + 发布”全链命令。 +- 研发功能: + - 新增 CI workflow plan gate,从 `ci_matrix.json` 生成 `ci_workflow_plan.json`。 + - 输出 `fan_out_jobs` 与 `fan_out_matrix.include`(含 shard job command/command_parts/report/summary 信息)。 + - 输出 `fan_in` 汇总与发布命令(aggregation/publish command + command_parts),直接对接 `P2-12`/`P2-13`。 + - 支持 `--github-output` 写入 GitHub Actions outputs(`fan_out_matrix`、`fan_in_summary_paths`、`aggregation_command`、`publish_command` 等)。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_plan_gate.py` + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:workflow payload 中 fan-out/fan-in 字段与命令契约。 + - `T2`:GitHub output 字段生成契约。 + - `T3`:`summary_paths` 与 `matrix.include` 顺序不一致时拒绝契约。 + - `T4`:`selected_shards` 与 include 数量不一致时拒绝契约。 +- 前置依赖:`P2-16` + +## P2-18 Linux CI Workflow YAML 渲染门禁 +- 卡号:`P2-18` +- 优先级:`P2` +- 解决问题: + - `P2-17` 已产出 `ci_workflow_plan.json`,但 CI 平台仍需人工编写/维护 workflow YAML,fan-out/fan-in 命令易漂移。 + - 缺少“计划产物 -> 可执行 workflow 文件”的最后一跳,导致门禁链仍存在手工介入点。 +- 研发功能: + - 新增 CI workflow YAML gate,从 `ci_workflow_plan.json` 渲染 `.github/workflows/linux_unified_gate.yml`。 + - 渲染 fan-out matrix job(分片执行 + summary artifact 上传)与 fan-in job(artifact 回收 + 聚合 + 发布 + 最终报告上传)。 + - 输出渲染元数据 `ci_workflow_render.json`,记录 source plan、fan-in 命令、报告路径与分片统计,供审计追踪。 + - 支持 `--dry-run`/`--print-yaml` 快速验看渲染结果,不在本地触发测试执行。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:workflow YAML fan-out/fan-in 字段与命令渲染契约。 + - `T2`:render metadata 字段契约(source/workflow path/summary stats)。 + - `T3`:`fan_in.summary_paths` 与 `fan_out_matrix.include[*].summary_path` 顺序不一致时拒绝契约。 + - `T4`:`selected_shards` 与 include 数量不一致时拒绝契约。 +- 前置依赖:`P2-17` + +## P2-19 Linux CI Workflow 漂移校验门禁 +- 卡号:`P2-19` +- 优先级:`P2` +- 解决问题: + - `P2-18` 已能渲染 workflow YAML 与 metadata,但仓库中的 `.github/workflows/linux_unified_gate.yml` 与 `ci_workflow_render.json` 可能被手工改动后与 `ci_workflow_plan.json` 脱节。 + - 缺少“计划产物 -> 渲染产物一致性校验”门禁,CI 可能在漂移状态下运行,导致 fan-out/fan-in 命令与报告路径失真。 +- 研发功能: + - 新增 workflow sync gate,基于 `ci_workflow_plan.json` 复算期望 workflow YAML 与 metadata,并与当前产物做一致性比对。 + - 输出 drift 摘要(workflow_drift/metadata_drift/drift_count),支持 `--print-diff` 打印 unified diff 便于审计。 + - 支持 `--write` 在本地或 CI 里自动回写标准产物,收敛人工漂移。 + - 支持 `--strict-generated-at` 开启 metadata `generated_at` 严格比对;默认忽略该字段仅校验结构与核心内容。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_sync_gate.py` + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:当 workflow/metadata 缺失时必须触发 drift 判定。 + - `T2`:当 workflow 内容被改写时,必须输出 drift 与可读 diff 契约。 + - `T3`:默认模式忽略 metadata `generated_at` 漂移,避免时间戳噪声误报。 + - `T4`:开启 strict 模式时,`generated_at` 漂移必须触发拒绝契约。 +- 前置依赖:`P2-18` + +## P2-20 Linux CI Workflow 命令护栏门禁 +- 卡号:`P2-20` +- 优先级:`P2` +- 解决问题: + - `P2-19` 已能校验 plan->yaml/metadata 漂移,但尚未约束 `ci_workflow_plan.json` 内 `command` 与 `command_parts` 的一致性,存在“字符串被改、parts 未改”导致 CI 实际执行偏移的风险。 + - fan-out 与 fan-in 命令中的关键路径(`report_dir`、`summary_paths`、`merged_summary/final_report`)缺少强契约绑定,可能在手工编辑后引入静默错误。 +- 研发功能: + - 新增 command guard gate,校验 `command`/`command_parts` 可逆一致性(`shlex.split(command)` == `command_parts`)与 canonical shell 规范化输出。 + - 校验脚本目标与关键 flag 契约: + - fan-out 必须指向 `run_p2_linux_unified_execution_gate.py`,且携带 `--report-dir/--shard-total/--shard-index`。 + - fan-in aggregation 必须指向 `run_p2_linux_shard_aggregation_gate.py`,且 `--summary` 顺序严格匹配 `fan_in.summary_paths`。 + - fan-in publish 必须指向 `run_p2_linux_report_publish_gate.py`,且输出路径与 `fan_in` 字段一致。 + - 支持 `--write` 回写 canonical command 字符串,收敛计划文件命令格式漂移。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:合法 workflow plan 必须通过 guard,`failed_commands=0`。 + - `T2`:`command`/`command_parts` 不一致时必须触发拒绝,并给出规范化命令。 + - `T3`:aggregation 的 `--summary` 顺序与 `fan_in.summary_paths` 不一致时必须拒绝。 + - `T4`:publish 输出路径与 `fan_in.final_report_*` 不一致时必须拒绝。 +- 前置依赖:`P2-19` + +## P2-21 Linux CI Workflow 治理收敛门禁 +- 卡号:`P2-21` +- 优先级:`P2` +- 解决问题: + - `P2-19` 与 `P2-20` 已分别覆盖“渲染漂移”和“命令契约”,但缺少统一治理结论,CI 侧仍需手工拼接多份报告判断是否可执行 workflow。 + - 缺少 metadata 血缘一致性门禁(`source_ci_workflow_plan` 与 `workflow_output_path`),存在“产物看似通过、来源已错位”的隐性风险。 +- 研发功能: + - 新增 governance gate,一次执行汇总 `workflow_sync + command_guard + metadata lineage` 三类检查,产出单一 `overall_status`。 + - 输出 `failed_checks` 明确失败类别:`workflow_sync_drift` / `command_guard_failed` / `lineage_mismatch`。 + - 将 `command_guard` 摘要化为 `failed_commands/issue_count/normalization_required`,便于 CI 聚合展示。 + - 对 metadata 血缘做强校验:`source_ci_workflow_plan` 与 `workflow_output_path` 必须与当前门禁输入路径一致。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_governance_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:workflow/command/lineage 全部一致时必须通过,`overall_status=passed`。 + - `T2`:workflow YAML 漂移时必须落入 `workflow_sync_drift` 失败类。 + - `T3`:command 契约异常时必须落入 `command_guard_failed` 失败类。 + - `T4`:metadata 血缘字段与输入路径不一致时必须落入 `lineage_mismatch` 失败类。 +- 前置依赖:`P2-20` + +## P2-22 Linux CI Workflow 治理发布门禁 +- 卡号:`P2-22` +- 优先级:`P2` +- 解决问题: + - `P2-21` 已产出统一治理结论,但 CI 下游仍需手工解析 `ci_workflow_governance.json` 才能决定“是否执行 workflow”。 + - 缺少固定的发布产物与 GitHub output 契约,导致 fan-in 阶段难以稳定消费治理结果与失败类别。 +- 研发功能: + - 新增 governance publish gate,读取 `P2-21` 报告后生成可消费发布产物: + - `ci_workflow_governance_publish.json` + - `ci_workflow_governance_publish.md` + - 输出稳定决策字段:`overall_status`、`should_execute_workflow`、`failed_checks`、`failed_check_count`。 + - 支持 `--github-output` 导出 CI 变量:`governance_status`、`governance_should_execute_workflow`、`governance_failed_checks` 等。 + - 增加发布侧一致性校验:若 `failed_checks` 或 `overall_status` 与重算结果不一致,必须落入 `structural_issues` 并判定失败。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:治理报告全量一致时应输出 `overall_status=passed` 且 `should_execute_workflow=true`。 + - `T2`:当治理报告包含失败检查时,应输出 `overall_status=failed` 且 `should_execute_workflow=false`。 + - `T3`:当治理报告的 `failed_checks` 与重算结果不一致时,必须触发 `structural_issues` 并失败。 + - `T4`:GitHub output 导出字段必须包含状态、失败列表与发布产物路径。 +- 前置依赖:`P2-21` + +## P2-23 Linux CI Workflow 执行决策门禁 +- 卡号:`P2-23` +- 优先级:`P2` +- 解决问题: + - `P2-22` 虽已产出 `should_execute_workflow`,但 CI 下游仍缺“统一执行闸门”将治理结果转成明确的 execute/blocked 结果与退出码语义。 + - 缺少对执行前关键产物(workflow plan / rendered workflow)的存在性校验,可能出现“治理通过但执行入口缺失”的断链。 +- 研发功能: + - 新增 execution decision gate,读取 `P2-22` 发布结果,统一输出: + - `ci_workflow_execution_decision.json` + - `ci_workflow_execution_decision.md` + - 固化决策字段:`decision`、`decision_status`、`should_execute_workflow`、`exit_code`、`reason_codes`、`missing_artifacts`。 + - 支持阻断策略开关:`--on-block fail|skip`,让 CI 可在阻断时选择 fail-fast 或 soft-skip。 + - 支持 `--github-output` 输出下游变量,减少 job 间手工解析。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_decision_gate.py` + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:治理通过且执行产物齐备时必须输出 `decision=execute`、`exit_code=0`。 + - `T2`:治理失败时必须输出 `decision=blocked` 且携带失败原因码。 + - `T3`:阻断策略为 `skip` 时,阻断决策应返回 `exit_code=0`。 + - `T4`:GitHub output 契约需包含 decision/status/should_execute/exit_code 与报告路径。 +- 前置依赖:`P2-22` + +## P2-24 Linux CI Workflow 调度就绪门禁 +- 卡号:`P2-24` +- 优先级:`P2` +- 解决问题: + - `P2-23` 已产出 execute/blocked 决策,但 CI 下游仍需手工将决策字段拼装为实际调度命令,存在命令不一致与错误触发风险。 + - 缺少统一 dispatch 契约报告,导致“是否可发起 `gh workflow run`”的判断无法被 job 间稳定复用。 +- 研发功能: + - 新增 dispatch readiness gate,读取 `P2-23` 决策产物并输出: + - `ci_workflow_dispatch.json` + - `ci_workflow_dispatch.md` + - 固化调度字段:`dispatch_status`、`dispatch_mode`、`should_dispatch_workflow`、`dispatch_command`、`dispatch_command_parts`。 + - execute 决策时生成规范化 `gh workflow run` 命令;blocked 决策时输出空命令并保持阻断原因可追溯。 + - 支持 `--github-output` 导出 dispatch 状态、命令与报告路径,供下游 CI 直接消费。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:execute 决策时必须产出 `dispatch_status=ready` 与合法 `gh workflow run` 命令。 + - `T2`:blocked 决策时必须产出 `dispatch_status=blocked` 且 `dispatch_command` 为空。 + - `T3`:决策契约冲突(如 decision=execute 但 should_execute=false)时必须拒绝。 + - `T4`:GitHub output 契约必须包含 dispatch 状态、调度命令与报告路径。 +- 前置依赖:`P2-23` + +## P2-25 Linux CI Workflow 调度执行门禁 +- 卡号:`P2-25` +- 优先级:`P2` +- 解决问题: + - `P2-24` 仅产出“可调度契约”,未统一处理真正 dispatch 执行结果,CI 下游仍需重复拼接“是否执行/执行失败如何回报”的逻辑。 + - 缺少调度执行层统一报告,导致 execute/blocked/fail 三态无法稳定沉淀为单一审计产物。 +- 研发功能: + - 新增 dispatch execution gate,读取 `P2-24` 调度契约并输出: + - `ci_workflow_dispatch_execution.json` + - `ci_workflow_dispatch_execution.md` + - 固化执行字段:`execution_status`、`execution_exit_code`、`dispatch_attempted`、`command_returncode`、`reason_codes`。 + - `dispatch_status=ready` 时执行 `dispatch_command_parts`(支持 `--dry-run` 跳过真实执行);`dispatch_status=blocked` 时保持无执行但保留阻断语义。 + - 支持 `--github-output` 导出执行状态、退出码与报告路径,供 Linux CI 下游 job 直接消费。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:ready + dry-run 必须产出 `execution_status=ready_dry_run` 且 `execution_exit_code=0`。 + - `T2`:blocked 决策必须保持 `dispatch_attempted=false` 且退出码遵循上游阻断策略语义。 + - `T3`:dispatch 契约冲突(如 command 与 command_parts 不一致)必须拒绝。 + - `T4`:命令执行失败时必须落地 `execution_status=dispatch_failed` 并携带失败原因码。 + - `T5`:GitHub output 契约需包含执行状态、是否尝试调度、退出码与报告路径。 +- 前置依赖:`P2-24` + +## P2-26 Linux CI Workflow 全链编排门禁 +- 卡号:`P2-26` +- 优先级:`P2` +- 解决问题: + - `P2-17` 至 `P2-25` 各门禁已具备,但 CI 侧仍需人工拼接多条命令,容易出现阶段遗漏、参数漂移与顺序错误。 + - 缺少“单入口”门禁统一调度 workflow plan/yaml/sync/governance/dispatch 全链,导致 Linux CI 编排重复维护成本高。 +- 研发功能: + - 新增 full pipeline gate,一次编排 `P2-17 -> P2-25` 九阶段: + - plan -> yaml -> sync -> command_guard -> governance -> governance_publish -> decision -> dispatch -> dispatch_execution。 + - 支持统一阶段裁剪 `--skip-*`,允许在 CI fan-in / rerun 场景按需跳过前序阶段。 + - 支持统一策略透传:`--strict-generated-at`、`--sync-write`、`--command-guard-write`、`--on-block`、`--workflow-ref`、`--dispatch-timeout-seconds`。 + - 支持 `--print-commands/--dry-run` 预览与 `--fail-fast` 执行策略,沉淀单一 Linux CI 编排入口。 +- 实施范围: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 测试代码要求: + - `T1`:默认阶段链必须完整覆盖 `P2-17 -> P2-25` 且顺序固定。 + - `T2`:策略参数(strict/sync-write/command-guard-write/on-block/workflow-ref/timeout)必须正确透传到对应子门禁。 + - `T3`:部分 `--skip-*` 时必须只保留后半链阶段并保持顺序。 + - `T4`:全部 `--skip-*` 时应返回空计划(不触发阶段执行)。 +- 前置依赖:`P2-25` +## 8. 推荐迭代顺序 + +若只允许一轮一轮推进,建议顺序如下: + +1. `P0-01` +2. `P0-02` +3. `P0-03` +4. `P0-04` +5. `P0-05` +6. `P0-06` +7. `P0-07` +8. `P0-09` +9. `P0-08` +10. `P0-10` + +说明: + +- `P0-08` 不阻塞主功能修复,因此放在 `P0-09` 后面亦可 +- `P0-10` 作为收口卡,应放在 Phase 0 尾部 + +## 9. 每轮迭代的最小产出要求 + +每完成一张卡,至少同步以下产物: + +1. 代码变更 +2. 测试变更 +3. 文档变更 +4. 手工验收命令 +5. 风险与剩余盲区 + +建议每张卡的交付说明统一采用: + +```text +【任务卡】P0-XX +【解决】…… +【完成】…… +【测试】…… +【风险】…… +【下一卡】…… +``` + +## 10. 当前最适合立即开工的卡单 + +若立刻让 Codex 下刀,推荐第一批开工卡: + +1. `P0-01 统一主运行时骨架` +2. `P0-02 /resume 真闭环` +3. `P0-03 auth/config 主链修复与统一` +4. `P0-04 feature flag 运行时修复` +5. `P0-05 review 命令产品化` + +理由: + +- 这 5 张卡直接决定项目是否从“可跑代码”进入“可施工产品” +- 它们都属于高确定性、高收益、可本地验证的任务 +- 其测试回报率最高,且能为后续平台化卡单扫清障碍 + +## 11. 文档维护规则 + +- 若某张任务卡进入开发,新增执行记录时优先更新本文件,而不是平行新建“同主题小文档” +- 若一张任务卡被继续拆分,使用子编号: + - `P0-03A` + - `P0-03B` +- 若策略变化影响任务边界,先更新上游战略文档,再回写本文件 + +## 12. 结论 + +本文档的目标不是“列待办事项”,而是建立一个研发可执行、测试可验收、管理可跟踪的施工面。 + +从现在开始,后续所有开工建议都应基于任务卡,而不是基于模糊意图。 + +换言之: + +> 先按卡开工,再按卡验尸,再按卡推进,不再凭感觉做重构。 + +## 13. Execution Log (2026-04-29) + +- Card: `P0-01 Runtime Backbone` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `claude_code/mcp/server.py` +- Test scripts landed: + - `tests/test_main_runtime.py` (runtime bootstrap + mcp serve wiring coverage) + - `tests/test_runtime_bootstrap.py` (entrypoint consistency + mcp registry injection) +- Not executed locally per release policy: + - Unit/component/integration tests will be executed in Linux unified validation stage. + +## 14. Execution Log (2026-04-29, P0-02) + +- Card: `P0-02 /resume Runtime Closure` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` +- Test scripts landed: + - `tests/test_resume_runtime.py` +- Not executed locally per release policy: + - Resume-related unit/component/integration checks will run in Linux unified validation stage. + +## 15. Execution Log (2026-04-29, P0-03) + +- Card: `P0-03 Auth/Config Runtime Unification` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/auth/__init__.py` +- Test scripts landed: + - `tests/test_commands_auth_runtime.py` + - `tests/test_main_runtime.py` (auth key precedence coverage extension) +- Not executed locally per release policy: + - Auth/config unit/component/integration checks will run in Linux unified validation stage. + +## 16. Execution Log (2026-04-29, P0-04) + +- Card: `P0-04 Feature Flag Runtime Repair` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/features.py` +- Test scripts landed: + - `tests/test_features_runtime.py` +- Not executed locally per release policy: + - Feature flag unit/component checks will run in Linux unified validation stage. + +## 17. Execution Log (2026-04-29, P0-05) + +- Card: `P0-05 Review Command Productization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/review/__init__.py` + - `claude_code/services/review_service.py` +- Test scripts landed: + - `tests/test_review_command_runtime.py` +- Not executed locally per release policy: + - Review command unit/component/integration checks will run in Linux unified validation stage. + +## 18. Execution Log (2026-04-29, P0-06) + +- Card: `P0-06 Session/History/Memory Boundary Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_session_history_memory_runtime.py` + - `tests/test_context_builder_runtime.py` (boundary assertions extension) + - `tests/test_main_runtime.py` (runtime service wiring extension) +- Not executed locally per release policy: + - Session/history/memory unit/component/integration checks will run in Linux unified validation stage. + +## 19. Execution Log (2026-04-29, P0-07) + +- Card: `P0-07 Hooks Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_hooks_runtime.py` + - `tests/test_main_runtime.py` (hooks manager runtime bootstrap wiring update) +- Not executed locally per release policy: + - Hooks runtime unit/component/integration checks will run in Linux unified validation stage. + +## 20. Execution Log (2026-04-29, P0-09) + +- Card: `P0-09 Temp Artifact Hygiene` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `tests/test_plugins_runtime.py` + - `.gitignore` +- Test scripts landed: + - `tests/test_plugins_runtime.py` (fixture teardown cleanup assertion + idempotent cleanup coverage) +- Not executed locally per release policy: + - Plugin runtime cleanup and residue-isolation checks will run in Linux unified validation stage. + +## 21. Execution Log (2026-04-29, P0-08) + +- Card: `P0-08 Windows Bootstrap & Interpreter Probe Stabilization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `README.md` +- Test scripts landed: + - `tests/test_doctor_runtime.py` +- Not executed locally per release policy: + - Doctor interpreter-detection unit/component checks and Windows stub regression cases will run in Linux unified validation stage. + +## 22. Execution Log (2026-04-29, P0-10) + +- Card: `P0-10 Phase 0 Regression Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `pytest.ini` + - `scripts/run_phase0_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_phase0_regression_contract.py` +- Not executed locally per release policy: + - Phase 0 gate contract and curated regression-entry checks will run in Linux unified validation stage. + +## 23. Execution Log (2026-04-29, P1-01) + +- Card: `P1-01 Daemon/API Control Plane` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` + - `claude_code/server/__init__.py` + - `claude_code/main.py` + - `scripts/run_p1_control_plane_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_server_runtime.py` (`T1` schema validation, `T2` service behavior, `T3` client->server->runtime chain, `T4` daemon down/timeout/session-not-found) + - `tests/test_main_runtime.py` (daemon CLI bootstrap wiring coverage) +- Not executed locally per release policy: + - Daemon/API unit/component/integration/regression checks will run in Linux unified validation stage. + +## 24. Execution Log (2026-04-29, P1-02) + +- Card: `P1-02 Event Journal` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/event_journal.py` + - `claude_code/services/__init__.py` + - `claude_code/tasks/manager.py` + - `claude_code/tasks/factory.py` + - `claude_code/tasks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/server/control_plane.py` + - `claude_code/main.py` + - `scripts/run_p1_event_journal_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_event_journal_runtime.py` (`T1` schema/versioning, `T2` writer/reader correctness, `T3` query->tool->task event chain, `T4` partial write/corruption recovery) + - `tests/test_server_runtime.py` (event list/replay daemon API coverage) + - `tests/test_main_runtime.py` (runtime bootstrap wiring includes event journal) +- Not executed locally per release policy: + - Event journal unit/component/integration/regression checks will run in Linux unified validation stage. + +## 25. Execution Log (2026-04-29, P1-03) + +- Card: `P1-03 SQLite Runtime State Backend` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tasks/repository.py` (SQLite runtime repository + schema version/migration guard) + - `claude_code/tasks/factory.py` (runtime backend default switched to `sqlite`) + - `claude_code/tasks/__init__.py` (SQLite repository exports) + - `claude_code/engine/session.py` (SQLite session store + manager constructor hook) + - `claude_code/services/event_journal.py` (SQLite event journal + schema version/migration guard) + - `claude_code/services/__init__.py` (SQLite event journal exports) + - `claude_code/engine/query.py` (event journal duck-typing for backend-agnostic write path) + - `claude_code/main.py` (runtime bootstrap switched to unified `.claude/runtime_state.db`) + - `scripts/run_p1_sqlite_state_backend_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_sqlite_runtime_state_backend.py` (`T1` schema init/version guard, `T2` repository/session/event correctness, `T3` runtime chain wiring) + - `tests/test_tasks_backend_contract.py` (SQLite repository contract + schema guard extension) + - `tests/test_tasks_factory_runtime.py` (default runtime backend contract update to `sqlite`) + - `tests/test_tasks_manager_runtime.py` (SQLite lifecycle persistence/recovery extension) + - `tests/test_event_journal_runtime.py` (SQLite event journal schema/query/replay guard extension) + - `tests/test_main_runtime.py` (runtime bootstrap mocks updated for SQLite state wiring) +- Not executed locally per release policy: + - SQLite runtime state backend unit/component/integration/regression checks will run in Linux unified validation stage. + +## 26. Execution Log (2026-04-30, P1-04) + +- Card: `P1-04 Active Memory Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/memory_service.py` (scoped memory namespaces + scope snapshot API) + - `claude_code/engine/query.py` (active memory hit audit event + runtime memory scope propagation) + - `claude_code/main.py` (system prompt active memory snapshot injection + runtime memory scope wiring) + - `claude_code/tools/base.py` (tool context memory scope) + - `claude_code/tools/agent/__init__.py` (agent-level memory scope propagation) + - `claude_code/agents/builtin.py` (builtin agent memory scope defaults) + - `scripts/run_p1_active_memory_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_memory_scope_runtime.py` (`T1` scope namespace isolation, scoped snapshot boundaries) + - `tests/test_active_memory_runtime.py` (`T2` system-prompt memory injection, `T3` query memory-hit event chain, runtime scope propagation) +- Not executed locally per release policy: + - Active memory unit/component/integration/regression checks will run in Linux unified validation stage. + +## 27. Execution Log (2026-04-30, P1-05) + +- Card: `P1-05 Hook/Permission/Audit Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/permissions.py` (structured permission evaluation reason output for audit chain) + - `claude_code/services/hooks_manager.py` (hook result serialization helper) + - `claude_code/engine/query.py` (permission request/decision events + hook execution audit events) + - `scripts/run_p1_hook_permission_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_hook_permission_audit_runtime.py` (`T2` permission+hook+audit linkage, `T3` tool execution audit chain, `T4` deny/error partial-failure path) +- Not executed locally per release policy: + - Hook/permission/audit convergence unit/component/integration/regression checks will run in Linux unified validation stage. + +## 28. Execution Log (2026-04-30, P1-06) + +- Card: `P1-06 CLI Thin Client Migration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` (daemon thin-client mode for query/pipe path, daemon-required hard-fail option, local fallback branch) + - `scripts/run_p1_cli_thin_client_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_cli_daemon_thin_client_runtime.py` (`T2` client adapter mode resolution contract) + - `tests/test_main_runtime.py` (daemon thin-client success/fallback/fail-fast and pipe-mode chain coverage) +- Not executed locally per release policy: + - CLI thin-client adapter and daemon-fallback regression checks will run in Linux unified validation stage. + +## 29. Execution Log (2026-04-30, P2-01) + +- Card: `P2-01 Multi-Agent Supervisor` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/multi_agent_supervisor.py` (agent DAG scheduler, budget/timeout/retry policy, dependency artifact passing, parallel fan-out and aggregate close) + - `claude_code/services/__init__.py` (supervisor exports) + - `claude_code/tools/agent/__init__.py` (orchestrate mode + workflow schema + supervisor integration with structured artifact summary) + - `scripts/run_p2_multi_agent_supervisor_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_multi_agent_supervisor_runtime.py` (`T1` graph cycle guard, `T2` budget/timeout state machine, `T3` parallel scheduling, `T4` retry-based failure recovery; plus agent tool orchestrate path and config error branch) +- Not executed locally per release policy: + - Multi-agent supervisor unit/component/integration/regression checks will run in Linux unified validation stage. + +## 30. Execution Log (2026-04-30, P2-02) + +- Card: `P2-02 Artifact Bus & Conflict Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/artifact_bus.py` (artifact schema contract for patch/note/diff/report/finding, producer payload parsing, merge strategies `fail|append|replace`, conflict error path) + - `claude_code/services/multi_agent_supervisor.py` (artifact bus integration for dependency artifact passing, workflow-level artifact snapshot, node artifact conflict/scheme failure handling) + - `claude_code/services/__init__.py` (artifact bus exports) + - `scripts/run_p2_artifact_bus_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_artifact_bus_runtime.py` (`T1` schema validation, `T2` producer/consumer dependency view, `T4` conflict-fail path, append merge behavior) + - `tests/test_multi_agent_supervisor_runtime.py` (artifact bus summary extension + conflict merge failure branch in DAG execution) +- Not executed locally per release policy: + - Artifact bus unit/component/integration/regression checks will run in Linux unified validation stage. + +## 31. Execution Log (2026-04-30, P2-03) + +- Card: `P2-03 IDE Integration (VS Code first)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` (added `/api/v1/ide/workspace` endpoint for IDE aggregate snapshot: diff, changed files, tasks, sessions, findings) + - `claude_code/server/ide_adapter.py` (VS Code client adapter and normalized workspace snapshot model) + - `claude_code/server/__init__.py` (IDE adapter exports) + - `scripts/run_p2_ide_integration_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_ide_integration_runtime.py` (`T2` client protocol adapter normalization, `T3` IDE -> daemon core chain for diff/task/session/findings) +- Not executed locally per release policy: + - IDE integration unit/component/integration/manual acceptance checks will run in Linux unified validation stage. + +## 32. Execution Log (2026-04-30, P2-04) + +- Card: `P2-04 GitHub/CI Workflow Agent` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/github_ci_workflow.py` (issue->plan->code->test->review->PR state machine, headless CI mode, command failure classification, structured workflow report) + - `claude_code/server/control_plane.py` (added `POST /api/v1/workflows/github-ci` endpoint + client helper for workflow execution) + - `claude_code/services/__init__.py` (GitHub/CI workflow service exports) + - `scripts/run_p2_github_ci_workflow_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_github_ci_workflow_runtime.py` (`T2` workflow state machine, `T3` fake repo/fake PR main chain, `T4` permission-denied/repo-dirty/network-failure branches) + - `tests/test_server_runtime.py` (daemon API route coverage for workflow success/failure mapping) +- Not executed locally per release policy: + - GitHub/CI workflow unit/component/integration/regression checks will run in Linux unified validation stage. + +## 33. Execution Log (2026-04-30, P2-05) + +- Card: `P2-05 Org Policy & Audit` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/org_policy_audit.py` (org policy matcher, secret redaction, approval queue transitions, audit event query/report) + - `claude_code/server/control_plane.py` (added org policy endpoints and client helpers: evaluate/approve/list/report) + - `claude_code/services/__init__.py` (Org policy audit service exports) + - `scripts/run_p2_org_policy_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_org_policy_audit_runtime.py` (`T1` policy matcher+redaction, `T2` approval transitions, `T3` audit report chain, `T4` policy conflict block) + - `tests/test_server_runtime.py` (daemon API coverage for org policy evaluate/approval/audit/report and failure mapping) +- Not executed locally per release policy: + - Org policy/audit unit/component/integration/regression checks will run in Linux unified validation stage. + +## 34. Execution Log (2026-04-30, P2-06) + +- Card: `P2-06 Linux Unified Verification Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_linux_unified_gate.py` (aggregated Linux-stage command manifest for Phase 0 -> Phase 2 runtime chain) + - `README.md` (added unified gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (added P2-06 card + execution log) +- Test scripts landed: + - `tests/test_linux_unified_gate_runtime.py` (`T1` manifest file existence contract, `T2` command generation order/format contract) +- Not executed locally per release policy: + - Unified gate contract checks and all listed runtime tests will run in Linux unified validation stage. + +## 35. Execution Log (2026-04-30, P2-07) + +- Card: `P2-07 Agent Runtime Parity (sync/background)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tools/base.py` (ToolContext adds conversation/provider/task_manager/tool_registry fields for runtime propagation) + - `claude_code/engine/query.py` (ToolContext now carries conversation/provider/task_manager/registry into tool execution path) + - `claude_code/tasks/manager.py` (`create_agent_task` supports metadata injection for audit/replay) + - `claude_code/tools/agent/__init__.py` (sync/background sub-agent parity: runtime provider/model resolution, registry propagation, task-manager reuse, background metadata contract) + - `scripts/run_p2_agent_runtime_parity_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-07 regression test to unified manifest) + - `README.md` (adds P2-07 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-07 card and execution log) +- Test scripts landed: + - `tests/test_agent_background_context_runtime.py` (`T2` sync/background context parity, `T3` background task metadata audit fields, `T4` provider/model inheritance regression) +- Not executed locally per release policy: + - Agent runtime parity unit/component/integration checks will run in Linux unified validation stage. + +## 36. Execution Log (2026-04-30, P2-08) + +- Card: `P2-08 Custom Agents Directory Loader` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/agents/loader.py` (custom agent markdown loader with YAML frontmatter parsing and cwd->git-root directory merge semantics) + - `claude_code/tools/agent/__init__.py` (lazy custom agent load, custom>builtin resolution, and merged unknown-type availability hints) + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-08 regression test to unified manifest) + - `README.md` (adds P2-08 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-08 card and execution log) +- Test scripts landed: + - `tests/test_custom_agents_loader_runtime.py` (`T1` frontmatter parsing, `T2` nested override merge, `T3` custom override builtin, `T4` invalid schema tolerance and unknown-type availability message) +- Not executed locally per release policy: + - Custom-agent loader unit/component/integration checks will run in Linux unified validation stage. + + +## 37. Execution Log (2026-04-30, P2-09) + +- Card: `P2-09 Linux Unified Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (Linux-stage executor over unified manifest; supports dry-run/print/continue-on-failure/report-dir and writes junit + summary json) + - `scripts/run_linux_unified_gate.py` (adds P2-09 execution gate contract test to unified manifest) + - `README.md` (adds P2-09 execution gate command entries) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-09 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (`T1` unified manifest order contract, `T2` junit path and extra-args forwarding contract) +- Not executed locally per release policy: + - Linux unified execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 38. Execution Log (2026-04-30, P2-10) + +- Card: `P2-10 JetBrains IDE Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/ide_adapter.py` (added `JetBrainsClientAdapter` + `JetBrainsWorkspaceSnapshot` + findings->inspections mapping with severity highlight normalization) + - `claude_code/server/__init__.py` (exports JetBrains adapter symbols) + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-10 adapter contract test to unified manifest) + - `README.md` (adds P2-10 gate command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-10 card and execution log) +- Test scripts landed: + - `tests/test_p2_jetbrains_ide_integration_runtime.py` (`T1` adapter snapshot normalization, `T2` findings->inspection highlight mapping, `T4` line type coercion tolerance) +- Not executed locally per release policy: + - JetBrains IDE adapter unit/component checks will run in Linux unified validation stage. + +## 39. Execution Log (2026-04-30, P2-11) + +- Card: `P2-11 Linux Sharded Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (adds deterministic shard slicing via `--shard-total` + `--shard-index`, shard-aware dry-run header, shard metadata in summary payload) + - `README.md` (adds Linux shard-parallel invocation example) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-11 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds shard slicing contract, invalid shard argument guards, summary shard metadata contract) +- Not executed locally per release policy: + - P2-11 shard execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 40. Execution Log (2026-04-30, P2-12) + +- Card: `P2-12 Linux Shard Aggregation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_aggregation_gate.py` (discovers shard summaries via direct path/glob/artifacts dir, validates summary schema, merges shard totals, emits merged summary and global pass/fail) + - `scripts/run_linux_unified_gate.py` (adds P2-12 aggregation contract test to unified manifest) + - `README.md` (adds Linux shard aggregation gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-12 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` (`T1` summary discovery dedupe, `T2` summary field validation, `T3` successful shard aggregation, `T4` missing-shard and total drift failure contract) + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds empty-shard summary payload contract used by aggregation stage) +- Not executed locally per release policy: + - P2-12 aggregation gate contract checks and Linux multi-shard recovery validation will run in Linux unified validation stage. + +## 41. Execution Log (2026-04-30, P2-13) + +- Card: `P2-13 Linux Final Report Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_report_publish_gate.py` (loads merged shard summary, validates publish contract, emits final_report.json + final_report.md, enforces computed-vs-reported status consistency) + - `scripts/run_linux_unified_gate.py` (adds P2-13 publish gate contract test to unified manifest) + - `README.md` (adds P2-13 final report publish gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-13 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_report_publish_gate_runtime.py` (`T1` merged summary required fields/types, `T2` happy-path payload contract, `T3` status mismatch failure contract, `T4` markdown report section contract) +- Not executed locally per release policy: + - P2-13 publish gate contract checks and Linux final-report recovery validation will run in Linux unified validation stage. + +## 42. Execution Log (2026-04-30, P2-14) + +- Card: `P2-14 Linux Unified Pipeline Orchestration Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_pipeline_gate.py` (orchestrates execution->aggregation->publish chain, supports stage skip controls, dry-run/print-commands, fail-fast, and pytest args forwarding to execution stage) + - `scripts/run_linux_unified_gate.py` (adds P2-14 pipeline contract test to unified manifest) + - `README.md` (adds P2-14 one-command Linux pipeline examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-14 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` (`T1` default 3-stage command plan with execution summary feed, `T2` skip-execution external summary passthrough, `T3` all-stage-skip empty plan contract) +- Not executed locally per release policy: + - P2-14 pipeline gate contract checks and Linux orchestrated recovery validation will run in Linux unified validation stage. + +## 43. Execution Log (2026-04-30, P2-15) + +- Card: `P2-15 Linux Shard Plan Generation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_plan_gate.py` (builds deterministic shard execution plan from unified manifest; emits per-shard report/summary/command payload; supports dry-run/print-commands/pytest args forwarding) + - `scripts/run_linux_unified_gate.py` (adds P2-15 shard plan contract test to unified manifest) + - `README.md` (adds P2-15 Linux shard plan command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-15 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_plan_gate_runtime.py` (`T1` deterministic shard slicing contract, `T2` command+args forwarding contract, `T3` invalid shard_total rejection contract) +- Not executed locally per release policy: + - P2-15 shard plan gate contract checks and Linux fan-out orchestration validation will run in Linux unified validation stage. + +## 44. Execution Log (2026-04-30, P2-16) + +- Card: `P2-16 Linux CI Matrix Export Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_matrix_gate.py` (loads P2-15 shard plan, validates shard/total contract, emits CI-consumable matrix payload and summary-path collection, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-16 matrix gate contract test to unified manifest) + - `README.md` (adds P2-16 CI matrix gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-16 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` (`T1` matrix include order/field contract, `T2` empty-shard filter contract, `T3` GitHub output value contract, `T4` totals mismatch rejection contract) +- Not executed locally per release policy: + - P2-16 CI matrix export contract checks and Linux fan-out/fan-in orchestration validation will run in Linux unified validation stage. + +## 45. Execution Log (2026-04-30, P2-17) + +- Card: `P2-17 Linux CI Workflow Plan Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_plan_gate.py` (loads P2-16 ci_matrix, validates matrix/summary_paths contracts, emits CI-consumable workflow plan with fan-out matrix and fan-in aggregation/publish commands, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-17 workflow plan gate contract test to unified manifest) + - `README.md` (adds P2-17 workflow plan gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-17 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` (`T1` fan-out/fan-in payload contract, `T2` GitHub output contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-17 CI workflow plan contract checks and Linux fan-out/fan-in/final-publish orchestration validation will run in Linux unified validation stage. + +## 46. Execution Log (2026-04-30, P2-18) + +- Card: `P2-18 Linux CI Workflow YAML Render Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` (loads P2-17 ci_workflow_plan, validates fan-out/fan-in contract, renders GitHub Actions workflow YAML, emits render metadata for audit traceability) + - `scripts/run_linux_unified_gate.py` (adds P2-18 workflow YAML render contract test to unified manifest) + - `README.md` (adds P2-18 workflow YAML gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-18 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` (`T1` workflow YAML fan-out/fan-in render contract, `T2` render metadata contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-18 workflow YAML render contract checks and Linux CI-end orchestration validation will run in Linux unified validation stage. + +## 47. Execution Log (2026-04-30, P2-19) + +- Card: `P2-19 Linux CI Workflow Drift Sync Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_sync_gate.py` (rebuilds expected workflow/metadata from P2-17 plan, validates drift against rendered artifacts, supports diff output and optional in-place sync write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-19 workflow sync contract test to unified manifest) + - `README.md` (adds P2-19 drift check/sync command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-19 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` (`T1` missing artifact drift contract, `T2` workflow content drift+diff contract, `T3` generated_at relaxed-mode contract, `T4` strict generated_at mismatch rejection) +- Not executed locally per release policy: + - P2-19 workflow sync contract checks and Linux CI drift-governance validation will run in Linux unified validation stage. + +## 48. Execution Log (2026-04-30, P2-20) + +- Card: `P2-20 Linux CI Workflow Command Guard Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` (validates command string/parts integrity, script target + required flag contract, fan-out/fan-in path binding, optional canonical write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-20 command guard contract test to unified manifest) + - `README.md` (adds P2-20 command guard gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-20 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` (`T1` valid payload pass contract, `T2` command string/parts mismatch rejection + normalization contract, `T3` aggregation summary order mismatch rejection, `T4` publish path mismatch rejection) +- Not executed locally per release policy: + - P2-20 command-integrity contract checks and Linux CI command-governance validation will run in Linux unified validation stage. + +## 49. Execution Log (2026-04-30, P2-21) + +- Card: `P2-21 Linux CI Workflow Governance Convergence Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_gate.py` (aggregates P2-19 drift sync + P2-20 command guard + metadata lineage checks into one governance verdict and categorized failed_checks) + - `scripts/run_linux_unified_gate.py` (adds P2-21 governance gate contract test to unified manifest) + - `README.md` (adds P2-21 governance gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-21 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` (`T1` full aligned pass contract, `T2` workflow drift failure category, `T3` command guard failure category, `T4` metadata lineage mismatch category) +- Not executed locally per release policy: + - P2-21 governance convergence contract checks and Linux CI final-governance validation will run in Linux unified validation stage. + +## 50. Execution Log (2026-04-30, P2-22) + +- Card: `P2-22 Linux CI Workflow Governance Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` (publishes P2-21 governance report into CI decision payload, markdown report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-22 governance publish contract test to unified manifest) + - `README.md` (adds P2-22 governance publish gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-22 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` (`T1` aligned governance pass contract, `T2` failed-checks deny-execution contract, `T3` failed_checks mismatch structural-failure contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-22 governance publish contract checks and Linux CI publish-stage validation will run in Linux unified validation stage. + +## 51. Execution Log (2026-04-30, P2-23) + +- Card: `P2-23 Linux CI Workflow Execution Decision Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_decision_gate.py` (converts P2-22 publish payload into execute/blocked decision contract with on-block exit policy, artifact-presence guard, markdown/json report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-23 execution decision gate contract test to unified manifest) + - `README.md` (adds P2-23 decision-gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-23 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` (`T1` aligned pass -> execute contract, `T2` failed-governance -> blocked contract, `T3` blocked+skip policy exit-code contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-23 execution-decision contract checks and Linux CI handoff validation will run in Linux unified validation stage. + +## 52. Execution Log (2026-04-30, P2-24) + +- Card: `P2-24 Linux CI Workflow Dispatch Readiness Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` (converts P2-23 execution decision payload into dispatch-ready contract, canonical gh workflow dispatch command, markdown/json reports, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-24 dispatch gate contract test to unified manifest) + - `README.md` (adds P2-24 dispatch gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-24 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` (`T1` execute -> ready dispatch contract, `T2` blocked -> no-dispatch contract, `T3` decision contract mismatch rejection, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-24 dispatch-readiness contract checks and Linux CI dispatch handoff validation will run in Linux unified validation stage. + +## 53. Execution Log (2026-04-30, P2-25) + +- Card: `P2-25 Linux CI Workflow Dispatch Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` (validates P2-24 dispatch contract, executes ready dispatch command with timeout handling, and emits execution JSON/markdown/github-output contract) + - `scripts/run_linux_unified_gate.py` (adds P2-25 dispatch execution gate contract test to unified manifest) + - `README.md` (adds P2-25 dispatch execution gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-25 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` (`T1` ready dry-run contract, `T2` blocked pass-through contract, `T3` command contract mismatch rejection, `T4` dispatch failure reason code contract, `T5` github-output field contract) +- Not executed locally per release policy: + - P2-25 dispatch execution contract checks and Linux CI dispatch execution validation will run in Linux unified validation stage. + +## 54. Execution Log (2026-04-30, P2-26) + +- Card: `P2-26 Linux CI Workflow Full Pipeline Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (composes P2-17 -> P2-25 into single pipeline entry with stage skips, strategy passthrough, dry-run/print, and fail-fast execution policy) + - `scripts/run_linux_unified_gate.py` (adds P2-26 pipeline gate contract test to unified manifest) + - `README.md` (adds P2-26 one-command workflow pipeline usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-26 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (`T1` default 9-stage chain contract, `T2` strategy passthrough contract, `T3` partial skip-stage chain contract, `T4` all-skip empty-plan contract) +- Not executed locally per release policy: + - P2-26 pipeline orchestration contract checks and Linux CI end-to-end workflow gate validation will run in Linux unified validation stage. diff --git a/.claude/blob_f3f2a26f0059515eee07afd722cf65dd5b9090d6.txt b/.claude/blob_f3f2a26f0059515eee07afd722cf65dd5b9090d6.txt new file mode 100644 index 0000000..f3f2a26 --- /dev/null +++ b/.claude/blob_f3f2a26f0059515eee07afd722cf65dd5b9090d6.txt @@ -0,0 +1,627 @@ +# Claude Code Python + +一个面向代码工作的 AI CLI / REPL / MCP 项目,提供 Claude Code 风格的交互、工具调用、任务运行时、Agent、多 Provider 接入,以及一套持续演进中的工程化基础设施。 + +本 README 是当前仓库的统一入口。大体量专题文档继续保留在 `docs/`,这里只保留必要摘要、明确导航和后续维护约定,避免文档再次失控。 + +## 项目定位 + +- 语言与运行时:Python 3.10+、`asyncio` +- 主入口:`python -m claude_code.main` 或 `python run.py` +- 交互模式:交互式 REPL、单次 query、管道模式、MCP 服务器模式 +- 模型接入:`anthropic`、`openai`、`ollama`、`vllm`、`deepseek`、`bedrock`、`vertex` +- 关键子系统:`QueryEngine`、`ToolRegistry`、权限系统、任务运行时、Agent、MCP、技能系统、服务层 + +当前代码中 `azure` 路径有显式 fail-fast 提示,因此这里不把它作为当前推荐运行路径。 + +## 当前整理结论 + +截至 `2026-04-17`,本仓库的 Markdown 文档可以收敛为以下结构: + +1. `README.md` 只负责统一入口,不再堆砌超长教程和实现枚举。 +2. “当前状态”优先看带日期的评估/执行文档,而不是更早的评审草稿。 +3. `docs/current/reference/AGENT.MD` 与 `docs/current/reference/MCP.md` 体量较大,保留独立文档,不再重复搬进 README。 +4. 架构、性能、中间件三条线分别保留“当前结论”与“历史细节”两层,不再并列堆叠多个同级入口。 + +本次额外核对结果: + +- 测试复核:`pytest -q -c pytest.ini` 通过,结果为 `90 passed` +- 架构最新快照:`2026-04-16` 的评测结论为“工程可用基线可推进”,评分 `8.4 / 10` +- 中间件演进快照:`Phase 0` 已完成,`Phase 1` 持续推进 +- 性能快照:`Baseline V1` 记录启动耗时约 `1109.38 ms` + +## 快速开始 + +### 1. 安装依赖 + +```bash +python -m pip install -r requirements.txt +``` + +如果你只是从源码运行,本仓库当前建议以 `requirements.txt` 和 `pytest.ini` 为准。 + +### 2. 选择 Provider + +#### Anthropic + +```bash +export CLAUDE_API_PROVIDER=anthropic +export ANTHROPIC_API_KEY=your-key +export CLAUDE_MODEL=claude-sonnet-4-20250514 +``` + +#### Ollama / 本地 OpenAI 兼容模型 + +```bash +export CLAUDE_API_PROVIDER=ollama +export OPENAI_BASE_URL=http://localhost:11434/v1 +export CLAUDE_MODEL=qwen2.5:14b +``` + +说明: + +- `openai` / `deepseek` 需要 `OPENAI_API_KEY` +- `ollama` / `vllm` 允许本地模型路径,不强制真实 API Key +- PowerShell 请将 `export` 改为 `$env:变量名="值"` + +### Windows 推荐启动(避免 WindowsApps stub) + +Windows 上不要依赖 `C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe` 这类 Store alias stub。 + +建议优先使用: + +```powershell +py -3 -m claude_code.main --doctor +py -3 -m claude_code.main +``` + +或显式使用虚拟环境解释器: + +```powershell +.\.venv\Scripts\python.exe -m claude_code.main --doctor +.\.venv\Scripts\python.exe -m claude_code.main +``` + +`--doctor` 会输出解释器来源与风险提示;若命中 WindowsApps stub,会给出明确警告与替代启动命令。 + +### 3. 常用启动方式 + +```bash +# 查看帮助 +python -m claude_code.main --help + +# 版本 +python -m claude_code.main --version + +# 健康检查 +python -m claude_code.main --doctor + +# 交互式 REPL +python -m claude_code.main + +# 单次提问 +python -m claude_code.main "解释当前目录下的项目结构" + +# 管道模式 +echo "帮我总结这段代码" | python -m claude_code.main --pipe + +# 作为 MCP 服务器启动 +python -m claude_code.main --mcp-serve + +# 启动本地 daemon / API 控制面(P1-01) +python -m claude_code.main --daemon-serve --daemon-host 127.0.0.1 --daemon-port 8787 + +# CLI 以 thin-client 走 daemon(P1-06) +python -m claude_code.main --daemon-client "explain current repo status" +echo "summarize changed files" | python -m claude_code.main --pipe --daemon-client +``` + +## 核心能力概览 + +- 交互层:REPL、单轮 query、管道模式、命令系统 +- 工具层:文件读写、搜索、Shell/PowerShell、工作流、任务控制、技能、浏览器、LSP、MCP、Worktree、Cron、Team +- 运行时:`QueryEngine` 编排模型对话、工具调用、权限判定和上下文构建 +- 任务体系:`TaskManager`、`TaskRepository`、`TaskQueue` 支撑后台任务和任务存储演进 +- 服务层:缓存、速率限制、遥测、历史管理、记忆、加密、hooks、shutdown、token estimation +- 扩展能力:MCP server/client、Agent、技能目录、项目级 `.claude/` 运行数据 + +如果你要看完整工具清单,不再从 README 查找,直接看 `docs/current/reference/MCP.md`。 + +## 仓库结构 + +```text +claude-code-python/ +├── claude_code/ # 主代码 +│ ├── main.py # CLI 入口 +│ ├── engine/ # QueryEngine / context / session / permissions +│ ├── tools/ # 工具系统 +│ ├── commands/ # 命令系统 +│ ├── tasks/ # 任务运行时、仓储、队列 +│ ├── services/ # 缓存、遥测、历史、hooks、shutdown 等 +│ ├── mcp/ # MCP 服务端实现 +│ ├── agents/ # 内置 Agent 定义 +│ ├── skills/ # 技能系统 +│ └── state/ # 状态模型 +├── docs/ # 专题文档 +├── examples/ # 示例脚本 +├── scripts/ # 辅助脚本与性能脚本 +├── tests/ # 测试 +├── run.py # 源码运行入口 +├── requirements.txt # 依赖清单 +└── pytest.ini # pytest 配置 +``` + +## 文档地图 + +下面是整理后的推荐阅读顺序。 + +| 文档 | 角色 | 什么时候看 | 当前定位 | +|---|---|---|---| +| `README.md` | 总入口 | 第一次进入仓库 | 唯一导航入口 | +| `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 架构现状快照 | 想知道项目现在整体状态 | 架构主入口 | +| `docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` | 产品战略与超车路线图 | 想知道项目如何从“可用底盘”走到“超越 Claude Code” | 战略主入口 | +| `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` | 执行计划与任务卡单 | 想直接开工并明确每张卡的研发范围、完成标准、测试深度 | 执行主入口 | +| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 任务/中间件落地记录 | 想知道任务系统已改到哪一步 | 中间件现状主入口 | +| `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 性能一页纸 | 想快速看性能状态 | 性能主入口 | +| `docs/current/reference/MCP.md` | MCP 使用手册 | 想把仓库作为 MCP server 接入 | MCP 主入口 | +| `docs/current/reference/AGENT.MD` | Agent 全量手册 | 想理解 Agent、异步子任务、多智能体 | Agent 主入口 | +| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 首轮问题盘点 | 想追溯最初发现了哪些问题 | 架构历史文档 | +| `docs/history/architecture/FIX_PROPOSALS.md` | 修复方案库 | 想看历史修复设计与备选方案 | 架构历史文档 | +| `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 中间件演进评估 | 想评估 Redis/MySQL/平台化是否有必要 | 中间件决策文档 | +| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 性能路线图 | 想看阶段目标和执行计划 | 性能路线图 | +| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 性能优化明细 | 想追踪具体改了哪些热路径 | 性能细节文档 | +| `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 性能进度记录 | 想看阶段性进展 | 性能历史文档 | +| `docs/current/performance/BASELINE_V1.md` | 性能基线摘要 | 想看基线结论 | 性能数据摘要 | +| `docs/current/performance/baseline_v1.json` | 原始基线数据 | 想做对比或自动化处理 | 性能原始数据 | + +## 各主题的权威入口 + +为避免“同一主题多个入口同时有效”,后续统一按下面的主入口理解: + +- 架构现状:`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` +- 产品战略 / 超越 Claude Code 路线图:`docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` +- 执行计划 / 任务卡单:`docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 架构历史问题与修复备选:`docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md` +- 任务/中间件当前落地状态:`docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` +- 任务/中间件演进决策依据:`docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` +- 性能当前状态:`docs/current/performance/PERFORMANCE_ONE_PAGER.md` +- 性能基线数据:`docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json` +- MCP 使用:`docs/current/reference/MCP.md` +- Agent 使用:`docs/current/reference/AGENT.MD` + +## 架构、性能、中间件的摘要 + +### 架构 + +综合 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 与历史评审文档,目前可以将架构结论收敛为: + +- 方向上是合理的模块化单体 +- 关键主链路曾出现过工具暴露、权限硬闸、Agent 异步、Provider 适配等断点 +- 截至 `2026-04-16` 的评测文档,这些关键断点已完成一轮闭环修复,测试记录为 `68 passed` +- 更早的 `docs/history/architecture/ARCHITECTURE_REVIEW.md` 和 `docs/history/architecture/FIX_PROPOSALS.md` 主要用于追溯“为什么会这么改”,不再代表最新状态 + +### 中间件 / 任务系统 + +综合两份中间件文档,当前结论是: + +- 仓库原始模型是“进程内队列 + 文件持久化”,不是数据库队列架构 +- 对单机 CLI 合理,但对平台化、多实例、可恢复场景风险较高 +- 当前落地重点已从“是否立刻上 Redis/MySQL”转为“先把任务模型、仓储、队列抽象收敛” +- 已完成的工作包括:`tasks.json` schema 统一、文件锁、安全写入、durable 语义修复、`TaskRepository` / `TaskQueue` 抽象接入 + +### 性能 + +综合性能文档,当前结论是: + +- 已建立语法门禁、性能 smoke、基线脚本三件基础设施 +- 当前基线主数据见 `docs/current/performance/baseline_v1.json` +- 单页摘要优先看 `docs/current/performance/PERFORMANCE_ONE_PAGER.md` +- 详细优化项优先看 `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` + +## 开发与验证 + +```bash +# 运行测试 +python -m pytest -q -c pytest.ini + +# 运行 Phase 0 主链回归门禁(提交前/合并前) +python scripts/run_phase0_gate.py + +# 运行 Phase 1 控制面回归门禁(P1-01) +python scripts/run_p1_control_plane_gate.py + +# 运行 Phase 1 事件日志回归门禁(P1-02) +python scripts/run_p1_event_journal_gate.py + +# 运行 Phase 1 SQLite 状态后端回归门禁(P1-03) +python scripts/run_p1_sqlite_state_backend_gate.py + +# 运行 Phase 1 Active Memory 回归门禁(P1-04) +python scripts/run_p1_active_memory_gate.py + +# 运行 Phase 1 Hook/Permission/Audit 收敛门禁(P1-05) +python scripts/run_p1_hook_permission_audit_gate.py + +# 运行 Phase 1 CLI thin-client 迁移门禁(P1-06) +python scripts/run_p1_cli_thin_client_gate.py + +# 运行 Phase 2 多 Agent Supervisor 门禁(P2-01) +python scripts/run_p2_multi_agent_supervisor_gate.py + +# 运行 Phase 2 Artifact Bus 门禁(P2-02) +python scripts/run_p2_artifact_bus_gate.py + +# 运行 Phase 2 IDE 集成门禁(P2-03) +python scripts/run_p2_ide_integration_gate.py + +# 运行 Phase 2 GitHub/CI workflow 门禁(P2-04) +python scripts/run_p2_github_ci_workflow_gate.py + +# 运行 Phase 2 组织策略与审计门禁(P2-05) +python scripts/run_p2_org_policy_audit_gate.py + +# 运行 Phase 2 Agent 运行时一致性门禁(P2-07) +python scripts/run_p2_agent_runtime_parity_gate.py + +# 运行 Phase 2 自定义 Agent 目录加载门禁(P2-08) +python scripts/run_p2_custom_agents_loader_gate.py + +# 运行 Phase 2 JetBrains IDE 集成门禁(P2-10) +python scripts/run_p2_jetbrains_ide_integration_gate.py + +# 运行 Phase 0-2 Linux 统一验尸门禁(P2-06) +python scripts/run_linux_unified_gate.py + +# 运行 Phase 2 Linux 统一验尸执行门禁(P2-09,Linux 执行) +python scripts/run_p2_linux_unified_execution_gate.py --dry-run +python scripts/run_p2_linux_unified_execution_gate.py --continue-on-failure +# 分片并行示例(3 个 job 中的第 2 片) +python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2 --continue-on-failure + +# 聚合多分片 summary 并给出全局通过/失败判定(P2-12,Linux 回收阶段执行) +python scripts/run_p2_linux_shard_aggregation_gate.py --artifacts-dir .claude/reports +python scripts/run_p2_linux_shard_aggregation_gate.py --summary-glob ".claude/reports/**/summary.json" + +# 从 merged summary 发布最终报告(P2-13,Linux 回收阶段执行) +python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json +python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md + +# 一键串联执行->汇总->发布全链门禁(P2-14,Linux 编排入口) +python scripts/run_p2_linux_unified_pipeline_gate.py --dry-run +python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure --fail-fast +# 仅做汇总+发布(跳过执行阶段) +python scripts/run_p2_linux_unified_pipeline_gate.py --skip-execution --summary-glob ".claude/reports/**/summary.json" + +# 生成 Linux 分片执行计划(P2-15,供 CI fan-out job 直接消费) +python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --dry-run --print-commands +python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --output .claude/reports/linux_unified_gate/shard_plan.json --continue-on-failure -- -k runtime + +# 生成 Linux CI matrix 产物(P2-16,供 CI matrix 直接消费) +python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --dry-run --print-matrix +python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --output .claude/reports/linux_unified_gate/ci_matrix.json --skip-empty-shards + +# 生成 Linux CI workflow 计划产物(P2-17,供 fan-out/fan-in job 直接消费) +python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --dry-run --print-plan +python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --output .claude/reports/linux_unified_gate/ci_workflow_plan.json + +# 将 workflow 计划渲染为 GitHub Actions YAML(P2-18,供 CI 直接执行) +python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-yaml +python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --output-workflow .github/workflows/linux_unified_gate.yml --output-metadata .claude/reports/linux_unified_gate/ci_workflow_render.json + +# 校验/同步 workflow 渲染产物漂移(P2-19,防止 plan->yaml 漂移) +python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --print-diff +python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --write + +# 校验/规范化 workflow 计划中的命令契约(P2-20,防止 command/parts/路径 漂移) +python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --write --output .claude/reports/linux_unified_gate/ci_workflow_command_guard.json + +# 汇总 workflow 漂移 + 命令 + 元数据血缘治理(P2-21,生成单一治理结论) +python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --output .claude/reports/linux_unified_gate/ci_workflow_governance.json + +# 发布治理结论为 CI 可消费决策(P2-22,生成 should_execute_workflow 与发布报告) +python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --output-json .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_governance_publish.md + +# 将治理发布结果收敛为执行决策闸门(P2-23,统一 execute/blocked 判定与退出策略) +python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --on-block skip --output-json .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_execution_decision.md + +# 将执行决策收敛为可调度命令契约(P2-24,统一 dispatch-ready/blocked 与 gh workflow run 命令) +python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch.md + +# 执行调度契约并输出最终执行回执(P2-25,统一 dispatched/blocked/fail 的执行结果) +python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md + +# 将 dispatch 执行结果收敛为 run trace 契约(P2-27,统一 run_id/url/poll 命令与状态) +python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md + +# 等待 dispatch run 收敛为最终完成判定(P2-28,统一 completed/in_progress/timeout/failure 语义) +python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md + +# 发布 dispatch completion 的终局结论(P2-29,统一 promote/hold 发布语义) +python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md + +# 将 terminal publish 收敛为 release handoff 契约(P2-30,统一 release job 触发语义) +python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --target-environment production --release-channel stable --output-json .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_handoff.md + +# 执行 release 最终触发门禁(P2-31,统一 release dispatch 执行语义) +python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trigger.md + +# 追踪 release trigger 执行后的 run 语义(P2-32,统一 release run trace/poll 状态) +python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trace.md + +# 等待 release run 收敛为最终完成判定(P2-33,统一 release 完成态语义) +python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_completion.md + +# 发布 release completion 的终局结论(P2-34,统一 release finalize/hold 语义) +python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md + +# 将 release terminal publish 收敛为最终 finalization 决策(P2-35,统一 finalize/hold/abort 语义) +python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --on-hold pass --output-json .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_finalization.md + +# 将 release finalization 收敛为最终 closure 发布契约(P2-36,统一 close/notify/rollback 语义) +python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_closure.md + +# 将 release closure 收敛为最终证据归档契约(P2-37,统一 archive/publish/block 语义) +python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_archive.md + +# 将 release archive 收敛为最终发布判词契约(P2-38,统一 ship/hold/block 语义) +python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_verdict.md + +# 将 release verdict 收敛为 incident dispatch 执行契约(P2-39,统一告警触发语义) +python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --incident-repo acme/demo --incident-label release-incident --output-json .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_incident.md + +# 将 release incident 收敛为终局 terminal verdict 契约(P2-40,统一 ship/hold/escalate/block 语义) +python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md + +# 将 release terminal verdict 收敛为最终 delivery 契约(P2-41,统一 deliver/hold/escalate/block 语义) +python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery.md + +# P2-42: converge release delivery to terminal publish contract (announce_release/announce_hold/announce_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md + +# P2-43: converge release delivery terminal publish to final verdict contract (close_release/open_follow_up/escalate_blocker/abort_close). +python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md + + +# P2-44: converge release delivery final verdict to follow-up dispatch contract (no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch). +python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md + +# P2-45: converge release follow-up dispatch to follow-up closure contract (no_action/queue_follow_up/queue_escalation/abort_queue). +python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --follow-up-repo acme/demo --follow-up-label release-follow-up --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md + +# P2-46: converge release follow-up closure to terminal publish contract (announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md + +# P2-47: converge release follow-up terminal publish to final verdict contract (close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close). +python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md + +# P2-48: converge delivery/follow-up final verdicts to one release final outcome contract (ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome). +python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md + +# P2-49: converge release final outcome to terminal publish contract (announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md + +# P2-50: converge release final terminal publish to final handoff contract (handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff). +python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md + +# P2-51: converge release final handoff to final closure contract (close_release/close_with_follow_up/close_blocker/abort_close). +python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md + +# P2-52: publish release final closure contract (announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md + +# P2-53: archive release final closure publish contract (archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive). +python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md + +# P2-54: converge release final archive to final verdict contract (ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict). +python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md + +# P2-55: publish release final verdict contract (announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md + +# P2-56: archive release final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive). +python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md + +# P2-57: close gate-script/runtime-test/manifest drift contract. +python scripts/run_p2_linux_gate_manifest_drift_gate.py --dry-run --print-report +python scripts/run_p2_linux_gate_manifest_drift_gate.py --output-json .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-markdown .claude/reports/linux_unified_gate/linux_gate_manifest_drift.md + +# P2-59: converge P2-56+P2-57 to one Linux terminal verdict contract. +python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md + +# P2-60: dispatch Linux validation command from P2-59 terminal verdict contract. +python scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --linux-validation-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md + +# P2-61: converge Linux validation dispatch into one final Linux validation verdict contract. +python scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md + +# P2-62: publish Linux validation verdict contract (announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish). +python scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md + +# P2-63: terminal publish Linux validation verdict publish contract (announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish). +python scripts/run_p2_linux_ci_workflow_linux_validation_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --dry-run --print-report +python scripts/run_p2_linux_ci_workflow_linux_validation_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md + +# 一键串联 P2-17 -> P2-63 CI workflow 全链门禁(P2-26,Linux CI 总入口) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --dry-run +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --fail-fast --sync-write --command-guard-write --dispatch-trace-poll-now --completion-poll-interval-seconds 20 --completion-max-polls 15 --target-environment production --release-channel stable --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --release-trace-poll-now --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --on-release-hold pass --follow-up-repo acme/demo --follow-up-label release-follow-up +# 仅执行后半链(从治理发布开始) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance +# 仅执行 P2-29 终局发布(复用已产出的 completion 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion +# 仅执行 P2-30 release handoff(复用已产出的 terminal publish 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --target-environment staging --release-channel canary --skip-release-trigger --skip-release-trace --skip-release-completion +# 仅执行 P2-31 release trigger(复用已产出的 release handoff 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --skip-release-trace --skip-release-completion +# 仅执行 P2-32 release trace(复用已产出的 release trigger 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --release-trace-poll-now --skip-release-completion +# 仅执行 P2-33 release completion(复用已产出的 release trace 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --skip-release-terminal-publish +# 仅执行 P2-34 release terminal publish(复用已产出的 release completion 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-finalization --skip-release-closure --skip-release-archive +# 仅执行 P2-35 release finalization(复用已产出的 release terminal publish 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --on-release-hold fail --skip-release-closure --skip-release-archive +# 仅执行 P2-36 release closure(复用已产出的 release finalization 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-archive +# 仅执行 P2-37 release archive(复用已产出的 release closure 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-verdict --skip-release-incident +# 仅执行 P2-38 release verdict(复用已产出的 release archive 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-incident +# 仅执行 P2-39 release incident(复用已产出的 release verdict 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-terminal-verdict --incident-repo acme/demo --incident-label release-incident +# 仅执行 P2-40 release terminal verdict(复用已产出的 release incident 报告) +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch +# P2-42 only: run release delivery terminal publish (reuse release delivery report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch +# P2-43 only: run release delivery final verdict (reuse release delivery terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-follow-up-dispatch +# P2-44 only: run release follow-up dispatch (reuse release delivery final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict +# P2-45 only: run release follow-up closure (reuse release follow-up dispatch report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up + +# P2-46 only: run release follow-up terminal publish (reuse release follow-up closure report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up + +# P2-47 only: run release follow-up final verdict (reuse release follow-up terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --follow-up-repo acme/demo --follow-up-label release-follow-up +# P2-48 only: run release final outcome (reuse P2-43 + P2-47 final verdict reports). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict +# P2-49 only: run release final terminal publish (reuse P2-48 final outcome report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish +# P2-50 only: run release final handoff (reuse P2-49 final terminal publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-closure --skip-release-final-closure-publish +# P2-51 only: run release final closure (reuse P2-50 final handoff report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure-publish +# P2-52 only: run release final closure publish (reuse P2-51 final closure report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-archive + +# P2-53 only: run release final archive (reuse P2-52 final closure publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish +# P2-54 only: run release final verdict (reuse P2-53 final archive report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive +# P2-55 only: run release final verdict publish (reuse P2-54 final verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict +# P2-56 only: run release final publish archive (reuse P2-55 final verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish +# P2-57 only: run gate manifest drift closure (independent closure check over scripts/tests/manifest). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive +# P2-59 only: run terminal verdict closure (reuse P2-56 + P2-57 reports). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift +# P2-60 only: run Linux validation dispatch (reuse P2-59 terminal verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --linux-validation-timeout-seconds 7200 +# P2-61 only: run Linux validation verdict (reuse P2-60 Linux validation dispatch report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch +# P2-62 only: run Linux validation verdict publish (reuse P2-61 Linux validation verdict report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict +# P2-63 only: run Linux validation terminal publish (reuse P2-62 Linux validation verdict publish report). +python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish +# 语法检查 +python scripts/check_syntax.py --root claude_code + +# 运行性能 smoke +python -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py + +# 生成性能基线 +python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json +``` + +本次整理时实际复核过: + +```bash +python -m pytest -q -c pytest.ini +``` + +结果为 `90 passed`。 + +## 配置与运行时文件 + +当前代码里常见的配置与运行时文件分布如下: + +| 位置 | 用途 | +|---|---| +| `~/.claude-code-python/base.json` | 全局基础配置 | +| `~/.claude-code-python/{env}.json` | 按环境分层配置 | +| `~/.claude-code-python/config.json` | 用户手工覆盖配置 | +| `.claude-code-python.json` | 项目本地设置 | +| `.claude/` | 项目运行数据目录,常见内容包括任务、计划、技能、hooks、todo、team、scheduled tasks | +| `.mcp.json` | MCP 相关配置 | +| `CLAUDE.md` / `CLAUDE.local.md` | 项目指导信息,由 `/init` 初始化 | + +如果你要排查任务系统,优先关注: + +- `.claude/tasks.json` +- `.claude/runtime_tasks.json` +- `.claude/scheduled_tasks.json` + +## 示例与入口文件 + +- `examples/basic_usage.py`:基础调用示例 +- `examples/configuration.py`:配置示例 +- `examples/custom_tools.py`:自定义工具示例 +- `examples/mcp_integration.py`:MCP 集成示例 +- `examples/session_management.py`:会话管理示例 + +## 后续文档维护规则 + +为了避免文档再次膨胀,后续建议遵守以下规则: + +1. README 只做入口、摘要和导航,不再追加大型专题说明。 +2. 同一主题最多保留一个“当前主文档”和一个“历史补充文档”。 +3. 新的评估/总结文档必须带日期,并明确自己是否取代旧文档。 +4. 超过一页的专题内容直接放到 `docs/`,README 只写摘要和链接。 +5. 如果某个专题已经有主文档,后续补充优先更新原文档,而不是新建平行文档。 + +## 结论 + +这次整理后,`README.md` 不再承担“所有知识都写在一处”的职责,而是成为仓库的稳定入口: + +- 想运行项目,先看“快速开始” +- 想看当前架构/性能/中间件状态,直接看对应主文档 +- 想深入 Agent 或 MCP,再进入大文档 +- 想追溯历史问题和修复方案,再看历史文档 + +这就是当前仓库文档的推荐阅读顺序和维护边界。 + + + + diff --git a/.claude/blob_f42_sample.txt b/.claude/blob_f42_sample.txt new file mode 100644 index 0000000..f42ff82 --- /dev/null +++ b/.claude/blob_f42_sample.txt @@ -0,0 +1,2974 @@ +# Claude Code Python 鎵ц璁″垝涓庝换鍔″崱鍗曪紙2026-04-29锛? +## 1. 鏂囨。瀹氫綅 + +- 鏂囨。鎬ц川锛氭墽琛岃鍒?+ 浠诲姟鍗″崟 +- 涓婃父鏂囨。锛歔CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md](D:/Download/gaming/new_program/claude-code-python/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md) +- 浣跨敤鏂瑰紡锛? - 浜у搧銆佹灦鏋勩€佺爺鍙戙€佹祴璇曢兘浠ユ湰鏂囦欢浣滀负杩戞湡瀹炴柦娓呭崟 + - 姣忓紶浠诲姟鍗″繀椤昏兘鐙珛寤哄垎鏀€佺嫭绔嬪紑鍙戙€佺嫭绔嬮獙璇併€佺嫭绔嬪悎骞? - 鑻ヤ竴寮犲崱鏃犳硶鍦ㄤ竴涓槑纭竟鐣屽唴瀹屾垚锛屽垯缁х画鍚戜笅鎷? +## 2. 褰撳墠鎷嗚В鍘熷垯 + +### 2.1 鎷嗗崱鍘熷垯 + +姣忓紶鍗″繀椤诲悓鏃跺洖绛?5 涓棶棰橈細 + +1. 瑙e喅浠€涔堥棶棰橈紵 +2. 瑕佸仛浠€涔堢爺鍙戝姛鑳斤紵 +3. 瑕佸仛鍒颁粈涔堢▼搴︽墠绠楀畬鎴愶紵 +4. 娴嬭瘯浠g爜瑕佽ˉ鍒颁粈涔堢▼搴︼紵 +5. 鏈変粈涔堝墠缃緷璧栧拰鏄庣‘闈炵洰鏍囷紵 + +### 2.2 瀹屾垚瀹氫箟锛圖efinition of Done锛? +姣忓紶鍗¢粯璁ゅ畬鎴愭爣鍑嗭細 + +- 浠g爜涓婚摼鍙繍琛?- 鏂板鎴栦慨鏀规祴璇曢€氳繃 +- 瀵瑰簲鏂囨。鍚屾鏇存柊 +- 涓嶅紩鍏ユ柊鐨勬湭瑙i噴鑴忕姸鎬?- 鑷冲皯瀹屾垚涓€杞湰鍦版墜宸ラ獙鏀? +### 2.3 娴嬭瘯鍒嗗眰鏍囧噯 + +涓洪伩鍏嶁€滃啓浜嗙偣娴嬭瘯鈥濅絾娌℃湁楠屾敹浠峰€硷紝缁熶竴閲囩敤浠ヤ笅娣卞害鍒嗗眰锛? +| 灞傜骇 | 鍚嶇О | 鐩殑 | 鏈€浣庤姹?| +|---|---|---|---| +| T1 | Unit | 绾嚱鏁般€佺姸鎬佹満銆佽緭鍏ヨ緭鍑鸿竟鐣?| 姝e悜 + 璐熷悜 + 杈圭晫 | +| T2 | Component | 鍗曟ā鍧楃紪鎺掋€佸甫 fake/mocks 鐨勮涓洪獙璇?| 鑷冲皯瑕嗙洊 1 鏉′富閾?+ 1 鏉″紓甯搁摼 | +| T3 | Integration | 璺ㄦā鍧楄仈鍔紝楠岃瘉鐪熷疄渚濊禆杈圭晫 | 鑷冲皯瑕嗙洊 1 鏉$鍒扮涓氬姟閾?| +| T4 | Regression | 澶嶇幇鍘嗗彶缂洪櫡锛岄槻姝㈠洖褰?| 姣忎釜宸茬煡 bug 鑷冲皯 1 鏉″洖褰掔敤渚?| +| T5 | Manual Smoke | 浜哄伐蹇€熼獙鏀?| 鍛戒护绾?鍏ュ彛绾у疄闄呰繍琛?| + +### 2.4 娴嬭瘯浜х墿瑕佹眰 + +姣忓紶浠g爜鍗¤嚦灏戞弧瓒筹細 + +- 鏂板鎴栦慨鏀?`T1/T2` 娴嬭瘯 +- 鑻ュ彉鏇村奖鍝?CLI銆乻ession銆乼ask銆丮CP銆乭ooks銆乤gent 涓婚摼锛屽垯蹇呴』琛?`T3` +- 鑻ヤ换鍔″崱婧愪簬宸茬煡缂洪櫡锛屽垯蹇呴』琛?`T4` +- 杈撳嚭 1 浠芥渶灏忔墜宸ラ獙鏀跺懡浠ゆ竻鍗? +## 3. 瀹炴柦闃舵鎬昏 + +## 3.1 Phase 0锛氭琛€涓庢敹鏁? +鐩爣锛? +- 淇帀鈥滅湅璧锋潵鏈夈€佸疄闄呮病鎵撻€氣€濈殑鍋囬棴鐜?- 寤虹珛鍞竴涓昏繍琛屾椂鍏ュ彛 +- 寤虹珛绋冲畾鐨勬湰鍦板紑鍙?娴嬭瘯鏂藉伐闈? +## 3.2 Phase 1锛氬钩鍙板寲 + +鐩爣锛? +- 浠庡崟杩涚▼ CLI 婕旇繘涓哄彲澶嶇敤鐨勬墽琛屽悗绔?- 琛ラ綈 event journal銆乻tate backend銆乤ctive memory + +## 3.3 Phase 2锛氳秴杞? +鐩爣锛? +- 寤虹珛澶?Agent 鎺у埗闈€両DE 瀹㈡埛绔€丆I 宸ヤ綔娴併€佺粍缁囩骇娌荤悊鑳藉姏 + +## 4. 渚濊禆椤哄簭鍥? +```text +P0-01 Runtime Backbone + -> P0-02 Session Resume + -> P0-03 Auth/Config + -> P0-04 Feature Flags + -> P0-05 Review Command + -> P0-06 Session/History/Memory Boundaries + -> P0-07 Hooks Runtime + -> P0-08 Windows Bootstrap + -> P0-09 Temp Artifact Hygiene + -> P0-10 Phase0 Regression Gate + +P1-01 Daemon/API Control Plane + -> P1-02 Event Journal + -> P1-03 SQLite State Backend + -> P1-04 Active Memory Runtime + -> P1-05 Hook Policy/Audit Convergence + -> P1-06 CLI Thin Client Migration + +P2-01 Agent Supervisor + -> P2-02 Artifact Bus + -> P2-03 IDE Integration + -> P2-04 GitHub/CI Workflow + -> P2-05 Org Policy & Audit + -> P2-06 Linux Unified Verification Gate + -> P2-07 Agent Runtime Parity + -> P2-08 Custom Agents Directory Loader + -> P2-09 Linux Unified Execution Gate + -> P2-10 JetBrains IDE Integration + -> P2-11 Linux Sharded Execution Gate + -> P2-12 Linux Shard Aggregation Gate + -> P2-13 Linux Final Report Publish Gate + -> P2-14 Linux Unified Pipeline Orchestration Gate + -> P2-15 Linux Shard Plan Generation Gate + -> P2-16 Linux CI Matrix Export Gate + -> P2-17 Linux CI Workflow Plan Gate + -> P2-18 Linux CI Workflow YAML Render Gate + -> P2-19 Linux CI Workflow Drift Sync Gate + -> P2-20 Linux CI Workflow Command Guard Gate + -> P2-21 Linux CI Workflow Governance Convergence Gate + -> P2-22 Linux CI Workflow Governance Publish Gate + -> P2-23 Linux CI Workflow Execution Decision Gate + -> P2-24 Linux CI Workflow Dispatch Readiness Gate + -> P2-25 Linux CI Workflow Dispatch Execution Gate + -> P2-26 Linux CI Workflow Full Pipeline Gate + -> P2-27 Linux CI Workflow Dispatch Traceability Gate + -> P2-28 Linux CI Workflow Dispatch Completion Gate + -> P2-29 Linux CI Workflow Terminal Publish Gate + -> P2-30 Linux CI Workflow Release Handoff Gate + -> P2-31 Linux CI Workflow Release Trigger Gate + -> P2-32 Linux CI Workflow Release Traceability Gate + -> P2-33 Linux CI Workflow Release Completion Gate + -> P2-34 Linux CI Workflow Release Terminal Publish Gate + -> P2-35 Linux CI Workflow Release Finalization Gate + -> P2-36 Linux CI Workflow Release Closure Publish Gate + -> P2-37 Linux CI Workflow Release Archive Publish Gate + -> P2-38 Linux CI Workflow Release Verdict Publish Gate + -> P2-39 Linux CI Workflow Release Incident Dispatch Gate + -> P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate + -> P2-41 Linux CI Workflow Release Delivery Closure Gate + -> P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate + -> P2-43 Linux CI Workflow Release Delivery Final Verdict Gate + -> P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate + -> P2-45 Linux CI Workflow Release Follow-Up Closure Gate + -> P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate + -> P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate + -> P2-48 Linux CI Workflow Release Final Outcome Gate + -> P2-49 Linux CI Workflow Release Final Terminal Publish Gate + -> P2-50 Linux CI Workflow Release Final Handoff Gate + -> P2-51 Linux CI Workflow Release Final Closure Gate + -> P2-52 Linux CI Workflow Release Final Closure Publish Gate + -> P2-53 Linux CI Workflow Release Final Archive Gate + -> P2-54 Linux CI Workflow Release Final Verdict Gate + -> P2-55 Linux CI Workflow Release Final Verdict Publish Gate + -> P2-56 Linux CI Workflow Release Final Publish Archive Gate + -> P2-57 Linux Gate Manifest Drift Closure Gate + -> P2-59 Linux CI Workflow Terminal Verdict Closure Gate + -> P2-60 Linux CI Workflow Linux Validation Dispatch Gate + -> P2-61 Linux CI Workflow Linux Validation Verdict Gate + -> P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate + -> P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate +``` + +## 5. Phase 0 浠诲姟鍗″崟 + +## P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦 + +- 鍗″彿锛歚P0-01` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `main.py`銆乣app.py`銆乻ession/task/hooks/memory 鍒濆鍖栭摼鏉″垎鏁? - 瀛愮郴缁熷瓨鍦ㄤ絾鏈舰鎴愬敮涓€杩愯鏃堕棴鐜?- 鐮斿彂鍔熻兘锛? - 寤虹珛缁熶竴 runtime bootstrap + - 鏄庣‘ `main -> Application -> QueryEngine -> Session -> TaskManager -> Hooks -> Memory` + - 娑堥櫎閲嶅鍒濆鍖栬矾寰勪笌闅愬紡鍏ㄥ眬鐘舵€佷緷璧?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - `claude_code/app.py` + - `claude_code/engine/query.py` + - 瑙嗛渶瑕佹柊澧?`claude_code/runtime/` 鎴栫瓑浠锋ā鍧?- 瀹屾垚绋嬪害锛? - REPL銆乻ingle query銆乸ipe銆丮CP serve 鍏辩敤鍚屼竴 bootstrap 娴? - runtime 鍏抽敭瀵硅薄鏈夊敮涓€鏋勫缓璺緞 + - 鍚庣画鍗″崟涓嶅啀渚濊禆鈥滃悇鑷伔鍋峰垵濮嬪寲鈥?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歜ootstrap 鍙傛暟瑙f瀽銆乺untime object 鏋勫缓杈圭晫 + - `T2`锛氫笉鍚岃繍琛屾ā寮忎笅 runtime 缁勪欢瑁呴厤涓€鑷存€? - `T3`锛歊EPL / pipe / single query 鑷冲皯鍚勬湁 1 鏉′富閾?smoke + - `T4`锛氳鐩栤€滄煇妯″紡婕忓垵濮嬪寲鏌愭湇鍔♀€濈殑鍘嗗彶鏂偣 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_runtime_bootstrap.py` + - 鎵╁睍 `tests/test_main_runtime.py` +- 鎵嬪伐楠屾敹锛? - `python -m claude_code.main --help` + - `python -m claude_code.main --doctor` + - `echo hi | python -m claude_code.main --pipe` +- 闈炵洰鏍囷細 + - 鏈崱涓嶅紩鍏?daemon/API + - 鏈崱涓嶅鐞嗙粍缁囩骇绛栫暐 +- 鍓嶇疆渚濊禆锛氭棤 + +## P0-02 /resume 鐪熼棴鐜? +- 鍗″彿锛歚P0-02` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `QueryEngine.resume_session()` 宸插瓨鍦紝浣?`/resume` 鍛戒护娌℃湁鐪熸璋冪敤鎭㈠閾捐矾 + - session 鎭㈠鏄€滄枃妗堝姛鑳解€濓紝涓嶆槸鈥滆繍琛屾椂鍔熻兘鈥?- 鐮斿彂鍔熻兘锛? - 灏?`/resume [session_id]` 鐪熸鎺ュ叆 `QueryEngine.resume_session()` + - 鏃犲弬鏁版椂鍒楁渶杩?session锛涙湁鍙傛暟鏃舵仮澶嶆寚瀹?session 骞舵帴缁璇? - 鏄庣‘鎭㈠鍚庣殑 session id銆乵essage list銆亀orking directory 琛屼负 +- 瀹炴柦鑼冨洿锛? - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/repl/__init__.py` +- 瀹屾垚绋嬪害锛? - `/resume` 涓嶅啀鍙繑鍥炶鏄庢枃瀛? - 鎭㈠鍚庝笅涓€杞?query 鑳芥帴鐫€鍘嗗彶娑堟伅杩愯 + - 鎭㈠澶辫触鏃舵湁鍙瘖鏂敊璇俊鎭?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ession list / load 杈圭晫 + - `T2`锛歝ommand -> engine 鎭㈠閾捐矾 + - `T3`锛氬垱寤?session銆佹寔涔呭寲銆佹仮澶嶃€佺户缁棶绛旂殑涓€鏉″畬鏁撮摼 + - `T4`锛氭棤 session / session_id 涓嶅瓨鍦?/ 浼氳瘽鏂囦欢鎹熷潖 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_query_engine_runtime.py` + - 鎵╁睍 `tests/test_main_runtime.py` + - 鏂板 `tests/test_resume_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鍚姩涓€娆′細璇濆苟鐢熸垚鎸佷箙鍖? - 鏂拌繘绋嬫墽琛?`/resume` + - 纭鎭㈠鍚庣户缁璇?- 闈炵洰鏍囷細 + - 涓嶅紩鍏ヨ法鏈哄櫒鎭㈠ + - 涓嶅紩鍏?session 鎼滅储 UI +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-03 auth/config 涓婚摼淇涓庣粺涓€ + +- 鍗″彿锛歚P0-03` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `logout` / `auth` 浣跨敤鏈鍏ョ殑 `ConfigManager` + - `Config` 涓?`ConfigManager` 鍙岃建骞跺瓨锛岃璇佺姸鎬佸垽瀹氫笉绋冲畾 +- 鐮斿彂鍔熻兘锛? - 閫夊畾 canonical config 璇诲啓璺緞 + - 淇 `/login`銆乣/logout`銆乣/auth` + - 缁熶竴 env/config/key masking 瑙勫垯 +- 瀹炴柦鑼冨洿锛? - `claude_code/commands/auth/__init__.py` + - `claude_code/config.py` + - `claude_code/utils/config_manager.py` + - 瑙嗛渶瑕佽皟鏁?`claude_code/main.py` +- 瀹屾垚绋嬪害锛? - `auth` 閾捐矾鏃?`NameError` + - 璁よ瘉鐘舵€佽緭鍑轰笌瀹為檯 env/config 涓€鑷? - logout 鑳芥竻鐞?runtime 鍙鐨?key 鏉ユ簮 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nv > config > default 浼樺厛绾? - `T2`锛歛uth commands 鍦ㄤ笉鍚?key 鏉ユ簮涓嬬殑琛屼负 + - `T3`锛氬懡浠ょ骇鐘舵€佸彉鍖栭獙璇? - `T4`锛氭棤 key / config 缂哄け / key mask 杈撳嚭 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_commands_auth_runtime.py` + - 鎵╁睍 `tests/test_main_runtime.py` +- 鎵嬪伐楠屾敹锛? - 璁剧疆 env key + - 杩愯 `/auth` + - 杩愯 `/logout` + - 鍐嶆杩愯 `/auth` +- 闈炵洰鏍囷細 + - 涓嶅紩鍏?OAuth 鐪熺櫥褰曟祦 + - 涓嶆帴绗笁鏂?secrets manager +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-04 feature flag 杩愯鏃朵慨澶? +- 鍗″彿锛歚P0-04` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `FeatureConfig` 涓?frozen dataclass锛屼絾 `enable()/disable()` 璇曞浘鍘熷湴淇敼 + - 褰撳墠 flag 鏈哄埗瀵规湭鏉ュぇ鍔熻兘鍒囨崲涓嶅彲闈?- 鐮斿彂鍔熻兘锛? - 閲嶆瀯 feature registry 鐨勫唴閮ㄧ姸鎬佽〃杈? - 淇濇寔 env fallback锛屽悓鏃跺厑璁哥▼搴忓唴瀹夊叏鍒囨崲 + - 鏄庣‘ feature 璇诲彇涓庡啓鍏ヨ涔?- 瀹炴柦鑼冨洿锛? - `claude_code/features.py` +- 瀹屾垚绋嬪害锛? - `enable()` / `disable()` 涓嶆姏 `FrozenInstanceError` + - registry 鍐呭琛屼负涓€鑷? - flags 鍙綔涓哄悗缁ぇ鍗$殑 gated rollout 鏈哄埗 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nable / disable / env override / unknown feature + - `T2`锛歳egistry 瀹炰緥鍦ㄥ娆¤皟鐢ㄤ腑鐨勭姸鎬佷竴鑷存€? - `T4`锛氬鐜板綋鍓?`FrozenInstanceError` 鐨勫洖褰掔敤渚?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_features_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鏈湴鑴氭湰鍒囨崲 feature 骞惰鍙?- 闈炵洰鏍囷細 + - 涓嶅紩鍏ヨ繙绋?flag service +- 鍓嶇疆渚濊禆锛氭棤 + +## P0-05 review 鍛戒护浜у搧鍖? +- 鍗″彿锛歚P0-05` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `/review` 鍙槸寮曞鏂囨锛屼笉鎵ц鐪熷疄瀹℃煡 + - 鐢ㄦ埛鏃犳硶渚濊禆璇ュ懡浠ゆ鏌?diff / 鏂囦欢椋庨櫓 +- 鐮斿彂鍔熻兘锛? - 鏀寔 review 褰撳墠 git diff + - 鏀寔 review 鎸囧畾鏂囦欢 + - 缁熶竴 findings 杈撳嚭鏍煎紡锛氶棶棰樸€佷綅缃€佷弗閲嶅害銆佸缓璁? - 鏄庣‘鈥滄棤鍙戠幇鈥濊緭鍑?- 瀹炴柦鑼冨洿锛? - `claude_code/commands/review/__init__.py` + - 瑙嗛渶瑕佹柊澧?`claude_code/services/review_service.py` + - 鍙帴鍏?`git diff` / read-only analyzer +- 瀹屾垚绋嬪害锛? - `/review` 涓嶅啀杩斿洖鍗犱綅 prompt + - 鑳藉褰撳墠鏀瑰姩鎴栬緭鍏ユ枃浠剁粰鍑虹粨鏋勫寲缁撴灉 + - 杈撳嚭鍙綔涓哄悗缁?PR/CI review 鐨勯洀褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬弬鏁拌В鏋愩€佹棤杈撳叆榛樿琛屼负 + - `T2`锛歴ervice 瀵?fake diff 鐨?findings 鐢熸垚 + - `T3`锛氫复鏃?git repo 涓殑 review 涓婚摼 + - `T4`锛氭棤 diff / 鏂囦欢涓嶅瓨鍦?/ 绌虹粨鏋?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_review_command_runtime.py` +- 鎵嬪伐楠屾敹锛? - 淇敼涓€涓枃浠跺悗鎵ц `/review` + - 鎸囧畾鏂囦欢鎵ц `/review path` +- 闈炵洰鏍囷細 + - 涓嶅仛缁勭粐绾?code review dashboard + - 涓嶅己琛屾帴 GitHub API +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-06 session/history/memory 鑱岃矗鏀舵暃 + +- 鍗″彿锛歚P0-06` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `Session`銆乣HistoryManager`銆乣SessionMemory` 鑱岃矗浜ゅ弶 + - 鐭湡瀵硅瘽銆侀暱鏈熻蹇嗐€佸綊妗e巻鍙茶竟鐣屼笉娓?- 鐮斿彂鍔熻兘锛? - 鏄庣‘涓夊眰鑱岃矗锛? - `Session`锛氬綋鍓嶄細璇濅簨瀹? - `History`锛氬凡缁撴潫/宸蹭繚瀛樼殑娑堟伅褰掓。 + - `Memory`锛氶暱鏈熺粨鏋勫寲鐭ヨ瘑 + - 娑堥櫎閲嶅淇濆瓨涓庝笉涓€鑷村姞杞借矾寰? - 涓?P1 active memory 閾哄簳 +- 瀹炴柦鑼冨洿锛? - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` +- 瀹屾垚绋嬪害锛? - 涓夎€呰亴璐d笌璋冪敤杈圭晫鍐欐竻 + - 褰撳墠涓婚摼鍙繚鐣欎竴涓?message persistence source of truth + - memory 涓嶅啀闅愬紡鎵挎媴 session/history 瑙掕壊 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ave/load/export/search 杈圭晫 + - `T2`锛歴ession -> persisted session 琛屼负 + - `T3`锛歝ontext builder 璇诲彇 memory/session 鐨勮仈鍔? - `T4`锛氶槻姝㈤噸澶嶆秷鎭€侀噸澶嶆寔涔呭寲銆侀敊璇仮澶?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_session_history_memory_runtime.py` + - 鎵╁睍 `tests/test_context_builder_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鐢熸垚浼氳瘽 + - 閲嶅惎鍔犺浇 + - 楠岃瘉 memory 涓嶈璇啓涓鸿亰澶╁巻鍙?- 闈炵洰鏍囷細 + - 涓嶅仛鍚戦噺鏁版嵁搴? - 涓嶅仛闀挎湡妫€绱㈠寮?- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-07 hooks 鎺ュ叆涓昏繍琛屾椂 + +- 鍗″彿锛歚P0-07` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - hooks command銆乭ooks manager銆乼ool/command 鎵ц閾炬病鏈夌湡姝h仈鍔? - hooks 鐩墠鏇村儚闈欐€侀厤缃紝鑰屼笉鏄繍琛屾椂鑳藉姏 +- 鐮斿彂鍔熻兘锛? - 鍦?tool execution 鍓嶅悗瑙﹀彂 hooks + - 鍦?command 鎵ц鍓嶅悗瑙﹀彂 hooks + - 鍦ㄩ敊璇矾寰勮Е鍙?`on_error` + - 绾﹀畾 hook context payload +- 瀹炴柦鑼冨洿锛? - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - 瑙嗛渶瑕佹帴鍏?command dispatch 灞?- 瀹屾垚绋嬪害锛? - hooks 閰嶇疆鑳界湡瀹炵敓鏁? - disabled hook銆佽秴鏃?hook銆佸け璐?hook 琛屼负娓呮櫚 + - 瀵逛富閾惧け璐ヤ笉浜х敓涓嶅彲鎺у壇浣滅敤 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歨ook load/save/parse + - `T2`锛歵rigger before_tool / after_tool / on_error + - `T3`锛氱湡瀹?tool 璋冪敤閾捐Е鍙?hook + - `T4`锛歨ook timeout / hook command failure / disabled hook +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_hooks_runtime.py` +- 鎵嬪伐楠屾敹锛? - 閰嶇疆涓€涓畝鍗?echo hook + - 鎵ц read / grep / bash + - 纭 hook 琚Е鍙?- 闈炵洰鏍囷細 + - 涓嶅仛 org-wide hook registry +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-08 Windows 鍚姩涓庤В閲婂櫒鎺㈡祴绋冲畾鍖? +- 鍗″彿锛歚P0-08` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - 绯荤粺 `python` 鎸囧悜 WindowsApps stub锛岀敤鎴峰鏄撹窇鍒板亣瑙i噴鍣? - Windows 鐢ㄦ埛鍚姩閾捐剢寮?- 鐮斿彂鍔熻兘锛? - 鍦?`--doctor` 涓瘑鍒В閲婂櫒鏉ユ簮 + - 瀵?WindowsApps stub 缁欏嚭鏄庣‘璀﹀憡 + - 缁熶竴鎺ㄨ崘鍚姩鍣?/ wrapper 琛屼负 + - 鏂囨。鍚屾鏄庣‘ Windows 鎺ㄨ崘杩愯娉?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - `README.md` + - 鍙€夋柊澧?`scripts/` 涓嬭緟鍔╁惎鍔ㄨ剼鏈?- 瀹屾垚绋嬪害锛? - doctor 杈撳嚭鑳借瘖鏂В閲婂櫒椋庨櫓 + - Windows 鐢ㄦ埛涓嶄細璇互涓?stub 鍙敤 + - 椤圭洰鍏ュ彛鏂囨。娓呮鏍囨敞鎺ㄨ崘瑙i噴鍣?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氳В閲婂櫒璺緞璇嗗埆閫昏緫 + - `T2`锛歞octor 杈撳嚭鍒嗘敮 + - `T4`锛歐indowsApps stub 鍥炲綊鐢ㄤ緥 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_doctor_runtime.py` +- 鎵嬪伐楠屾敹锛? - 妯℃嫙 / 妫€鏌?doctor 杈撳嚭 +- 闈炵洰鏍囷細 + - 涓嶅仛瀹屾暣瀹夎鍣?- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-09 娴嬭瘯涓存椂浜х墿娓呯悊涓庨殧绂? +- 鍗″彿锛歚P0-09` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - `tests/.tmp_plugins/` 瀛樺湪娈嬬暀鐩綍 + - 娴嬭瘯闅旂鎬т笌浠撳簱鏁存磥鎬т笉瓒?- 鐮斿彂鍔熻兘锛? - 淇 plugin 娴嬭瘯鐨勬竻鐞嗛€昏緫 + - 澧炶ˉ `.gitignore` 鎴栨祴璇?teardown + - 纭繚 pytest 缁撴潫鍚庝笉鏂板鑴忕洰褰?- 瀹炴柦鑼冨洿锛? - `tests/test_plugins_runtime.py` + - `.gitignore` + - 瑙嗛渶瑕佽皟鏁?plugin fixture +- 瀹屾垚绋嬪害锛? - 鏈湴瀹屾暣 pytest 鍚庝笉鍐嶆畫鐣欐柊澧?`.tmp_*` / `.tmp_plugins/*` + - 娴嬭瘯浜掍笉姹℃煋 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歠ixture cleanup 鏂█ + - `T4`锛氶噸澶嶈繍琛?pytest 涓嶇疮璁℃畫鐣?- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_plugins_runtime.py` +- 鎵嬪伐楠屾敹锛? - 杩炵画璺戜袱娆?pytest + - 妫€鏌ュ伐浣滃尯鑴忔枃浠?- 闈炵洰鏍囷細 + - 涓嶆竻鐞嗙敤鎴风湡瀹炶繍琛屾暟鎹?- 鍓嶇疆渚濊禆锛氭棤 + +## P0-10 Phase 0 鍥炲綊闂ㄧ + +- 鍗″彿锛歚P0-10` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - 鐜版湁娴嬭瘯鍒嗗竷杈冩暎锛屾病鏈夊洿缁?Phase 0 涓婚摼寤虹珛鏄庣‘闂ㄧ +- 鐮斿彂鍔熻兘锛? - 瀹氫箟涓€缁勫繀椤婚€氳繃鐨?runtime regression tests + - 澧炲姞鎸変富閾惧垎缁勭殑 pytest marker 鎴栬剼鏈叆鍙? - 鏄庣‘鎻愪氦鍓嶅繀璺戝懡浠?- 瀹炴柦鑼冨洿锛? - `pytest.ini` + - `scripts/` 涓嬫牎楠岃剼鏈紙濡傞渶瑕侊級 + - 娴嬭瘯鏂囦欢鏍囪鏁寸悊 +- 瀹屾垚绋嬪害锛? - 鑳戒竴鏉″懡浠ゆ墽琛?P0 涓婚摼鍥炲綊 + - 鍥㈤槦鐭ラ亾鈥滃紑宸ュ墠/鍚堝苟鍓嶅繀椤昏窇浠€涔堚€?- 娴嬭瘯浠g爜瑕佹眰锛? - 鏈崱鏈川涓婃槸娴嬭瘯缁勭粐鍗? - 闇€瑕佽ˉ 1 涓祴璇曞叆鍙h剼鏈垨 marker 楠岃瘉 +- 寤鸿娴嬭瘯鏂囦欢锛? - 濡傞渶鏂板锛歚tests/test_phase0_regression_contract.py` +- 鎵嬪伐楠屾敹锛? - 鎵ц涓€鏉?phase0 gate 鍛戒护骞堕獙璇侀€氳繃 +- 闈炵洰鏍囷細 + - 涓嶈姹傛鏃舵帴 CI SaaS +- 鍓嶇疆渚濊禆锛? - `P0-02` ~ `P0-09` + +## 6. Phase 1 浠诲姟鍗″崟 + +## P1-01 daemon / API 鎺у埗闈? +- 鍗″彿锛歚P1-01` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - 褰撳墠 CLI 鏄帤瀹㈡埛绔紝IDE/desktop/web/CI 鏃犳硶鍏辩敤鎵ц鍚庣 +- 鐮斿彂鍔熻兘锛? - 鎻愪緵鏈湴 daemon/API 灞? - 鎶界 session銆乼ask銆乼ool execution銆乤rtifact 璁块棶鎺ュ彛 + - CLI 鏀逛负 daemon client 鐨勪笂灞傚叆鍙?- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/server/` 鎴栫瓑浠锋帶鍒堕潰妯″潡 + - 璋冩暣 `main.py` + - 澶嶇敤 `app.py` +- 瀹屾垚绋嬪害锛? - 鍗曟満鍙惎鍔?daemon + - CLI 涓?daemon 鍙叡浜竴濂楁墽琛屾牳蹇?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欰PI schema / request validation + - `T2`锛歴erver service 灞傝涓? - `T3`锛歝lient -> server -> runtime 涓€鏉″畬鏁撮摼 + - `T4`锛歞aemon 鏈惎鍔?/ 瓒呮椂 / 浼氳瘽涓嶅瓨鍦?- 鍓嶇疆渚濊禆锛歚P0-01`銆乣P0-10` + +## P1-02 event journal 浜嬩欢鏃ュ織 + +- 鍗″彿锛歚P1-02` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - prompt銆乼ool銆乼ask銆乸ermission 鍐崇瓥涓嶅彲鍥炴斁锛屼笉鍙璁?- 鐮斿彂鍔熻兘锛? - 瀹氫箟缁熶竴浜嬩欢妯″瀷 + - 鍐欏叆 prompt/message/tool/task/permission/artifact 浜嬩欢 + - 鎻愪緵鏌ヨ涓?replay 鍩虹鑳藉姏 +- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/services/event_journal.py` 鎴栫瓑浠锋ā鍧? - 鎺ュ叆 `QueryEngine`銆乣TaskManager` +- 瀹屾垚绋嬪害锛? - 鎵€鏈夊叧閿富閾惧姩浣滃彲浜х敓缁撴瀯鍖栦簨浠? - 鏀寔鎸?session 鏌ヨ浜嬩欢娴?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氫簨浠?schema銆佸簭鍒楀寲銆乿ersioning + - `T2`锛歸riter/reader correctness + - `T3`锛歲uery -> tool -> task 浜嬩欢閾? - `T4`锛氶儴鍒嗗啓鍏ュけ璐ャ€佷贡搴忔仮澶?- 鍓嶇疆渚濊禆锛歚P1-01` + +## P1-03 SQLite runtime state backend + +- 鍗″彿锛歚P1-03` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - file/memory backend 瀵规仮澶嶃€佹绱€佸瀹炰緥鍓嶅噯澶囦笉瓒?- 鐮斿彂鍔熻兘锛? - 涓?session/task/event 鎻愪緵 SQLite backend + - 淇濈暀 file backend 浣滀负鍏煎灞傛垨 fallback + - 鏄庣‘ migration 涓?schema version +- 瀹炴柦鑼冨洿锛? - `claude_code/tasks/factory.py` + - `claude_code/tasks/repository.py` + - session/event 鐩稿叧瀛樺偍妯″潡 +- 瀹屾垚绋嬪害锛? - 鑷冲皯鍙敤 SQLite 鎸佷箙鍖?task/session/event + - 鍗曟満閲嶅惎鍚庡彲鎭㈠鏍稿績杩愯鎬?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴chema init / migration + - `T2`锛歳epository CRUD contract + - `T3`锛氶噸鍚仮澶? - `T4`锛氭暟鎹簱鎹熷潖銆侀攣鍐茬獊銆侀噸澶嶅垵濮嬪寲 +- 鍓嶇疆渚濊禆锛歚P1-02` + +## P1-04 active memory 鎺ュ叆杩愯鏃? +- 鍗″彿锛歚P1-04` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - `SessionMemory` 鐜板湪鏄绔?KV锛屼笉褰卞搷涓诲璇濅笂涓嬫枃 +- 鐮斿彂鍔熻兘锛? - 璁?memory 涓?context builder/runtime 鑱斿姩 + - 瀹氫箟 memory scope锛歶ser/project/local + - 鏀寔鏄惧紡鍐欏叆銆佹寜闇€娉ㄥ叆銆佸懡涓粺璁?- 瀹炴柦鑼冨洿锛? - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` + - `claude_code/agents/builtin.py` +- 瀹屾垚绋嬪害锛? - memory 鎴愪负杩愯鏃剁湡瀹炶緭鍏ワ紝鑰岄潪鏃佽矾瀛樺偍 + - agent / main query 鍙€夋嫨涓嶅悓 memory scope +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歯amespace / key / search 琛屼负 + - `T2`锛歝ontext builder memory 娉ㄥ叆 + - `T3`锛歲uery 鎵ц鏃跺懡涓?memory + - `T4`锛氱┖ memory / 鎹熷潖 memory / scope 闅旂 +- 鍓嶇疆渚濊禆锛歚P0-06`銆乣P1-02` + +## P1-05 hooks / permission / audit 鏀舵暃 + +- 鍗″彿锛歚P1-05` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - hooks銆乸ermissions銆乪vent journal 浠嶆槸鍒嗙珛鑳藉姏 +- 鐮斿彂鍔熻兘锛? - 缁熶竴 hook 浜嬩欢銆乸ermission decision銆乤udit event + - 褰㈡垚涓€娆℃墽琛岀殑瀹屾暣瀹¤閾? - 涓烘湭鏉?org policy 閾哄簳 +- 瀹炴柦鑼冨洿锛? - `claude_code/permissions.py` + - `claude_code/services/hooks_manager.py` + - event journal 鐩稿叧妯″潡 +- 瀹屾垚绋嬪害锛? - 姣忔 tool call 鑷冲皯鍏峰 request -> decision -> hook -> result 鐨勯棴鐜褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歱ermission + hook + event 鑱斿姩 + - `T3`锛氱湡瀹?tool 鎵ц瀹¤閾? - `T4`锛歞eny / timeout / partial failure +- 鍓嶇疆渚濊禆锛歚P0-07`銆乣P1-02` + +## P1-06 CLI thin client 杩佺Щ + +- 鍗″彿锛歚P1-06` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - CLI 涓庢帶鍒堕潰鑰﹀悎杩囨繁锛屼笉鍒╀簬 IDE/CI 鍏辩敤 +- 鐮斿彂鍔熻兘锛? - CLI 鍙戣姹傜粰鏈湴 runtime server/daemon + - 淇濈暀鍗曡繘绋?fallback 妯″紡 + - 缁熶竴杈撳嚭涓庨敊璇涔?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - CLI/REPL client adapter +- 瀹屾垚绋嬪害锛? - CLI 浣滀负 runtime 鍓嶇瀛樺湪锛岃€岄潪鎵ц鍐呮牳鏈韩 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient adapter 琛屼负 + - `T3`锛欳LI -> daemon 瀹屾暣涓婚摼 + - `T4`锛歞aemon 涓嶅彲鐢?fallback +- 鍓嶇疆渚濊禆锛歚P1-01` + +## 7. Phase 2 浠诲姟鍗″崟 + +## P2-01 澶?Agent supervisor + +- 鍗″彿锛歚P2-01` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠 agent 鏇村儚鈥滃崟娆″瓙浠诲姟鎵ц鍣ㄢ€濓紝涓嶆槸鎺у埗闈?- 鐮斿彂鍔熻兘锛? - agent DAG + - budget / timeout / retry / ownership + - parent-child artifact passing +- 瀹屾垚绋嬪害锛? - 鑷冲皯鏀寔骞惰 agent + 姹囨€绘敹鍙?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歡raph / budget / timeout 鐘舵€佹満 + - `T2`锛歴upervisor 璋冨害 + - `T3`锛氬苟琛?agent 鎵ц + - `T4`锛氬瓙 agent 澶辫触鎭㈠ +- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02` + +## P2-02 artifact bus 涓庡啿绐佹敹鏁? +- 鍗″彿锛歚P2-02` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 澶?agent 杈撳嚭缂哄皯缁撴瀯鍖?artifact 浜ゆ崲灞?- 鐮斿彂鍔熻兘锛? - 缁熶竴 artifact schema + - patch / note / diff / report / finding 娴佽浆 + - 鍐茬獊妫€娴嬩笌鍚堝苟绛栫暐 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歛rtifact schema + - `T2`锛歱roducer/consumer + - `T3`锛氬 agent artifact 姹囨祦 + - `T4`锛氬啿绐佸悎骞跺け璐?- 鍓嶇疆渚濊禆锛歚P2-01` + +## P2-03 IDE 闆嗘垚锛圴S Code first锛? +- 鍗″彿锛歚P2-03` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠浠撳簱缂哄皯鐪熸鍙敤鐨?IDE workflow +- 鐮斿彂鍔熻兘锛? - VS Code client + - daemon 杩炴帴 + - diff銆乼ask銆乻ession銆乫indings 灞曠ず +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient protocol adapter + - `T3`锛欼DE -> daemon 鏍稿績閾? - `T5`锛氫汉宸ヤ氦浜掗獙鏀朵负涓?- 鍓嶇疆渚濊禆锛歚P1-06` + +## P2-04 GitHub / CI 宸ヤ綔娴?agent + +- 鍗″彿锛歚P2-04` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠鑳藉姏涓昏鍋滅暀鍦ㄦ湰鍦?CLI锛屾棤娉曡繘鍏ュ洟闃熷崗浣滈棴鐜?- 鐮斿彂鍔熻兘锛? - issue -> plan -> code -> test -> review -> PR 宸ヤ綔娴? - CI 鐜鐨?headless execution 妯″紡 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歸orkflow state machine + - `T3`锛歠ake repo / fake PR 涓婚摼 + - `T4`锛氭潈闄愪笉瓒?/ repo 鐘舵€佷笉骞插噣 / 缃戠粶澶辫触 +- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02` + +## P2-05 缁勭粐绾х瓥鐣ヤ笌瀹¤闈㈡澘 + +- 鍗″彿锛歚P2-05` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 鍥㈤槦鍦烘櫙涓嬬己涔忔不鐞嗐€佸鎵广€佸璁°€佸畨鍏ㄨ竟鐣?- 鐮斿彂鍔熻兘锛? - policy model + - approval queue + - secret redaction + - audit query / reporting +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歱olicy matcher / redaction rules + - `T2`锛歛pproval state transitions + - `T3`锛氭墽琛屽璁¢摼闂幆 + - `T4`锛氱瓥鐣ュ啿绐?/ 鏈鎵规墽琛岄樆鏂?- 鍓嶇疆渚濊禆锛歚P1-05` + +## P2-06 Linux 缁熶竴楠屽案闂ㄧ + +- 鍗″彿锛歚P2-06` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠姣忓紶鍗″悇鏈?gate 鑴氭湰锛屼絾缂哄皯涓€娆℃€х粺涓€鎵ц娓呭崟锛屼笉鍒╀簬 Linux 闃舵鍏ㄩ噺楠屽案 +- 鐮斿彂鍔熻兘锛? - 鑱氬悎 `P0-01` 鍒?`P2-05` 鍏ㄩ摼璺叧閿祴璇曡剼鏈竻鍗? - 鎻愪緵 Linux 缁熶竴鎵ц鍏ュ彛鑴氭湰 + - 寤虹珛 unified gate manifest 鐨?contract test锛岄伩鍏嶆竻鍗曟紓绉?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮anifest 瀛樺湪鎬т笌鏂囦欢鏄犲皠鏍¢獙 + - `T2`锛氬懡浠ょ敓鎴愰『搴忎笌鏍煎紡濂戠害 + - `T3`锛歀inux 闃舵鎸夋竻鍗曢€愭潯鎵ц锛堟湰鍦颁笉鎵ц锛? - `T4`锛氭柊澧?鍒犲噺娴嬭瘯鏂囦欢鏃跺绾﹀洖褰掓彁绀?- 鍓嶇疆渚濊禆锛歚P2-05` + +## P2-07 Agent 杩愯鏃朵竴鑷存€э紙sync/background parity锛? +- 鍗″彿锛歚P2-07` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `AgentTool` 鐨勫悓姝ヤ笌鍚庡彴璺緞鍦?runtime context 缁ф壙涓婂瓨鍦ㄦ紓绉婚闄? - 鍚庡彴 agent 浠诲姟缂哄皯 session/conversation 绛夊璁″厓鏁版嵁锛屼笉鍒╀簬鍥炴斁涓庢不鐞?- 鐮斿彂鍔熻兘锛? - 瀵归綈 sync/background 瀛?agent 鍦?`working_directory`銆乣tool_registry`銆乣permission_mode`銆乣provider/model`銆乣memory_scope` 鐨勮涓? - 灏?`session_id`銆乣conversation_id`銆乺untime context 瀛楁鍐欏叆鍚庡彴浠诲姟 metadata + - 璁╁悗鍙拌矾寰勪紭鍏堝鐢ㄤ笂灞?runtime `TaskManager`锛岄伩鍏嶅叏灞€鍗曚緥婕傜Щ +- 瀹炴柦鑼冨洿锛? - `claude_code/tools/base.py` + - `claude_code/engine/query.py` + - `claude_code/tools/agent/__init__.py` + - `claude_code/tasks/manager.py` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歴ync/background context 缁ф壙涓€鑷存€? - `T3`锛氬悗鍙颁换鍔?metadata 瀹¤瀛楁瀹屾暣鎬? - `T4`锛歱rovider/model 缁ф壙鍥炲綊锛堟湰鍦?provider 涓?Claude model alias 宸紓锛?- 鍓嶇疆渚濊禆锛歚P2-01`銆乣P2-06` + +## P2-08 鑷畾涔?Agent 鐩綍鍔犺浇闂幆锛?claude/agents锛? +- 鍗″彿锛歚P2-08` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - Python 鐗堟湰 `AgentTool` 鍙敮鎸佸唴缃?`BUILTIN_AGENTS`锛屾湭褰㈡垚 `.claude/agents/*.md` 鐨勫姞杞介棴鐜? - `subagent_type` 鎵句笉鍒版椂鍙娴嬫€т笉瓒筹紝鏃犳硶鍚屾椂鐪嬪埌 custom + builtin 鍙敤绫诲瀷 +- 鐮斿彂鍔熻兘锛? - 鏂板 `.claude/agents/*.md` loader锛屾敮鎸?YAML frontmatter + prompt body 瑙f瀽 + - 鏀寔浠庡綋鍓嶇洰褰曞悜涓婃悳绱㈠埌 git 鏍圭殑澶氱骇 `.claude/agents`锛屽苟鎸夆€滆繎璺緞瑕嗙洊杩滆矾寰勨€濆悎骞? - 鍦?`AgentTool` 涓帴鍏?custom agent lazy-load锛屼紭鍏堢骇 `custom > builtin` + - `Unknown agent type` 閿欒鎻愮ず杈撳嚭鑱斿悎鍙敤 agent 鍒楄〃 +- 瀹炴柦鑼冨洿锛? - `claude_code/agents/loader.py` + - `claude_code/tools/agent/__init__.py` + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歠rontmatter 瑙f瀽涓庡瓧娈靛綊涓€鍖栵紙tools/disallowedTools/memory/maxTurns锛? - `T2`锛氬绾х洰褰曡鐩栫瓥鐣ワ紙root -> nested锛? - `T3`锛欰gentTool custom 瑕嗙洊 builtin 瑙f瀽閾? - `T4`锛歩nvalid YAML / 缂哄瓧娈靛閿?+ unknown type 鑱斿悎鍙敤娓呭崟 +- 鍓嶇疆渚濊禆锛歚P2-07` + +## P2-09 Linux 缁熶竴楠屽案鎵ц闂ㄧ +- 鍗″彿锛歚P2-09` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-06` 褰撳墠鍙緭鍑哄懡浠ゆ竻鍗曪紝涓嶇洿鎺ユ彁渚?Linux 闃舵鐨勪竴閿墽琛屻€佹姤鍛婃矇娣€涓庡け璐ョ瓥鐣ユ帶鍒躲€? - 缁熶竴楠屽案閾剧己灏戞爣鍑嗗寲浜х墿锛坖unit xml + summary json锛夛紝涓嶅埄浜?CI 姹囨€讳笌鍥炴斁銆?- 鐮斿彂鍔熻兘锛? - 鏂板 Linux 鎵ц闂ㄧ鑴氭湰锛屽鐢?`run_linux_unified_gate.py` manifest銆? - 鎻愪緵 `--dry-run` / `--print-commands` / `--continue-on-failure` / `--report-dir` 鑳藉姏銆? - 椤哄簭鎵ц姣忔潯 pytest 鍛戒护骞朵骇鍑烘瘡鏉$敤渚嬬殑 `--junitxml`锛屾渶缁堝啓鍏ョ粺涓€ `summary.json`銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` + - `scripts/run_linux_unified_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭墽琛岄棬绂佸懡浠ょ敓鎴愪弗鏍艰窡闅?unified manifest 椤哄簭銆? - `T2`锛氶澶?pytest 鍙傛暟閫忎紶濂戠害涓?junit 璺緞濂戠害銆? - `T3`锛歀inux 闃舵鎵ц鍏ュ彛鍙?dry-run锛屼笉鍦ㄦ湰鍦拌繍琛屾祴璇曘€? - `T4`锛歮anifest 鍙樻洿鍚庢墽琛岄棬绂佸绾︽祴璇曞彲鍙婃椂鎷︽埅婕傜Щ銆?- 鍓嶇疆渚濊禆锛歚P2-06`銆乣P2-08` + +## P2-10 JetBrains IDE 闆嗘垚锛圛ntelliJ / PyCharm锛?- 鍗″彿锛歚P2-10` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-03` 浠呰鐩?`VS Code first` 閫傞厤锛孞etBrains 鐢熸€佺己灏戞爣鍑嗗寲蹇収閫傞厤灞傘€? - 鐜版湁 IDE payload 铏藉彲澶嶇敤锛屼絾缂哄皯 JetBrains 渚у彲鐩存帴娑堣垂鐨?inspection 缁撴瀯鏄犲皠銆?- 鐮斿彂鍔熻兘锛? - 鍦?`ide_adapter` 澧炲姞 `JetBrainsClientAdapter`銆? - 灏?daemon `workspace.findings` 鏄犲皠涓?JetBrains inspection 缁撴瀯锛坰everity -> highlight锛夈€? - 淇濇寔 daemon API 涓嶅彉锛岄€傞厤灞傚畬鎴愨€滃悓 payload锛屽 IDE 璇箟鈥濇敹鏁涖€?- 瀹炴柦鑼冨洿锛? - `claude_code/server/ide_adapter.py` + - `claude_code/server/__init__.py` + - `tests/test_p2_jetbrains_ide_integration_runtime.py` + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欽etBrains adapter 鍗忚褰掍竴鍖栵紙project_root/vcs/sessions/tasks锛夈€? - `T2`锛歠indings -> inspections 瀛楁鏄犲皠涓?severity/highlight 濂戠害銆? - `T3`锛氫繚鎸?daemon `ide/workspace` payload 鍏煎锛屼笉鏂板璺敱銆? - `T4`锛氶潪娉?line/severity 杈撳叆瀹归敊銆?- 鍓嶇疆渚濊禆锛歚P2-03`銆乣P2-09` + +## P2-11 Linux 鍒嗙墖骞惰楠屽案闂ㄧ +- 鍗″彿锛歚P2-11` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-09` 浠呮敮鎸佸崟杩涚▼椤哄簭鎵ц锛孡inux 闃舵鏃犳硶鐩存帴鎸?CI job 鍒嗙墖骞惰锛屾暣浣撳洖褰掕€楁椂楂樸€? - 缂哄皯鍒嗙墖绾?summary 鍏冩暟鎹紝涓嶅埄浜庡 job 鍥炴敹闃舵鍋氱粺涓€鑱氬悎銆?- 鐮斿彂鍔熻兘锛? - 鍦?`run_p2_linux_unified_execution_gate.py` 澧炲姞 `--shard-total` / `--shard-index`銆? - 鎸?unified manifest 椤哄簭鍋氱‘瀹氭€у垎鐗囬€夋嫨锛堝悓涓€杈撳叆濮嬬粓绋冲畾鍒囩墖锛夈€? - summary 澧炲姞 `shard_total` / `shard_index` / `manifest_total_tests` 瀛楁銆? - 淇濇寔 `--dry-run` / `--print-commands` 璺緞鍙褰撳墠鍒嗙墖瑕嗙洊鑼冨洿銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囬€夋嫨涓?manifest 椤哄簭濂戠害锛坉eterministic slicing锛夈€? - `T2`锛氶潪娉曞垎鐗囧弬鏁版嫆缁濆绾︼紙`shard_total < 1` / `shard_index` 瓒婄晫锛夈€? - `T3`锛歴ummary 鍒嗙墖鍏冩暟鎹绾︺€? - `T4`锛氶澶?pytest 鍙傛暟閫忎紶銆乯unit 璺緞濂戠害缁х画淇濇寔銆?- 鍓嶇疆渚濊禆锛歚P2-09` + +## P2-12 Linux 鍒嗙墖楠屽案姹囨€婚棬绂?- 鍗″彿锛歚P2-12` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-11` 宸叉敮鎸佸垎鐗囧苟琛屾墽琛岋紝浣嗙己灏?Linux 鍥炴敹闃舵鐨勭粺涓€姹囨€诲叆鍙o紝鏃犳硶缁欏嚭鍏ㄥ眬閫氳繃/澶辫触鍒ゅ畾銆? - 澶氫釜 shard 鐨?`summary.json` 缂哄皯涓€鑷存€ф牎楠岋紙缂虹墖/閲嶅鐗?manifest 婕傜Щ锛夛紝瀹规槗璁?CI 璇垽銆?- 鐮斿彂鍔熻兘锛? - 鏂板鍒嗙墖姹囨€婚棬绂佽剼鏈紝鏀寔 `--summary` / `--summary-glob` / `--artifacts-dir` 鑷姩鍙戠幇 summary 鏂囦欢銆? - 鍚堝苟 shard 缁撴灉骞惰緭鍑?`merged_summary.json`锛屾矇娣€ `overall_status`銆乣structural_issues`銆乣shards_missing` 涓庡叏灞€ totals銆? - 瀵瑰叧閿粨鏋勫仛濂戠害鏍¢獙锛氬瓧娈靛畬鏁存€с€乣shard_index` 鑼冨洿銆乣passed+failed<=total_tests`銆乵anifest/鍒嗙墖鎬绘暟涓€鑷存€с€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_aggregation_gate.py` + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ummary 鍙戠幇绛栫暐鍘婚噸濂戠害锛坉irect + glob + artifacts-dir锛夈€? - `T2`锛歴ummary 鍩虹瀛楁鏍¢獙涓庣被鍨嬬害鏉熷绾︺€? - `T3`锛氬叏鍒嗙墖瑕嗙洊鏃剁殑鑱氬悎 totals 涓?passed 鐘舵€佸绾︺€? - `T4`锛氱己鐗?鎬绘暟婕傜Щ鏃剁殑 structural issue 涓?failed 鐘舵€佸绾︺€?- 鍓嶇疆渚濊禆锛歚P2-11` + +## P2-13 Linux 鏈€缁堟姤鍛婂彂甯冮棬绂?- 鍗″彿锛歚P2-13` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-12` 浠呰緭鍑?`merged_summary.json`锛岀己灏?Linux 鍥炴敹闃舵鍙洿鎺ュ綊妗d笌灞曠ず鐨勬渶缁堟姤鍛婁骇鐗┿€? - 瀵?`overall_status` 缂哄皯鈥滃啀璁$畻鏍¢獙鈥濅笌鍙戝竷濂戠害锛孋I 渚ч毦浠ョǔ瀹氭秷璐规渶缁堢粨璁恒€?- 鐮斿彂鍔熻兘锛? - 鏂板鏈€缁堟姤鍛婂彂甯冮棬绂佽剼鏈紝璇诲彇 `merged_summary.json` 骞跺仛瀛楁濂戠害鏍¢獙銆? - 杈撳嚭 `final_report.json` 涓?`final_report.md`锛堢姸鎬併€佸垎鐗囥€乼otals銆乻tructural issues銆乶otes锛夈€? - 瀵?`overall_status` 鎵ц computed-vs-reported 涓€鑷存€у鏍革紝涓嶄竴鑷存椂寮哄埗澶辫触銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_report_publish_gate.py` + - `tests/test_p2_linux_report_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮erged summary 蹇呭~瀛楁涓庣被鍨嬪绾︽牎楠屻€? - `T2`锛歨appy path 涓嬫渶缁堢姸鎬佷笌 totals 鍙戝竷濂戠害銆? - `T3`锛歳eported status 涓?computed status 涓嶄竴鑷存椂澶辫触濂戠害銆? - `T4`锛歁arkdown 鎶ュ憡缁撴瀯濂戠害锛圱otals/Shards/Structural Issues/Notes锛夈€?- 鍓嶇疆渚濊禆锛歚P2-12` + +## P2-14 Linux 缁熶竴楠屽案鍏ㄩ摼缂栨帓闂ㄧ +- 鍗″彿锛歚P2-14` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-09` / `P2-12` / `P2-13` 宸叉媶鍒嗕负澶氶樁娈佃剼鏈紝浣嗙己灏戜竴鏉?Linux 涓€閿覆鑱旀墽琛屽叆鍙c€? - 澶氶樁娈垫墜宸ユ嫾瑁呭懡浠ゆ槗婕傜Щ锛屽け璐ョ瓥鐣ヤ笌鍙傛暟閫忎紶鍦?CI 渚ч毦缁熶竴銆?- 鐮斿彂鍔熻兘锛? - 鏂板 pipeline gate锛屾寜 `execution -> aggregation -> publish` 缂栨帓鎵ц銆? - 鏀寔 `--skip-execution` / `--skip-aggregation` / `--skip-publish`锛岃鐩栧崟鏈轰笌澶?job 鍥炴敹鍦烘櫙銆? - 鏀寔 `--print-commands` / `--dry-run` 浜у嚭鍙璁″懡浠ゆ竻鍗曪紝鏀寔 `--fail-fast` 澶辫触鎺у埗銆? - 鏀寔 execution 闃舵 pytest 棰濆鍙傛暟閫忎紶锛坄--` 杞彂鍒?`P2-09`锛夈€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_pipeline_gate.py` + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁や笁闃舵鍛戒护鐢熸垚濂戠害锛坋xecution summary 鑷姩娉ㄥ叆 aggregation锛夈€? - `T2`锛歚skip-execution` 涓嬪閮?summary/glob/artifacts 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氬叏璺宠繃妯″紡杩斿洖绌?pipeline 濂戠害銆? - `T4`锛歱ytest 棰濆鍙傛暟閫忎紶濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-13` + +## P2-15 Linux 鍒嗙墖鎵ц璁″垝鐢熸垚闂ㄧ +- 鍗″彿锛歚P2-15` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-11`/`P2-14` 宸叉敮鎸佸垎鐗囨墽琛屼笌鍏ㄩ摼缂栨帓锛屼絾 Linux CI fan-out job 浠嶉渶鎵嬪伐鎷兼帴 `shard_index` 鍛戒护锛屽鏄撴紓绉汇€? - 缂哄皯鈥滄墽琛屽墠鈥濊鍒掍骇鐗╋紝CI 鍥炴敹闃舵闅句互瀵圭収鏍¢獙鍒嗙墖瑕嗙洊鏄惁瀹屾暣銆?- 鐮斿彂鍔熻兘锛? - 鏂板 shard plan gate锛屽熀浜?unified manifest 鐢熸垚纭畾鎬?`shard_plan.json`銆? - 姣忎釜鍒嗙墖杈撳嚭锛歚test_files`銆乣report_dir`銆乣summary_path`銆乣execution command`銆乣command_parts`銆? - 鏀寔 `--print-commands`/`--dry-run` 鐢ㄤ簬 CI 缂栨帓鎺㈡祴锛涙敮鎸?pytest 棰濆鍙傛暟閫忎紶锛坄--`锛夈€? - 瀵?`shard_total` 涓?manifest 鏂囦欢瀛樺湪鎬у仛鍓嶇疆濂戠害鏍¢獙锛岄潪娉曡緭鍏ョ洿鎺ュけ璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_plan_gate.py` + - `tests/test_p2_linux_shard_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囧垏鐗囩‘瀹氭€у绾︼紙涓?`P2-11` slicing 涓€鑷达級銆? - `T2`锛氬垎鐗囨墽琛屽懡浠や笌 pytest 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氶潪娉?`shard_total` 鎷掔粷濂戠害銆? - `T4`锛氳緭鍑?`summary_path/report_dir` 瀛楁濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-14` + +## P2-16 Linux CI Matrix 瀵煎嚭闂ㄧ +- 鍗″彿锛歚P2-16` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-15` 宸蹭骇鍑?`shard_plan.json`锛屼絾 Linux CI 浠嶉渶鎵嬪伐鎶婂垎鐗囪鍒掕浆涓?matrix 鍙橀噺锛屾槗浜х敓缂栨帓婕傜Щ銆? - fan-out 涓?fan-in 涔嬮棿缂哄皯缁熶竴鐨?`summary_paths` 鍥炴敹娓呭崟锛屽鑷村洖鏀堕樁娈靛弬鏁版嫾瑁呬笉绋冲畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 CI matrix gate锛屼粠 `shard_plan.json` 鐢熸垚鍙洿鎺ユ秷璐圭殑 `ci_matrix.json`銆? - 杈撳嚭鏍囧噯 `matrix.include` 鏉$洰锛坄shard_index/shard_total/command/command_parts/report_dir/summary_path`锛夈€? - 杈撳嚭 `summary_paths` 鍥炴敹娓呭崟锛屼緵 `P2-12` 鑱氬悎闃舵鐩存帴浣跨敤銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions output 鏂囦欢锛涙敮鎸?`--skip-empty-shards` 杩囨护绌哄垎鐗囥€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_matrix_gate.py` + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮atrix include 椤哄簭涓庡瓧娈靛绾︺€? - `T2`锛歚skip-empty-shards` 杩囨护濂戠害銆? - `T3`锛欸itHub output 瀛楁鐢熸垚濂戠害锛坢atrix/summary_paths/selected_shards锛夈€? - `T4`锛歚manifest_total_tests` 涓庡垎鐗?totals 涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-15` + +## P2-17 Linux CI Workflow 璁″垝闂ㄧ +- 鍗″彿锛歚P2-17` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-16` 宸蹭骇鍑?`ci_matrix.json`锛屼絾 fan-out/fan-in CI job 浠嶉渶浜哄伐鎷兼帴鍥炴敹鍛戒护锛屽彂甯冮摼瀛樺湪鎵嬪伐婕傜Щ椋庨櫓銆? - 缂哄皯缁熶竴 workflow 璁″垝浜х墿锛孋I 缂栨帓渚ф棤娉曠洿鎺ユ秷璐光€滃垎鐗囨墽琛?+ 姹囨€?+ 鍙戝竷鈥濆叏閾惧懡浠ゃ€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow plan gate锛屼粠 `ci_matrix.json` 鐢熸垚 `ci_workflow_plan.json`銆? - 杈撳嚭 `fan_out_jobs` 涓?`fan_out_matrix.include`锛堝惈 shard job command/command_parts/report/summary 淇℃伅锛夈€? - 杈撳嚭 `fan_in` 姹囨€讳笌鍙戝竷鍛戒护锛坅ggregation/publish command + command_parts锛夛紝鐩存帴瀵规帴 `P2-12`/`P2-13`銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions outputs锛坄fan_out_matrix`銆乣fan_in_summary_paths`銆乣aggregation_command`銆乣publish_command` 绛夛級銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_plan_gate.py` + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow payload 涓?fan-out/fan-in 瀛楁涓庡懡浠ゅ绾︺€? - `T2`锛欸itHub output 瀛楁鐢熸垚濂戠害銆? - `T3`锛歚summary_paths` 涓?`matrix.include` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-16` + +## P2-18 Linux CI Workflow YAML 娓叉煋闂ㄧ +- 鍗″彿锛歚P2-18` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-17` 宸蹭骇鍑?`ci_workflow_plan.json`锛屼絾 CI 骞冲彴浠嶉渶浜哄伐缂栧啓/缁存姢 workflow YAML锛宖an-out/fan-in 鍛戒护鏄撴紓绉汇€? - 缂哄皯鈥滆鍒掍骇鐗?-> 鍙墽琛?workflow 鏂囦欢鈥濈殑鏈€鍚庝竴璺筹紝瀵艰嚧闂ㄧ閾句粛瀛樺湪鎵嬪伐浠嬪叆鐐广€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow YAML gate锛屼粠 `ci_workflow_plan.json` 娓叉煋 `.github/workflows/linux_unified_gate.yml`銆? - 娓叉煋 fan-out matrix job锛堝垎鐗囨墽琛?+ summary artifact 涓婁紶锛変笌 fan-in job锛坅rtifact 鍥炴敹 + 鑱氬悎 + 鍙戝竷 + 鏈€缁堟姤鍛婁笂浼狅級銆? - 杈撳嚭娓叉煋鍏冩暟鎹?`ci_workflow_render.json`锛岃褰?source plan銆乫an-in 鍛戒护銆佹姤鍛婅矾寰勪笌鍒嗙墖缁熻锛屼緵瀹¤杩借釜銆? - 鏀寔 `--dry-run`/`--print-yaml` 蹇€熼獙鐪嬫覆鏌撶粨鏋滐紝涓嶅湪鏈湴瑙﹀彂娴嬭瘯鎵ц銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow YAML fan-out/fan-in 瀛楁涓庡懡浠ゆ覆鏌撳绾︺€? - `T2`锛歳ender metadata 瀛楁濂戠害锛坰ource/workflow path/summary stats锛夈€? - `T3`锛歚fan_in.summary_paths` 涓?`fan_out_matrix.include[*].summary_path` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-17` + +## P2-19 Linux CI Workflow 婕傜Щ鏍¢獙闂ㄧ +- 鍗″彿锛歚P2-19` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-18` 宸茶兘娓叉煋 workflow YAML 涓?metadata锛屼絾浠撳簱涓殑 `.github/workflows/linux_unified_gate.yml` 涓?`ci_workflow_render.json` 鍙兘琚墜宸ユ敼鍔ㄥ悗涓?`ci_workflow_plan.json` 鑴辫妭銆? - 缂哄皯鈥滆鍒掍骇鐗?-> 娓叉煋浜х墿涓€鑷存€ф牎楠屸€濋棬绂侊紝CI 鍙兘鍦ㄦ紓绉荤姸鎬佷笅杩愯锛屽鑷?fan-out/fan-in 鍛戒护涓庢姤鍛婅矾寰勫け鐪熴€?- 鐮斿彂鍔熻兘锛? - 鏂板 workflow sync gate锛屽熀浜?`ci_workflow_plan.json` 澶嶇畻鏈熸湜 workflow YAML 涓?metadata锛屽苟涓庡綋鍓嶄骇鐗╁仛涓€鑷存€ф瘮瀵广€? - 杈撳嚭 drift 鎽樿锛坵orkflow_drift/metadata_drift/drift_count锛夛紝鏀寔 `--print-diff` 鎵撳嵃 unified diff 渚夸簬瀹¤銆? - 鏀寔 `--write` 鍦ㄦ湰鍦版垨 CI 閲岃嚜鍔ㄥ洖鍐欐爣鍑嗕骇鐗╋紝鏀舵暃浜哄伐婕傜Щ銆? - 鏀寔 `--strict-generated-at` 寮€鍚?metadata `generated_at` 涓ユ牸姣斿锛涢粯璁ゅ拷鐣ヨ瀛楁浠呮牎楠岀粨鏋勪笌鏍稿績鍐呭銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_sync_gate.py` + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬綋 workflow/metadata 缂哄け鏃跺繀椤昏Е鍙?drift 鍒ゅ畾銆? - `T2`锛氬綋 workflow 鍐呭琚敼鍐欐椂锛屽繀椤昏緭鍑?drift 涓庡彲璇?diff 濂戠害銆? - `T3`锛氶粯璁ゆā寮忓拷鐣?metadata `generated_at` 婕傜Щ锛岄伩鍏嶆椂闂存埑鍣0璇姤銆? - `T4`锛氬紑鍚?strict 妯″紡鏃讹紝`generated_at` 婕傜Щ蹇呴』瑙﹀彂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-18` + +## P2-20 Linux CI Workflow 鍛戒护鎶ゆ爮闂ㄧ +- 鍗″彿锛歚P2-20` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-19` 宸茶兘鏍¢獙 plan->yaml/metadata 婕傜Щ锛屼絾灏氭湭绾︽潫 `ci_workflow_plan.json` 鍐?`command` 涓?`command_parts` 鐨勪竴鑷存€э紝瀛樺湪鈥滃瓧绗︿覆琚敼銆乸arts 鏈敼鈥濆鑷?CI 瀹為檯鎵ц鍋忕Щ鐨勯闄┿€? - fan-out 涓?fan-in 鍛戒护涓殑鍏抽敭璺緞锛坄report_dir`銆乣summary_paths`銆乣merged_summary/final_report`锛夌己灏戝己濂戠害缁戝畾锛屽彲鑳藉湪鎵嬪伐缂栬緫鍚庡紩鍏ラ潤榛橀敊璇€?- 鐮斿彂鍔熻兘锛? - 鏂板 command guard gate锛屾牎楠?`command`/`command_parts` 鍙€嗕竴鑷存€э紙`shlex.split(command)` == `command_parts`锛変笌 canonical shell 瑙勮寖鍖栬緭鍑恒€? - 鏍¢獙鑴氭湰鐩爣涓庡叧閿?flag 濂戠害锛? - fan-out 蹇呴』鎸囧悜 `run_p2_linux_unified_execution_gate.py`锛屼笖鎼哄甫 `--report-dir/--shard-total/--shard-index`銆? - fan-in aggregation 蹇呴』鎸囧悜 `run_p2_linux_shard_aggregation_gate.py`锛屼笖 `--summary` 椤哄簭涓ユ牸鍖归厤 `fan_in.summary_paths`銆? - fan-in publish 蹇呴』鎸囧悜 `run_p2_linux_report_publish_gate.py`锛屼笖杈撳嚭璺緞涓?`fan_in` 瀛楁涓€鑷淬€? - 鏀寔 `--write` 鍥炲啓 canonical command 瀛楃涓诧紝鏀舵暃璁″垝鏂囦欢鍛戒护鏍煎紡婕傜Щ銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬悎娉?workflow plan 蹇呴』閫氳繃 guard锛宍failed_commands=0`銆? - `T2`锛歚command`/`command_parts` 涓嶄竴鑷存椂蹇呴』瑙﹀彂鎷掔粷锛屽苟缁欏嚭瑙勮寖鍖栧懡浠ゃ€? - `T3`锛歛ggregation 鐨?`--summary` 椤哄簭涓?`fan_in.summary_paths` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛歱ublish 杈撳嚭璺緞涓?`fan_in.final_report_*` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆?- 鍓嶇疆渚濊禆锛歚P2-19` + +## P2-21 Linux CI Workflow 娌荤悊鏀舵暃闂ㄧ +- 鍗″彿锛歚P2-21` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-19` 涓?`P2-20` 宸插垎鍒鐩栤€滄覆鏌撴紓绉烩€濆拰鈥滃懡浠ゅ绾︹€濓紝浣嗙己灏戠粺涓€娌荤悊缁撹锛孋I 渚т粛闇€鎵嬪伐鎷兼帴澶氫唤鎶ュ憡鍒ゆ柇鏄惁鍙墽琛?workflow銆? - 缂哄皯 metadata 琛€缂樹竴鑷存€ч棬绂侊紙`source_ci_workflow_plan` 涓?`workflow_output_path`锛夛紝瀛樺湪鈥滀骇鐗╃湅浼奸€氳繃銆佹潵婧愬凡閿欎綅鈥濈殑闅愭€ч闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance gate锛屼竴娆℃墽琛屾眹鎬?`workflow_sync + command_guard + metadata lineage` 涓夌被妫€鏌ワ紝浜у嚭鍗曚竴 `overall_status`銆? - 杈撳嚭 `failed_checks` 鏄庣‘澶辫触绫诲埆锛歚workflow_sync_drift` / `command_guard_failed` / `lineage_mismatch`銆? - 灏?`command_guard` 鎽樿鍖栦负 `failed_commands/issue_count/normalization_required`锛屼究浜?CI 鑱氬悎灞曠ず銆? - 瀵?metadata 琛€缂樺仛寮烘牎楠岋細`source_ci_workflow_plan` 涓?`workflow_output_path` 蹇呴』涓庡綋鍓嶉棬绂佽緭鍏ヨ矾寰勪竴鑷淬€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow/command/lineage 鍏ㄩ儴涓€鑷存椂蹇呴』閫氳繃锛宍overall_status=passed`銆? - `T2`锛歸orkflow YAML 婕傜Щ鏃跺繀椤昏惤鍏?`workflow_sync_drift` 澶辫触绫汇€? - `T3`锛歝ommand 濂戠害寮傚父鏃跺繀椤昏惤鍏?`command_guard_failed` 澶辫触绫汇€? - `T4`锛歮etadata 琛€缂樺瓧娈典笌杈撳叆璺緞涓嶄竴鑷存椂蹇呴』钀藉叆 `lineage_mismatch` 澶辫触绫汇€?- 鍓嶇疆渚濊禆锛歚P2-20` + +## P2-22 Linux CI Workflow 娌荤悊鍙戝竷闂ㄧ +- 鍗″彿锛歚P2-22` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-21` 宸蹭骇鍑虹粺涓€娌荤悊缁撹锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽 `ci_workflow_governance.json` 鎵嶈兘鍐冲畾鈥滄槸鍚︽墽琛?workflow鈥濄€? - 缂哄皯鍥哄畾鐨勫彂甯冧骇鐗╀笌 GitHub output 濂戠害锛屽鑷?fan-in 闃舵闅句互绋冲畾娑堣垂娌荤悊缁撴灉涓庡け璐ョ被鍒€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance publish gate锛岃鍙?`P2-21` 鎶ュ憡鍚庣敓鎴愬彲娑堣垂鍙戝竷浜х墿锛? - `ci_workflow_governance_publish.json` + - `ci_workflow_governance_publish.md` + - 杈撳嚭绋冲畾鍐崇瓥瀛楁锛歚overall_status`銆乣should_execute_workflow`銆乣failed_checks`銆乣failed_check_count`銆? - 鏀寔 `--github-output` 瀵煎嚭 CI 鍙橀噺锛歚governance_status`銆乣governance_should_execute_workflow`銆乣governance_failed_checks` 绛夈€? - 澧炲姞鍙戝竷渚т竴鑷存€ф牎楠岋細鑻?`failed_checks` 鎴?`overall_status` 涓庨噸绠楃粨鏋滀笉涓€鑷达紝蹇呴』钀藉叆 `structural_issues` 骞跺垽瀹氬け璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗘姤鍛婂叏閲忎竴鑷存椂搴旇緭鍑?`overall_status=passed` 涓?`should_execute_workflow=true`銆? - `T2`锛氬綋娌荤悊鎶ュ憡鍖呭惈澶辫触妫€鏌ユ椂锛屽簲杈撳嚭 `overall_status=failed` 涓?`should_execute_workflow=false`銆? - `T3`锛氬綋娌荤悊鎶ュ憡鐨?`failed_checks` 涓庨噸绠楃粨鏋滀笉涓€鑷存椂锛屽繀椤昏Е鍙?`structural_issues` 骞跺け璐ャ€? - `T4`锛欸itHub output 瀵煎嚭瀛楁蹇呴』鍖呭惈鐘舵€併€佸け璐ュ垪琛ㄤ笌鍙戝竷浜х墿璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-21` + +## P2-23 Linux CI Workflow 鎵ц鍐崇瓥闂ㄧ +- 鍗″彿锛歚P2-23` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-22` 铏藉凡浜у嚭 `should_execute_workflow`锛屼絾 CI 涓嬫父浠嶇己鈥滅粺涓€鎵ц闂搁棬鈥濆皢娌荤悊缁撴灉杞垚鏄庣‘鐨?execute/blocked 缁撴灉涓庨€€鍑虹爜璇箟銆? - 缂哄皯瀵规墽琛屽墠鍏抽敭浜х墿锛坵orkflow plan / rendered workflow锛夌殑瀛樺湪鎬ф牎楠岋紝鍙兘鍑虹幇鈥滄不鐞嗛€氳繃浣嗘墽琛屽叆鍙g己澶扁€濈殑鏂摼銆?- 鐮斿彂鍔熻兘锛? - 鏂板 execution decision gate锛岃鍙?`P2-22` 鍙戝竷缁撴灉锛岀粺涓€杈撳嚭锛? - `ci_workflow_execution_decision.json` + - `ci_workflow_execution_decision.md` + - 鍥哄寲鍐崇瓥瀛楁锛歚decision`銆乣decision_status`銆乣should_execute_workflow`銆乣exit_code`銆乣reason_codes`銆乣missing_artifacts`銆? - 鏀寔闃绘柇绛栫暐寮€鍏筹細`--on-block fail|skip`锛岃 CI 鍙湪闃绘柇鏃堕€夋嫨 fail-fast 鎴?soft-skip銆? - 鏀寔 `--github-output` 杈撳嚭涓嬫父鍙橀噺锛屽噺灏?job 闂存墜宸ヨВ鏋愩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_decision_gate.py` + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗛€氳繃涓旀墽琛屼骇鐗╅綈澶囨椂蹇呴』杈撳嚭 `decision=execute`銆乣exit_code=0`銆? - `T2`锛氭不鐞嗗け璐ユ椂蹇呴』杈撳嚭 `decision=blocked` 涓旀惡甯﹀け璐ュ師鍥犵爜銆? - `T3`锛氶樆鏂瓥鐣ヤ负 `skip` 鏃讹紝闃绘柇鍐崇瓥搴旇繑鍥?`exit_code=0`銆? - `T4`锛欸itHub output 濂戠害闇€鍖呭惈 decision/status/should_execute/exit_code 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-22` + +## P2-24 Linux CI Workflow 璋冨害灏辩华闂ㄧ +- 鍗″彿锛歚P2-24` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-23` 宸蹭骇鍑?execute/blocked 鍐崇瓥锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐灏嗗喅绛栧瓧娈垫嫾瑁呬负瀹為檯璋冨害鍛戒护锛屽瓨鍦ㄥ懡浠や笉涓€鑷翠笌閿欒瑙﹀彂椋庨櫓銆? - 缂哄皯缁熶竴 dispatch 濂戠害鎶ュ憡锛屽鑷粹€滄槸鍚﹀彲鍙戣捣 `gh workflow run`鈥濈殑鍒ゆ柇鏃犳硶琚?job 闂寸ǔ瀹氬鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch readiness gate锛岃鍙?`P2-23` 鍐崇瓥浜х墿骞惰緭鍑猴細 + - `ci_workflow_dispatch.json` + - `ci_workflow_dispatch.md` + - 鍥哄寲璋冨害瀛楁锛歚dispatch_status`銆乣dispatch_mode`銆乣should_dispatch_workflow`銆乣dispatch_command`銆乣dispatch_command_parts`銆? - execute 鍐崇瓥鏃剁敓鎴愯鑼冨寲 `gh workflow run` 鍛戒护锛沚locked 鍐崇瓥鏃惰緭鍑虹┖鍛戒护骞朵繚鎸侀樆鏂師鍥犲彲杩芥函銆? - 鏀寔 `--github-output` 瀵煎嚭 dispatch 鐘舵€併€佸懡浠や笌鎶ュ憡璺緞锛屼緵涓嬫父 CI 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟xecute 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=ready` 涓庡悎娉?`gh workflow run` 鍛戒护銆? - `T2`锛歜locked 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=blocked` 涓?`dispatch_command` 涓虹┖銆? - `T3`锛氬喅绛栧绾﹀啿绐侊紙濡?decision=execute 浣?should_execute=false锛夋椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 dispatch 鐘舵€併€佽皟搴﹀懡浠や笌鎶ュ憡璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-23` + +## P2-25 Linux CI Workflow 璋冨害鎵ц闂ㄧ +- 鍗″彿锛歚P2-25` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-24` 浠呬骇鍑衡€滃彲璋冨害濂戠害鈥濓紝鏈粺涓€澶勭悊鐪熸 dispatch 鎵ц缁撴灉锛孋I 涓嬫父浠嶉渶閲嶅鎷兼帴鈥滄槸鍚︽墽琛?鎵ц澶辫触濡備綍鍥炴姤鈥濈殑閫昏緫銆? - 缂哄皯璋冨害鎵ц灞傜粺涓€鎶ュ憡锛屽鑷?execute/blocked/fail 涓夋€佹棤娉曠ǔ瀹氭矇娣€涓哄崟涓€瀹¤浜х墿銆?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch execution gate锛岃鍙?`P2-24` 璋冨害濂戠害骞惰緭鍑猴細 + - `ci_workflow_dispatch_execution.json` + - `ci_workflow_dispatch_execution.md` + - 鍥哄寲鎵ц瀛楁锛歚execution_status`銆乣execution_exit_code`銆乣dispatch_attempted`銆乣command_returncode`銆乣reason_codes`銆? - `dispatch_status=ready` 鏃舵墽琛?`dispatch_command_parts`锛堟敮鎸?`--dry-run` 璺宠繃鐪熷疄鎵ц锛夛紱`dispatch_status=blocked` 鏃朵繚鎸佹棤鎵ц浣嗕繚鐣欓樆鏂涔夈€? - 鏀寔 `--github-output` 瀵煎嚭鎵ц鐘舵€併€侀€€鍑虹爜涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歳eady + dry-run 蹇呴』浜у嚭 `execution_status=ready_dry_run` 涓?`execution_exit_code=0`銆? - `T2`锛歜locked 鍐崇瓥蹇呴』淇濇寔 `dispatch_attempted=false` 涓旈€€鍑虹爜閬靛惊涓婃父闃绘柇绛栫暐璇箟銆? - `T3`锛歞ispatch 濂戠害鍐茬獊锛堝 command 涓?command_parts 涓嶄竴鑷达級蹇呴』鎷掔粷銆? - `T4`锛氬懡浠ゆ墽琛屽け璐ユ椂蹇呴』钀藉湴 `execution_status=dispatch_failed` 骞舵惡甯﹀け璐ュ師鍥犵爜銆? - `T5`锛欸itHub output 濂戠害闇€鍖呭惈鎵ц鐘舵€併€佹槸鍚﹀皾璇曡皟搴︺€侀€€鍑虹爜涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-24` + +## P2-26 Linux CI Workflow 鍏ㄩ摼缂栨帓闂ㄧ +- 鍗″彿锛歚P2-26` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-17` 鑷?`P2-33` 鍚勯棬绂佸凡鍏峰锛屼絾 CI 渚т粛闇€浜哄伐鎷兼帴澶氭潯鍛戒护锛屽鏄撳嚭鐜伴樁娈甸仐婕忋€佸弬鏁版紓绉讳笌椤哄簭閿欒銆? - 缂哄皯鈥滃崟鍏ュ彛鈥濋棬绂佺粺涓€璋冨害 workflow plan/yaml/sync/governance/dispatch/terminal publish 鍏ㄩ摼锛屽鑷?Linux CI 缂栨帓閲嶅缁存姢鎴愭湰楂樸€?- 鐮斿彂鍔熻兘锛? - 鏂板 full pipeline gate锛屼竴娆$紪鎺?`P2-17 -> P2-33` 鍗佸叚闃舵锛? - plan -> yaml -> sync -> command_guard -> governance -> governance_publish -> decision -> dispatch -> dispatch_execution -> dispatch_trace -> dispatch_completion -> terminal_publish -> release_handoff -> release_trigger -> release_trace -> release_completion銆? - 鏀寔缁熶竴闃舵瑁佸壀 `--skip-*`锛屽厑璁稿湪 CI fan-in / rerun 鍦烘櫙鎸夐渶璺宠繃鍓嶅簭闃舵銆? - 鏀寔缁熶竴绛栫暐閫忎紶锛歚--strict-generated-at`銆乣--sync-write`銆乣--command-guard-write`銆乣--on-block`銆乣--workflow-ref`銆乣--dispatch-timeout-seconds`銆乣--dispatch-trace-poll-now`銆乣--completion-allow-in-progress`銆? - 鏀寔 `--print-commands/--dry-run` 棰勮涓?`--fail-fast` 鎵ц绛栫暐锛屾矇娣€鍗曚竴 Linux CI 缂栨帓鍏ュ彛銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁ら樁娈甸摼蹇呴』瀹屾暣瑕嗙洊 `P2-17 -> P2-33` 涓旈『搴忓浐瀹氥€? - `T2`锛氱瓥鐣ュ弬鏁帮紙strict/sync-write/command-guard-write/on-block/workflow-ref/timeout/trace-poll锛夊繀椤绘纭€忎紶鍒板搴斿瓙闂ㄧ銆? - `T3`锛氶儴鍒?`--skip-*` 鏃跺繀椤诲彧淇濈暀鍚庡崐閾鹃樁娈靛苟淇濇寔椤哄簭銆? - `T4`锛氬叏閮?`--skip-*` 鏃跺簲杩斿洖绌鸿鍒掞紙涓嶈Е鍙戦樁娈垫墽琛岋級銆? - `T5`锛氭敮鎸佷粎缁堝眬鍙戝竷闃舵锛坄terminal_publish`锛変笌 release completion 闃舵鐙珛鎵ц銆?- 鍓嶇疆渚濊禆锛歚P2-25` + +## P2-27 Linux CI Workflow 璋冨害杩借釜闂ㄧ +- 鍗″彿锛歚P2-27` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-25` 鍙鐩?dispatch 鍛戒护鎵ц鍥炴墽锛屼絾鏈敹鏁?workflow run 鐨勮拷韪绾︼紝CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽鏃ュ織鎻愬彇 `run_id`/`run_url`銆? - 缂哄皯缁熶竴 run poll 鍛戒护涓庤拷韪姸鎬侊紝瀵艰嚧鍚庣画 job 瀵光€滃凡璋冨害浣嗘湭瀹屾垚鈥濈殑鐘舵€佸垽鏂笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch trace gate锛岃鍙?`P2-25` 鎵ц鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_dispatch_trace.json` + - `ci_workflow_dispatch_trace.md` + - 浠?dispatch stdout/stderr 鎻愬彇 canonical GitHub Actions run URL 涓?`run_id`锛岀敓鎴愬彲澶嶇敤 poll 鍛戒护锛坄gh run view --json ...`锛夈€? - 鏀寔 `--poll-now` 绔嬪嵆鎵ц涓€娆?run 鐘舵€佹煡璇紝缁熶竴 `run_tracking_ready` / `run_in_progress` / `run_completed_success` / `run_completed_failure` / `run_poll_failed` 鍒ゅ畾銆? - 鏀寔 `--github-output` 瀵煎嚭杩借釜鐘舵€併€乺un 鏍囪瘑銆乸oll 缁撴灉涓庢姤鍛婅矾寰勶紝渚?CI 涓嬫父鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚dispatched` 涓斿惈 run URL 鏃跺繀椤绘彁鍙?`run_id/run_url` 骞剁敓鎴?poll 鍛戒护銆? - `T2`锛歚dispatched` 浣嗙己 run URL 鏃跺繀椤昏惤鍏?`run_tracking_missing` 涓旇繑鍥炲け璐ラ€€鍑虹爜銆? - `T3`锛歚blocked`/`ready_dry_run` 璺緞蹇呴』淇濇寔鏃?poll 灏濊瘯骞堕€忎紶涓婃父璇箟銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈杩借釜鐘舵€併€乺un_id/run_url銆佹姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-26` +## P2-28 Linux CI Workflow 璋冨害瀹屾垚鏀舵暃闂ㄧ +- 鍗″彿锛歚P2-28` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-27` 浠呮彁渚涘崟娆?trace 涓庡彲閫夊崟娆?poll锛屾湭鎻愪緵鈥滅瓑寰呰嚦缁堟€佲€濈殑缁熶竴鏀舵暃濂戠害锛汣I 涓嬫父浠嶉渶鑷寰幆 `gh run view` 骞跺鐞嗚秴鏃?杩涜涓?澶辫触鍒嗘敮銆? - 缂哄皯缁熶竴 completion verdict锛屽鑷?`run_in_progress`銆乣run_completed_failure`銆乣run_poll_failed`銆乣run_tracking_missing` 鍦ㄤ笉鍚?job 涓€€鍑鸿涔変笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch completion gate锛岃鍙?`P2-27` trace 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_dispatch_completion.json` + - `ci_workflow_dispatch_completion.md` + - 鏀寔杞绛夊緟鍙傛暟锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`锛屽皢 run 鐘舵€佺粺涓€鏀舵暃涓猴細 + - `run_completed_success` + - `run_completed_failure` + - `run_poll_failed` + - `run_await_timeout` + - 浠ュ強閫忎紶鍨?`blocked` / `ready_dry_run` / `run_tracking_missing` + - 鎻愪緵 `--allow-in-progress` 绛栫暐寮€鍏筹細瓒呮椂浠?in-progress 鏃跺彲閫夋槸鍚﹁繑鍥?exit 0锛屾弧瓒充笉鍚?CI 缂栨帓绛栫暐銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€乸oll 杩唬銆乺un_id/url銆佹姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_tracking_ready` 涓?poll 杩斿洖 completed+success 鏃讹紝蹇呴』鏀舵暃涓?`run_completed_success` 涓?`completion_exit_code=0`銆? - `T2`锛氭寔缁?in-progress 鍒?`max_polls` 鍚庯紝蹇呴』钀?`run_await_timeout`锛屽苟鎸?`--allow-in-progress` 鍐冲畾閫€鍑虹爜銆? - `T3`锛歵race 涓?`poll_command` 涓?`poll_command_parts` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乪xit code銆乸oll 娆℃暟銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-27` + +## P2-29 Linux CI Workflow 缁堝眬鍙戝竷闂ㄧ +- 鍗″彿锛歚P2-29` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-28` 宸茬粰鍑?completion verdict锛屼絾涓嬫父鍙戝竷娴佺▼浠嶉渶鑷灏?`completion_status` 鏄犲皠涓哄彲鍙戝竷璇箟锛坧romote/hold/fail锛夛紝瀵艰嚧鏀跺彛绛栫暐涓嶄竴鑷淬€? - 缂哄皯缁熶竴缁堝眬 publish 濂戠害锛岄毦浠ョǔ瀹氬鐢ㄥ湪 release gate 涓庡悗缁幆澧冩帹骞跨瓥鐣ヤ腑銆?- 鐮斿彂鍔熻兘锛? - 鏂板 terminal publish gate锛岃鍙?`P2-28` completion 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_terminal_publish.json` + - `ci_workflow_terminal_publish.md` + - 灏?completion 璇箟缁熶竴鏀舵暃涓?`publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_promote`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`run_completed_success` 蹇呴』 `completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_completed_success` + `completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `publish_status=passed` 涓?`should_promote=true`銆? - `T2`锛歚run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `publish_status=in_progress` 涓?`publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-28` + +## P2-30 Linux CI Workflow Release Handoff Gate +- 鍗″彿锛歚P2-30` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-29` 宸茬粰鍑?publish verdict锛屼絾 release job 浠嶉渶鑷瑙f瀽 `publish_status/should_promote` 缁勮瑙﹀彂鏉′欢锛屽鑷存帹骞胯涔夊垎鍙夈€? - 缂哄皯缁熶竴 handoff 濂戠害锛屼笉鍒╀簬鍚庣画閮ㄧ讲骞冲彴鎴栧鎵规祦澶嶇敤 Linux CI 缁堝眬缁撹銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release handoff gate锛岃鍙?`P2-29` terminal publish 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_handoff.json` + - `ci_workflow_release_handoff.md` + - 灏?publish 璇箟缁熶竴鏀舵暃涓?`handoff_status`锛坄ready_for_release`/`awaiting_completion`/`blocked`/`failed`/`contract_failed`锛変笌 `should_trigger_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`publish_status=passed` 蹇呴』 `publish_exit_code=0` 涓?`should_promote=true`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--target-environment`銆乣--release-channel` 閫忎紶鍙戝竷鐩爣璇箟锛屽苟鏀寔 `--github-output` 瀵煎嚭 handoff 鐘舵€併€乺elease action銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` + - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚publish_status=passed` + `should_promote=true` 鏃跺繀椤绘敹鏁涗负 `handoff_status=ready_for_release` 涓?`should_trigger_release=true`銆? - `T2`锛歚publish_status=in_progress` 鏃跺繀椤绘敹鏁涗负 `handoff_status=awaiting_completion` 涓?`release_exit_code=0`銆? - `T3`锛歵erminal publish 濂戠害鍐茬獊锛堝 passed 浣?should_promote=false锛夋椂蹇呴』鏀舵暃涓?`handoff_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 handoff 鐘舵€併€乺elease action銆乼arget env/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-29` + +## P2-31 Linux CI Workflow Release Trigger Gate +- 鍗″彿锛歚P2-31` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-30` 宸蹭骇鍑?`should_trigger_release` 璇箟锛屼絾浠嶇己灏戔€滅湡姝f墽琛?release dispatch鈥濈殑缁熶竴鎵ц闂ㄧ锛汣I 涓嬫父闇€鑷鎷艰鍛戒护骞跺鐞嗚秴鏃?澶辫触鍒嗘敮锛岃Е鍙戣涓轰笉涓€鑷淬€? - 缂哄皯缁熶竴 trigger 鎵ц鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 杩借釜銆佸洖婧笌瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trigger gate锛岃鍙?`P2-30` release handoff 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_trigger.json` + - `ci_workflow_release_trigger.md` + - 褰?`should_trigger_release=true` 鏃讹紝鑷姩鏋勫缓骞舵墽琛?release dispatch command锛堟敮鎸佽嚜瀹氫箟 `--release-command` 瑕嗙洊榛樿鍛戒护锛夈€? - 褰?`should_trigger_release=false` 鏃讹紝鎸?handoff 鐘舵€佹敹鏁涗负 `release_trigger_status`锛坄awaiting_completion`/`blocked`/`handoff_failed`锛夛紝骞朵繚鎸侀浂瑙﹀彂鍓綔鐢ㄣ€? - 鍥哄寲濂戠害鏍¢獙锛歨andoff 鐘舵€併€乤ction銆乪xit code 涓庤Е鍙戝竷灏斿€煎繀椤讳竴鑷达紱涓嶄竴鑷村嵆鎷掔粷鎵ц銆? - 鏀寔 `--github-output` 瀵煎嚭 trigger 鐘舵€併€佽Е鍙戝皾璇曘€乪xit code銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` + - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚ready_for_release + should_trigger_release=true` 鍦?dry-run 璺緞蹇呴』鏀舵暃涓?`release_trigger_status=ready_dry_run`銆? - `T2`锛歚awaiting_completion` 璺緞蹇呴』淇濇寔 `release_trigger_status=awaiting_completion` 涓斾笉瑙﹀彂鍛戒护鎵ц銆? - `T3`锛歨andoff 濂戠害鍐茬獊锛堝 `ready_for_release` 浣?`should_trigger_release=false`锛夊繀椤昏鎷掔粷鍔犺浇銆? - `T4`锛氳Е鍙戝懡浠ら潪闆惰繑鍥炲繀椤绘敹鏁涗负 `release_trigger_status=trigger_failed` 骞惰褰曞け璐?reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trigger 鐘舵€併€乤ttempted銆乪xit code銆乪nv/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-30` + +## P2-32 Linux CI Workflow Release Traceability Gate +- 鍗″彿锛歚P2-32` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-31` 宸插畬鎴?release trigger 鎵ц锛屼絾缂哄皯 release run 杩借釜濂戠害锛涗笅娓?job 闇€瑕侀噸澶嶈В鏋?stdout/stderr 鎵嶈兘鎷垮埌 `run_id/run_url`锛岃拷韪涔変笉涓€鑷淬€? - 缂哄皯缁熶竴 release trace 鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 瀹屾垚鎬佹敹鏁涖€佸璁¤拷婧笌璺?job 澶嶇敤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trace gate锛岃鍙?`P2-31` release trigger 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_trace.json` + - `ci_workflow_release_trace.md` + - 褰?`release_trigger_status=triggered` 鏃讹紝缁熶竴鎻愬彇 release run 寮曠敤骞舵敹鏁涗负锛? - `release_run_id` / `release_run_url` + - `release_tracking_status`锛坄release_run_tracking_ready`/`release_run_tracking_missing`锛? - 鍙€?poll 鍛戒护濂戠害锛坄release_poll_command` + `release_poll_command_parts`锛? - 鏀寔 `--poll-now` 绔嬪嵆鏌ヨ release run 鐘舵€侊紝鏀舵暃涓猴細 + - `release_run_in_progress` / `release_run_completed_success` / `release_run_completed_failure` / `release_run_poll_failed` + - 瀵归潪瑙﹀彂鐘舵€侊紙`ready_dry_run` / `awaiting_completion` / `blocked` / `handoff_failed` / `trigger_failed`锛夌粺涓€閫忎紶鎴愭棤鍓綔鐢?trace 璇箟銆? - 鏀寔 `--github-output` 瀵煎嚭 trace 鐘舵€併€乻hould_poll銆乺un_id/url銆乸oll 鐘舵€佷笌鎶ュ憡璺緞銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` + - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚triggered` 鎶ュ憡蹇呴』鎻愬彇鍑?run 寮曠敤锛屽苟鏀舵暃涓?`release_tracking_status=release_run_tracking_ready`銆? - `T2`锛歚triggered` 浣嗙己灏?run 寮曠敤鏃跺繀椤绘敹鏁涗负 `release_tracking_status=release_run_tracking_missing` 涓?`release_trace_exit_code=1`銆? - `T3`锛歚trigger_failed` 璺緞蹇呴』閫忎紶涓?`release_tracking_status=release_trigger_failed` 涓斾笉瑙﹀彂 poll銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trace 鐘舵€併€乻hould_poll銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-31` + +## P2-33 Linux CI Workflow Release Completion Gate +- 鍗″彿锛歚P2-33` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-32` 宸插叿澶?release run trace 濂戠害锛屼絾鈥滅瓑寰?run 瀹屾垚鈥濅粛闇€涓嬫父 job 鑷杞锛岀己灏戠粺涓€瀹屾垚鎬佹敹鏁涖€? - 鏃犵粺涓€ release completion 鎶ュ憡锛屽鑷?release 鎴愬姛/澶辫触/瓒呮椂/杩涜涓殑鏈€缁堣涔夊湪 CI 鍚勯樁娈典笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 release completion gate锛岃鍙?`P2-32` release trace 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_completion.json` + - `ci_workflow_release_completion.md` + - 鏀寔 bounded poll loop锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`銆? - 鏀舵暃 completion_status锛? - `release_run_completed_success` + - `release_run_completed_failure` + - `release_run_in_progress` + - `release_run_await_timeout` + - `release_run_poll_failed` + - 鍙?blocked/dry-run/trigger_failed 绛夐€忎紶鐘舵€併€? - 鏀寔 `--allow-in-progress`锛氬厑璁歌秴鏃朵粛 in-progress 鏃惰繑鍥?exit 0銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€佽疆璇俊鎭€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` + - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_tracking_ready` + poll success 蹇呴』鏀舵暃涓?`release_run_completed_success`銆? - `T2`锛氭寔缁?in-progress 涓旀湭鍏佽 in-progress 蹇呴』鏀舵暃涓?`release_run_await_timeout` 涓旈€€鍑虹爜闈為浂銆? - `T3`锛歳elease poll command 涓?parts 涓嶄竴鑷存椂蹇呴』鎷掔粷鍔犺浇銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乸oll 淇℃伅銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-32` + +## P2-34 Linux CI Workflow Release Terminal Publish Gate +- 鍗″彿锛歚P2-34` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-33` 宸茬粰鍑?release completion verdict锛屼絾涓嬫父 release 鏈€缁堟敹鍙d粛闇€鑷瑙f瀽 completion 璇箟锛屽鏄撲骇鐢?finalize/hold/fail 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release terminal publish 濂戠害锛屼笉鍒╀簬鍚庣画 release governance 涓庣幆澧冩帹杩涚瓥鐣ュ鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal publish gate锛岃鍙?`P2-33` release completion 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_terminal_publish.json` + - `ci_workflow_release_terminal_publish.md` + - 灏?release completion 璇箟缁熶竴鏀舵暃涓?`release_publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_finalize_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_run_completed_success` 蹇呴』 `release_completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚夸簬 Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_completed_success` + `release_completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=passed` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=in_progress` 涓?`release_publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-33` + +## P2-35 Linux CI Workflow Release Finalization Gate +- 鍗″彿锛歚P2-35` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-34` 宸茶緭鍑?release terminal publish verdict锛屼絾鈥滄渶缁堟槸鍚︽敹鍙?release 鐢熷懡鍛ㄦ湡鈥濈殑绛栫暐浠嶅垎鏁e湪涓嬫父 job锛屽鏄撳嚭鐜?finalize/hold/abort 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 finalization 濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈电粰鍑虹ǔ瀹氱殑 release closure 缁撴灉涓庡彲瀹¤鍑哄彛銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release finalization gate锛岃鍙?`P2-34` release terminal publish 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_finalization.json` + - `ci_workflow_release_finalization.md` + - 灏?release terminal publish 璇箟缁熶竴鏀舵暃涓猴細 + - `release_finalization_status`锛坄finalized` / `awaiting_release` / `failed` / `contract_failed`锛? - `release_finalization_decision`锛坄finalize` / `hold` / `abort`锛? - `release_finalization_exit_code` + - 鏀寔 `--on-hold` 绛栫暐锛坄pass`/`fail`锛夋帶鍒?blocked/in_progress 鏀跺彛閫€鍑虹爜璇箟銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_publish_status=passed` 蹇呴』 `release_publish_exit_code=0` 涓?`should_finalize_release=true`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` + - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_publish_status=passed` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_finalization_status=finalized` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_publish_status=in_progress` 涓?`--on-hold=fail` 鏃跺繀椤绘敹鏁涗负 `release_finalization_status=awaiting_release` 涓旈€€鍑虹爜闈?0銆? - `T3`锛歵erminal publish 濂戠害涓嶄竴鑷达紙濡?passed 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_finalization_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-34` + +## P2-36 Linux CI Workflow Release Closure Publish Gate +- 鍗″彿锛歚P2-36` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-35` 宸茶緭鍑?finalization verdict锛屼絾瀵瑰娑堣垂锛堥€氱煡銆佸綊妗c€佷笅娓哥郴缁熸帴鍏ワ級浠嶉渶鑷鎷兼帴瀛楁锛宑losure 璇箟瀹规槗鍑虹幇 close/hold/rollback 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release closure 鍙戝竷濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈靛澶栨彁渚涚ǔ瀹氬崟鍑哄彛锛坢achine-readable + human-readable锛夌粨鏋溿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release closure gate锛岃鍙?`P2-35` release finalization 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_closure.json` + - `ci_workflow_release_closure.md` + - 灏?finalization 璇箟缁熶竴鏀舵暃涓猴細 + - `release_closure_status`锛坄closed` / `pending` / `failed` / `contract_failed`锛? - `release_closure_decision`锛坄ship` / `hold` / `rollback`锛? - `release_closure_exit_code` + - `should_close_release` 涓?`should_notify` + - 鍥哄寲 finalization 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_finalization_status=finalized` 蹇呴』 `release_finalization_decision=finalize` 涓?`release_finalization_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_finalization_status=finalized` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_closure_status=closed` 涓?`release_closure_decision=ship`銆? - `T2`锛歚release_finalization_status=awaiting_release` 鏃跺繀椤绘敹鏁涗负 `release_closure_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歠inalization 濂戠害涓嶄竴鑷达紙濡?finalized 浣?decision!=finalize锛夋椂蹇呴』鏀舵暃涓?`release_closure_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-35` + +## P2-37 Linux CI Workflow Release Archive Publish Gate +- 鍗″彿锛歚P2-37` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-36` 宸茶緭鍑?release closure verdict锛屼絾 release evidence锛坈losure/finalization/trace/dispatch source artifacts锛変粛鏈舰鎴愮粺涓€ machine-readable 褰掓。濂戠害銆? - 缂哄皯缁堢偣褰掓。鍗曞嚭鍙f椂锛孡inux CI 涓嬫父锛堥€氱煡銆佸璁°€佸綊妗g郴缁燂級浠嶉渶鑷鎷兼帴璺緞涓庡垽瀹氾紝瀛樺湪鍙戝竷缁撹涓庤瘉鎹紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release archive gate锛岃鍙?`P2-36` release closure 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_archive.json` + - `ci_workflow_release_archive.md` + - 灏?closure 璇箟缁熶竴鏀舵暃涓猴細 + - `release_archive_status`锛坄ready` / `pending` / `failed` / `contract_failed`锛? - `release_archive_decision`锛坄publish` / `hold` / `block`锛? - `release_archive_exit_code` + - `should_publish_archive` + - 鏋勫缓 evidence manifest锛堝寘鍚?closure 涓庡叾涓婃父 source_* 鎶ュ憡璺緞瀛樺湪鎬э級锛屽彂鐜扮己澶卞嵆寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 archive 鐘舵€併€佸喅绛栥€乵issing artifacts 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_closure_status=closed` 涓?evidence 瀹屾暣鏃跺繀椤绘敹鏁涗负 `release_archive_status=ready` 涓?`release_archive_decision=publish`銆? - `T2`锛歚release_closure_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_archive_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歝losure 濂戠害涓嶄竴鑷达紙濡?closed 浣?decision!=ship锛夋椂蹇呴』鏀舵暃涓?`release_archive_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 archive 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-36` + +## P2-38 Linux CI Workflow Release Verdict Publish Gate +- 鍗″彿锛歚P2-38` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-37` 宸茶緭鍑?release archive verdict锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄槸鍚︽斁琛屽彂甯冣€濈殑鍗曞嚭鍙e垽璇嶏紝缁堟€?ship/hold/block 璇箟鍒嗘暎鍦ㄥ瀛楁涓€? - 缂哄皯鏈€缁?verdict 濂戠害鏃讹紝涓嬫父娴佹按绾夸笌鍙戝竷鐪嬫澘浠嶉渶鑷鎷艰鍒ゅ畾閫昏緫锛屽瓨鍦ㄥ嚭鍙h涔夋紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release verdict gate锛岃鍙?`P2-37` release archive 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_verdict.json` + - `ci_workflow_release_verdict.md` + - 灏?archive 璇箟缁熶竴鏀舵暃涓猴細 + - `release_verdict_status`锛坄published` / `awaiting_archive` / `blocked` / `contract_failed`锛? - `release_verdict_decision`锛坄ship` / `hold` / `block`锛? - `release_verdict_exit_code` + - `should_ship_release` 涓?`should_open_incident` + - 鍥哄寲 archive 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_archive_status=ready` 蹇呴』 `release_archive_decision=publish` 涓?`release_archive_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 浜屾妫€鏌?evidence manifest锛屽彂鐜扮己澶辨椂寮哄埗 `contract_failed` 骞惰ˉ鍏?reason code銆? - 鏀寔 `--github-output` 瀵煎嚭 verdict 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_archive_status=ready` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_verdict_status=published` 涓?`release_verdict_decision=ship`銆? - `T2`锛歚release_archive_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_verdict_status=awaiting_archive` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歛rchive 濂戠害涓嶄竴鑷达紙濡?ready 浣?decision!=publish锛夋椂蹇呴』鏀舵暃涓?`release_verdict_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 verdict 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-37` + +## P2-39 Linux CI Workflow Release Incident Dispatch Gate +- 鍗″彿锛歚P2-39` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-38` 宸茶緭鍑?release verdict 涓?`should_open_incident` 璇箟锛屼絾 Linux CI 浠嶇己灏戠粺涓€鈥滀綍鏃?濡備綍瑙﹀彂 incident鈥濈殑鎵ц濂戠害锛屼笅娓搁渶瑕佽嚜琛屾嫾鎺ュ懡浠や笌澶辫触璇箟銆? - 缂哄皯 incident dispatch 鍗曞嚭鍙f椂锛岀粓鐐瑰憡璀﹂摼璺紙issue/incident system锛夊鏄撳嚭鐜拌Е鍙戞紡鎶ャ€侀噸澶嶈Е鍙戞垨澶辫触涓嶅彲瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release incident gate锛岃鍙?`P2-38` release verdict 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_incident.json` + - `ci_workflow_release_incident.md` + - 灏?verdict 璇箟缁熶竴鏀舵暃涓猴細 + - `incident_dispatch_status`锛坄not_required` / `ready_dry_run` / `dispatched` / `dispatch_failed` / `contract_failed`锛? - `should_dispatch_incident` + - `incident_dispatch_exit_code` + - 鍥哄寲 verdict 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_verdict_status=published` 蹇呴』 `should_open_incident=false`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 auto incident command锛堥粯璁?`gh issue create`锛変笌 `--incident-command` 瑕嗙洊锛屽け璐ユ椂缁熶竴鏀舵暃 error reason銆? - 鏀寔 `--github-output` 瀵煎嚭 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` + - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`incident_dispatch_status=not_required`銆? - `T2`锛歚release_verdict_status=blocked` 涓?`should_open_incident=true` 鏃讹紝dry-run 蹇呴』鏀舵暃涓?`incident_dispatch_status=ready_dry_run`銆? - `T3`锛歷erdict 濂戠害涓嶄竴鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`incident_dispatch_status=contract_failed`銆? - `T4`锛歩ncident command 澶辫触鏃跺繀椤绘敹鏁涗负 `incident_dispatch_status=dispatch_failed` 骞惰ˉ鍏?failure reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-38` +## P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate +- 鍗″彿锛歚P2-40` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-39` 宸茶緭鍑?incident dispatch 濂戠害锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄渶缁堝彂甯冨垽璇嶁€濇敹鍙o紱涓嬫父浠嶉渶鑷鎷兼帴 ship/hold/escalate/block 璇箟銆? - 缂哄皯 terminal verdict 鍗曞嚭鍙f椂锛宺elease 缁堝眬鐪嬫澘涓庤嚜鍔ㄥ寲闂幆锛堥€氱煡銆佷汉宸ユ帴绠°€侀樆鏂級鏄撳嚭鐜拌涔夋紓绉讳笌閲嶅鍒ゅ畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal verdict gate锛岃鍙?`P2-39` release incident 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_terminal_verdict.json` + - `ci_workflow_release_terminal_verdict.md` + - 灏?release+incident 璇箟缁熶竴鏀舵暃涓猴細 + - `release_terminal_verdict_status`锛坄released` / `awaiting_archive` / `blocked_incident_ready_dry_run` / `blocked_incident_dispatched` / `blocked_incident_failed` / `blocked` / `contract_failed`锛? - `release_terminal_verdict_decision`锛坄ship` / `hold` / `escalate` / `block`锛? - `release_terminal_verdict_exit_code` + - `terminal_should_ship_release` + - `terminal_requires_follow_up` + - 鍥哄寲 `P2-38` + `P2-39` 濂戠害涓€鑷存€ф牎楠岋紙渚嬪 `release_verdict_status=published` 蹇呴』 `should_open_incident=false` 涓?`incident_dispatch_status=not_required`锛夛紝涓嶄竴鑷村己鍒?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + `incident_dispatch_status=not_required` 鏃跺繀椤绘敹鏁涗负 `release_terminal_verdict_status=released`銆? - `T2`锛歚release_verdict_status=blocked` + `incident_dispatch_status=ready_dry_run` 鏃讹紝蹇呴』鏀舵暃涓?`release_terminal_verdict_status=blocked_incident_ready_dry_run` 涓?decision=`escalate`銆? - `T3`锛氬绾︿笉涓€鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed`銆? - `T4`锛氳瘉鎹己澶辨椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed` 骞惰ˉ鍏?missing artifact reason銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-39` + +## P2-41 Linux CI Workflow Release Delivery Closure Gate +- ???`P2-41` +- ????`P2` +- ????? + - `P2-40` ??? terminal verdict ???? Linux CI ?????????????????????????????????? + - ?? delivery closure gate ??????? ship/hold/escalate/block ???????????????????????????? +- ????? + - ?? release delivery gate??? `P2-40` release terminal verdict ?????? + - `ci_workflow_release_delivery.json` + - `ci_workflow_release_delivery.md` + - ? terminal verdict + incident ???????? + - `release_delivery_status`?`shipped` / `pending_follow_up` / `blocked_incident` / `blocked_incident_failed` / `blocked` / `contract_failed`? + - `release_delivery_decision`?`deliver` / `hold` / `escalate` / `block`? + - `release_delivery_exit_code` + - `delivery_should_ship_release` + - `delivery_requires_human_action` + - `delivery_should_announce_blocker` + - ?? `P2-40` ?????????? `release_terminal_verdict_status=released` ?? `incident_dispatch_status=not_required`??????? `contract_failed`? + - ?? `--github-output` ?? delivery ??????exit code??????????incident ??/url?run_id/url ?????? +- ????? + - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- ??????? + - `T1`?`release_terminal_verdict_status=released` + `incident_dispatch_status=not_required` ?????? `release_delivery_status=shipped` ? decision=`deliver`? + - `T2`?`release_terminal_verdict_status=blocked_incident_ready_dry_run` ?????? `release_delivery_status=blocked_incident` ? decision=`escalate`? + - `T3`???????? `released` ? `incident_dispatch_status=ready_dry_run`??????? `release_delivery_status=contract_failed`? + - `T4`??????????? `release_delivery_status=contract_failed` ??? missing artifact reason? + - `T5`?GitHub output ?????? delivery ??????exit code????????incident ??/url?run_id/url ?????? +- ?????`P2-40` + + +## P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate +- Card ID: `P2-42` +- Priority: `P2` +- Problem: + - `P2-41` converges to delivery semantics, but Linux CI still lacks one terminal publish contract for downstream notification/hand-off consumers. + - Without one terminal publish output, follow-up automation and human takeover paths must re-interpret delivery fields independently. +- Scope: + - Add terminal publish gate that consumes `P2-41` delivery report and emits: + - `ci_workflow_release_delivery_terminal_publish.json` + - `ci_workflow_release_delivery_terminal_publish.md` + - Converge to normalized outputs: + - `release_delivery_terminal_publish_status` (`published`/`pending_follow_up`/`blocked`/`contract_failed`) + - `release_delivery_terminal_publish_decision` (`announce_release`/`announce_hold`/`announce_blocker`/`abort_publish`) + - `release_delivery_terminal_publish_exit_code` + - `terminal_publish_should_notify` / `terminal_publish_should_create_follow_up` / `terminal_publish_channel` + - Export `--github-output` fields for Linux CI terminal job consumption. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_status=shipped` converges to `published` + `announce_release`. + - `T2`: `release_delivery_status=blocked_incident` converges to `blocked` + `announce_blocker`. + - `T3`: delivery/incident contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/channel/run_id/run_url. +- Depends on: `P2-41` + +## P2-43 Linux CI Workflow Release Delivery Final Verdict Gate +- Card ID: `P2-43` +- Priority: `P2` +- Problem: + - `P2-42` converges release delivery to terminal publish semantics, but Linux CI still lacks one final closure verdict contract for release close/follow-up/escalation takeover. + - Without one final verdict output, downstream consumers must re-interpret terminal publish + delivery + incident fields independently. +- Scope: + - Add final verdict gate that consumes `P2-42` delivery terminal publish report and emits: + - `ci_workflow_release_delivery_final_verdict.json` + - `ci_workflow_release_delivery_final_verdict.md` + - Converge to normalized outputs: + - `release_delivery_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) + - `release_delivery_final_verdict_decision` (`close_release`/`open_follow_up`/`escalate_blocker`/`abort_close`) + - `release_delivery_final_verdict_exit_code` + - `final_should_close_release` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` + - Export `--github-output` fields for Linux CI terminal closeout job consumption. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_terminal_publish_status=published` converges to `completed` + `close_release`. + - `T2`: `release_delivery_terminal_publish_status=pending_follow_up` converges to `requires_follow_up` + `open_follow_up`. + - `T3`: terminal publish / delivery / incident contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/final-action booleans/announcement_target/run_id/run_url. +- Depends on: `P2-42` + +## P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate +- Card ID: `P2-44` +- Priority: `P2` +- Problem: + - `P2-43` converges release delivery to final verdict semantics, but Linux CI still lacks one terminal follow-up dispatch contract for queue/escalation takeover consumers. + - Without one follow-up dispatch output, downstream automation must re-interpret final verdict + incident + publish fields independently. +- Scope: + - Add follow-up dispatch gate that consumes `P2-43` delivery final verdict report and emits: + - `ci_workflow_release_follow_up_dispatch.json` + - `ci_workflow_release_follow_up_dispatch.md` + - Converge to normalized outputs: + - `release_follow_up_dispatch_status` (`closed`/`follow_up_required`/`escalated`/`contract_failed`) + - `release_follow_up_dispatch_decision` (`no_action`/`dispatch_follow_up`/`dispatch_escalation`/`abort_dispatch`) + - `release_follow_up_dispatch_exit_code` + - `follow_up_required` / `escalation_required` / `dispatch_target` + - Export `--github-output` fields for Linux CI final hand-off consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_final_verdict_status=completed` converges to `closed` + `no_action`. + - `T2`: `release_delivery_final_verdict_status=requires_follow_up` converges to `follow_up_required` + `dispatch_follow_up`. + - `T3`: final-verdict contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/follow-up/escalation booleans/dispatch_target/run_id/run_url. +- Depends on: `P2-43` + +## P2-45 Linux CI Workflow Release Follow-Up Closure Gate +- Card ID: `P2-45` +- Priority: `P2` +- Problem: + - `P2-44` converges release delivery final verdict to follow-up dispatch semantics, but Linux CI still lacks one terminal follow-up closure contract for backlog ownership/queue intake consumers. + - Without one follow-up closure output, downstream automation must re-interpret dispatch status + target + escalation flags and cannot safely distinguish dry-run readiness vs actual queue success. +- Scope: + - Add follow-up closure gate that consumes `P2-44` release follow-up dispatch report and emits: + - `ci_workflow_release_follow_up_closure.json` + - `ci_workflow_release_follow_up_closure.md` + - Converge to normalized outputs: + - `release_follow_up_closure_status` (`closed`/`queued_dry_run`/`queued`/`queue_failed`/`contract_failed`) + - `release_follow_up_closure_decision` (`no_action`/`queue_follow_up`/`queue_escalation`/`abort_queue`) + - `release_follow_up_closure_exit_code` + - `should_queue_follow_up` / `follow_up_queue_attempted` / `follow_up_task_queued` / `escalation_task_queued` + - Export `--github-output` fields for Linux CI terminal follow-up hand-off consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_dispatch_status=closed` converges to `closed` + `no_action`. + - `T2`: `release_follow_up_dispatch_status=follow_up_required` converges to `queued_dry_run` + `queue_follow_up` in dry-run mode. + - `T3`: follow-up dispatch contract mismatch converges to `contract_failed`. + - `T4`: executable follow-up queue command failure converges to `queue_failed`. + - `T5`: GitHub output includes status/decision/exit_code/queue booleans/target/queue_url/run_id/run_url. +- Depends on: `P2-44` + +## P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate +- Card ID: `P2-46` +- Priority: `P2` +- Problem: + - `P2-45` converges follow-up dispatch to closure semantics, but Linux CI still lacks one terminal publish contract that cleanly separates `closed`/`queued`/`pending_queue`/`queue_failed` outcomes for downstream notification and hand-off jobs. + - Without one terminal publish output, downstream automation must re-interpret closure status + queue attempt fields and cannot consume a single stable final follow-up signal. +- Scope: + - Add follow-up terminal publish gate that consumes `P2-45` release follow-up closure report and emits: + - `ci_workflow_release_follow_up_terminal_publish.json` + - `ci_workflow_release_follow_up_terminal_publish.md` + - Converge to normalized outputs: + - `release_follow_up_terminal_publish_status` (`published`/`pending_queue`/`queue_failed`/`contract_failed`) + - `release_follow_up_terminal_publish_decision` (`announce_closed`/`announce_queued`/`announce_pending_queue`/`announce_queue_failure`/`abort_publish`) + - `release_follow_up_terminal_publish_exit_code` + - `follow_up_terminal_publish_should_notify` / `follow_up_terminal_requires_manual_action` / `follow_up_terminal_publish_channel` + - Export `--github-output` fields for Linux CI terminal follow-up publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_closure_status=closed` converges to `published` + `announce_closed`. + - `T2`: `release_follow_up_closure_status=queued_dry_run` converges to `pending_queue` + `announce_pending_queue`. + - `T3`: closure contract mismatch converges to `contract_failed`. + - `T4`: `release_follow_up_closure_status=queue_failed` converges to `queue_failed` + `announce_queue_failure`. + - `T5`: GitHub output includes status/decision/exit_code/notify/manual-action/channel/closure-status/queue-url/run_id/run_url. +- Depends on: `P2-45` + +## P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate +- Card ID: `P2-47` +- Priority: `P2` +- Problem: + - `P2-46` converges follow-up closure to terminal publish semantics, but Linux CI still lacks one final verdict contract that cleanly separates `completed`/`requires_follow_up`/`blocked`/`contract_failed` outcomes for post-release ownership and escalation consumers. + - Without one final verdict output, downstream automation must re-interpret terminal publish + closure + queue fields and cannot consume one stable close/open/escalate signal. +- Scope: + - Add follow-up final verdict gate that consumes `P2-46` release follow-up terminal publish report and emits: + - `ci_workflow_release_follow_up_final_verdict.json` + - `ci_workflow_release_follow_up_final_verdict.md` + - Converge to normalized outputs: + - `release_follow_up_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) + - `release_follow_up_final_verdict_decision` (`close_follow_up`/`keep_follow_up_open`/`escalate_queue_failure`/`abort_close`) + - `release_follow_up_final_verdict_exit_code` + - `final_should_close_follow_up` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` + - Export `--github-output` fields for Linux CI terminal follow-up ownership consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_terminal_publish_status=published` + `announce_closed` converges to `completed` + `close_follow_up`. + - `T2`: `release_follow_up_terminal_publish_status=pending_queue` converges to `requires_follow_up` + `keep_follow_up_open`. + - `T3`: `release_follow_up_terminal_publish_status=queue_failed` converges to `blocked` + `escalate_queue_failure`. + - `T4`: terminal publish / closure contract mismatch converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/final booleans/announcement-target/terminal-channel/queue-url/run_id/run_url. +- Depends on: `P2-46` + +## P2-48 Linux CI Workflow Release Final Outcome Gate +- Card ID: `P2-48` +- Priority: `P2` +- Problem: + - `P2-43` delivers release final verdict semantics and `P2-47` delivers follow-up final verdict semantics, but Linux CI still lacks one single terminal outcome contract that downstream release governance and notification consumers can consume without re-stitching two reports. + - Without one final outcome output, downstream automation must merge delivery/follow-up statuses itself and risks drift on ship-with-follow-up vs blocked escalation semantics. +- Scope: + - Add release final outcome gate that consumes `P2-43` release delivery final verdict report and `P2-47` release follow-up final verdict report and emits: + - `ci_workflow_release_final_outcome.json` + - `ci_workflow_release_final_outcome.md` + - Converge to normalized outputs: + - `release_final_outcome_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_outcome_decision` (`ship_and_close`/`ship_with_follow_up_open`/`escalate_blocker`/`abort_outcome`) + - `release_final_outcome_exit_code` + - `final_should_ship_release` / `final_follow_up_open` / `final_should_page_owner` / `final_outcome_target` + - Export `--github-output` fields for Linux CI terminal release ownership consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: delivery=`completed` + follow-up=`completed` converges to `released` + `ship_and_close`. + - `T2`: delivery=`completed` + follow-up=`requires_follow_up` converges to `released_with_follow_up` + `ship_with_follow_up_open`. + - `T3`: delivery or follow-up=`blocked` converges to `blocked` + `escalate_blocker`. + - `T4`: delivery/follow-up contract mismatch (decision/flags/run-id) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/ship+follow-up flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-43`, `P2-47` + +## P2-49 Linux CI Workflow Release Final Terminal Publish Gate +- Card ID: `P2-49` +- Priority: `P2` +- Problem: + - `P2-48` already converges one final release outcome artifact, but Linux CI still lacks one terminal publish contract dedicated to final stakeholder-facing announcement semantics. + - Without a dedicated terminal publish gate, downstream consumers still need custom mapping from outcome states to channel/action semantics, increasing drift risk on `released_with_follow_up` vs `blocked` paths. +- Scope: + - Add release final terminal publish gate that consumes `P2-48` release final outcome report and emits: + - `ci_workflow_release_final_terminal_publish.json` + - `ci_workflow_release_final_terminal_publish.md` + - Converge to normalized outputs: + - `release_final_terminal_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_terminal_publish_decision` (`announce_release_closure`/`announce_release_with_follow_up`/`announce_blocker`/`abort_publish`) + - `release_final_terminal_publish_exit_code` + - `final_terminal_should_notify` / `final_terminal_requires_manual_action` / `final_terminal_channel` + - Export `--github-output` fields for Linux CI terminal publish/notification consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final outcome=`released` converges to `published` + `announce_release_closure`. + - `T2`: final outcome=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_with_follow_up`. + - `T3`: final outcome=`blocked` converges to `blocked` + `announce_blocker`. + - `T4`: final outcome contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/run-id/run-url/report paths. +- Depends on: `P2-48` + +## P2-50 Linux CI Workflow Release Final Handoff Gate +- Card ID: `P2-50` +- Priority: `P2` +- Problem: + - `P2-49` already converges final terminal publish semantics, but Linux CI still lacks one terminal handoff contract that downstream closure/archive consumers can consume directly without remapping publish statuses. + - Without this dedicated handoff gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/handoff ownership semantics. +- Scope: + - Add release final handoff gate that consumes `P2-49` release final terminal publish report and emits: + - `ci_workflow_release_final_handoff.json` + - `ci_workflow_release_final_handoff.md` + - Converge to normalized outputs: + - `release_final_handoff_status` (`completed`/`completed_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_handoff_decision` (`handoff_release_closure`/`handoff_release_with_follow_up`/`handoff_blocker`/`abort_handoff`) + - `release_final_handoff_exit_code` + - `final_handoff_should_archive_release` / `final_handoff_keep_follow_up_open` / `final_handoff_should_page_owner` / `final_handoff_target` + - Export `--github-output` fields for Linux CI terminal handoff/closure consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final terminal publish=`published` converges to `completed` + `handoff_release_closure`. + - `T2`: final terminal publish=`published_with_follow_up` converges to `completed_with_follow_up` + `handoff_release_with_follow_up`. + - `T3`: final terminal publish=`blocked` converges to `blocked` + `handoff_blocker`. + - `T4`: final terminal publish contract mismatch (decision/flags/channel/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/archive+follow-up+page flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-49` + +## P2-51 Linux CI Workflow Release Final Closure Gate +- Card ID: `P2-51` +- Priority: `P2` +- Problem: + - `P2-50` already converges final handoff semantics, but Linux CI still lacks one terminal closure contract that downstream owner-closure consumers can consume directly without remapping handoff statuses. + - Without this dedicated closure gate, downstream automation still repeats custom logic for `completed_with_follow_up` versus `blocked` paths, increasing drift risk on final close ownership semantics. +- Scope: + - Add release final closure gate that consumes `P2-50` release final handoff report and emits: + - `ci_workflow_release_final_closure.json` + - `ci_workflow_release_final_closure.md` + - Converge to normalized outputs: + - `release_final_closure_status` (`closed`/`closed_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_closure_decision` (`close_release`/`close_with_follow_up`/`close_blocker`/`abort_close`) + - `release_final_closure_exit_code` + - `final_closure_is_closed` / `final_closure_has_open_follow_up` / `final_closure_should_page_owner` / `final_closure_target` + - Export `--github-output` fields for Linux CI terminal closure consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final handoff=`completed` converges to `closed` + `close_release`. + - `T2`: final handoff=`completed_with_follow_up` converges to `closed_with_follow_up` + `close_with_follow_up`. + - `T3`: final handoff=`blocked` converges to `blocked` + `close_blocker`. + - `T4`: final handoff contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/closed+follow-up+page flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-50` + +## P2-52 Linux CI Workflow Release Final Closure Publish Gate +- Card ID: `P2-52` +- Priority: `P2` +- Problem: + - `P2-51` already converges final closure semantics, but Linux CI still lacks one terminal publish contract dedicated to final closure announcement semantics for downstream consumers. + - Without this dedicated closure-publish gate, downstream automation still repeats custom logic for `closed_with_follow_up` versus `blocked` paths, increasing drift risk on final announce/manual-action semantics. +- Scope: + - Add release final closure publish gate that consumes `P2-51` release final closure report and emits: + - `ci_workflow_release_final_closure_publish.json` + - `ci_workflow_release_final_closure_publish.md` + - Converge to normalized outputs: + - `release_final_closure_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_closure_publish_decision` (`announce_release_closed`/`announce_release_closed_with_follow_up`/`announce_release_blocker`/`abort_publish`) + - `release_final_closure_publish_exit_code` + - `final_closure_publish_should_notify` / `final_closure_publish_requires_manual_action` / `final_closure_publish_channel` + - Export `--github-output` fields for Linux CI final closure publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final closure=`closed` converges to `published` + `announce_release_closed`. + - `T2`: final closure=`closed_with_follow_up` converges to `published_with_follow_up` + `announce_release_closed_with_follow_up`. + - `T3`: final closure=`blocked` converges to `blocked` + `announce_release_blocker`. + - `T4`: final closure contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-51` + +## P2-53 Linux CI Workflow Release Final Archive Gate +- Card ID: `P2-53` +- Priority: `P2` +- Problem: + - `P2-52` already converges terminal closure publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. + - Without this dedicated final-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics. +- Scope: + - Add release final archive gate that consumes `P2-52` release final closure publish report and emits: + - `ci_workflow_release_final_archive.json` + - `ci_workflow_release_final_archive.md` + - Converge to normalized outputs: + - `release_final_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_archive_decision` (`archive_release_closed`/`archive_release_closed_with_follow_up`/`archive_release_blocker`/`abort_archive`) + - `release_final_archive_exit_code` + - `final_archive_should_publish` / `final_archive_requires_manual_action` / `final_archive_channel` + - Export `--github-output` fields for Linux CI final archive consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final closure publish=`published` converges to `archived` + `archive_release_closed`. + - `T2`: final closure publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_closed_with_follow_up`. + - `T3`: final closure publish=`blocked` converges to `blocked` + `archive_release_blocker`. + - `T4`: final closure publish contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/publish+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-52` + +## P2-54 Linux CI Workflow Release Final Verdict Gate +- Card ID: `P2-54` +- Priority: `P2` +- Problem: + - `P2-53` already converges final archive semantics, but Linux CI still lacks one terminal final-verdict contract that downstream release decision consumers can consume directly without remapping archive statuses. + - Without this dedicated final-verdict gate, downstream automation still repeats custom logic for `archived_with_follow_up` versus `blocked` paths, increasing drift risk on ship/manual-action semantics. +- Scope: + - Add release final verdict gate that consumes `P2-53` release final archive report and emits: + - `ci_workflow_release_final_verdict.json` + - `ci_workflow_release_final_verdict.md` + - Converge to normalized outputs: + - `release_final_verdict_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_verdict_decision` (`ship_release`/`ship_release_with_follow_up`/`escalate_release_blocker`/`abort_verdict`) + - `release_final_verdict_exit_code` + - `final_verdict_should_ship` / `final_verdict_requires_manual_action` / `final_verdict_channel` + - Export `--github-output` fields for Linux CI final verdict consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final archive=`archived` converges to `released` + `ship_release`. + - `T2`: final archive=`archived_with_follow_up` converges to `released_with_follow_up` + `ship_release_with_follow_up`. + - `T3`: final archive=`blocked` converges to `blocked` + `escalate_release_blocker`. + - `T4`: final archive contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/ship+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-53` + +## P2-55 Linux CI Workflow Release Final Verdict Publish Gate +- Card ID: `P2-55` +- Priority: `P2` +- Problem: + - `P2-54` already converges final verdict semantics, but Linux CI still lacks one terminal publish contract that downstream notification/audit consumers can consume directly without remapping verdict statuses. + - Without this dedicated final-verdict-publish gate, downstream automation still repeats custom logic for `released_with_follow_up` versus `blocked` paths, increasing drift risk on notify/manual-action semantics. +- Scope: + - Add release final verdict publish gate that consumes `P2-54` release final verdict report and emits: + - `ci_workflow_release_final_verdict_publish.json` + - `ci_workflow_release_final_verdict_publish.md` + - Converge to normalized outputs: + - `release_final_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_verdict_publish_decision` (`announce_release_shipped`/`announce_release_shipped_with_follow_up`/`announce_release_blocker`/`abort_publish`) + - `release_final_verdict_publish_exit_code` + - `final_verdict_publish_should_notify` / `final_verdict_publish_requires_manual_action` / `final_verdict_publish_channel` + - Export `--github-output` fields for Linux CI final verdict publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final verdict=`released` converges to `published` + `announce_release_shipped`. + - `T2`: final verdict=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_shipped_with_follow_up`. + - `T3`: final verdict=`blocked` converges to `blocked` + `announce_release_blocker`. + - `T4`: final verdict contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-54` + +## P2-56 Linux CI Workflow Release Final Publish Archive Gate +- Card ID: `P2-56` +- Priority: `P2` +- Problem: + - `P2-55` already converges final verdict publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. + - Without this dedicated final-publish-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics. +- Scope: + - Add release final publish archive gate that consumes `P2-55` release final verdict publish report and emits: + - `ci_workflow_release_final_publish_archive.json` + - `ci_workflow_release_final_publish_archive.md` + - Converge to normalized outputs: + - `release_final_publish_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_publish_archive_decision` (`archive_release_shipped`/`archive_release_shipped_with_follow_up`/`archive_release_blocker`/`abort_archive`) + - `release_final_publish_archive_exit_code` + - `final_publish_archive_should_archive` / `final_publish_archive_requires_manual_action` / `final_publish_archive_channel` + - Export `--github-output` fields for Linux CI final publish archive consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final verdict publish=`published` converges to `archived` + `archive_release_shipped`. + - `T2`: final verdict publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_shipped_with_follow_up`. + - `T3`: final verdict publish=`blocked` converges to `blocked` + `archive_release_blocker`. + - `T4`: final verdict publish contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/archive+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-55` + +## P2-57 Linux Gate Manifest Drift Closure Gate +- Card ID: `P2-57` +- Priority: `P2` +- Problem: + - As `P2-xx` gate cards keep growing, there is no single guard that continuously verifies closure across gate scripts, runtime contract tests, and Linux unified manifest wiring. + - Without this closure gate, future card additions can silently miss `test_*_gate_runtime.py` or omit Linux manifest entries, only failing late in Linux stage. +- Scope: + - Add a dedicated drift-closure gate that scans: + - `scripts/run_*_gate.py` (excluding `run_linux_unified_gate.py`) + - expected `tests/test_*_gate_runtime.py` + - `scripts/run_linux_unified_gate.py::LINUX_UNIFIED_TEST_FILES` + - Emit normalized drift report artifacts: + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.md` + - Export `--github-output` fields for CI observability: + - status + missing-runtime-tests count + missing-manifest count + orphan-manifest count. +- Implementation: + - `scripts/run_p2_linux_gate_manifest_drift_gate.py` + - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: gate script scan includes `run_p2_linux_gate_manifest_drift_gate.py` and excludes `run_linux_unified_gate.py`. + - `T2`: expected runtime test mapping follows `run_xxx_gate.py` -> `test_xxx_gate_runtime.py`. + - `T3`: repository baseline report converges to `status=passed` with zero missing runtime tests / manifest entries / orphan manifest entries. + - `T4`: GitHub output fields expose status and drift counters. +- Depends on: `P2-56` + +## P2-59 Linux CI Workflow Terminal Verdict Closure Gate +- Card ID: `P2-59` +- Priority: `P2` +- Problem: + - `P2-56` and `P2-57` are both terminal quality signals, but Linux CI still lacks one single contract that downstream automation can consume as a final go/no-go verdict. + - Without a unified terminal verdict, Linux validation entry and release control logic must duplicate merge rules across two artifacts, increasing drift risk. +- Scope: + - Add one terminal verdict gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json` (`P2-56`) + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` (`P2-57`) + - Emit normalized outputs: + - `terminal_verdict_status` (`ready_for_linux_validation`/`ready_with_follow_up_for_linux_validation`/`blocked`/`contract_failed`) + - `terminal_verdict_decision` (`proceed_linux_validation`/`proceed_linux_validation_with_follow_up`/`halt_linux_validation_blocker`/`abort_linux_validation`) + - `terminal_verdict_exit_code` + - `terminal_verdict_should_proceed` / `terminal_verdict_requires_manual_action` / `terminal_verdict_channel` + - Export `--github-output` fields for Linux CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: publish-archive=`archived` + drift=`passed` converges to `ready_for_linux_validation` + `proceed_linux_validation`. + - `T2`: publish-archive=`archived_with_follow_up` + drift=`passed` converges to `ready_with_follow_up_for_linux_validation` + `proceed_linux_validation_with_follow_up`. + - `T3`: publish-archive=`blocked` + drift=`passed` converges to `blocked` + `halt_linux_validation_blocker`. + - `T4`: publish-archive mismatch or drift failure converges to `contract_failed` + `abort_linux_validation`. + - `T5`: GitHub output includes status/decision/exit_code/proceed+manual-action/channel/drift counters/report paths. +- Depends on: `P2-57` + +## P2-60 Linux CI Workflow Linux Validation Dispatch Gate +- Card ID: `P2-60` +- Priority: `P2` +- Problem: + - `P2-59` gives a terminal go/no-go contract, but Linux CI still lacks a single dispatch contract that transforms terminal verdict into an executable Linux validation action. + - Without a dedicated Linux-validation-dispatch gate, downstream jobs still duplicate mapping logic across `ready_for_linux_validation` / `ready_with_follow_up_for_linux_validation` / `blocked` / `contract_failed` paths. +- Scope: + - Add one Linux validation dispatch gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json` (`P2-59`) + - Emit normalized outputs: + - `linux_validation_dispatch_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatched`/`dispatch_failed`/`blocked`/`contract_failed`) + - `linux_validation_dispatch_decision` (`dispatch_linux_validation`/`dispatch_linux_validation_with_follow_up`/`hold_linux_validation_blocker`/`abort_linux_validation_dispatch`) + - `linux_validation_dispatch_exit_code` + - `linux_validation_should_dispatch` / `linux_validation_requires_manual_action` / `linux_validation_channel` + - Support optional command execution with timeout control: + - default command: `python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure` + - optional override: `--linux-validation-command` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_dispatch_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: terminal verdict=`ready_for_linux_validation` converges to `ready_dry_run` + `dispatch_linux_validation`. + - `T2`: terminal verdict=`ready_with_follow_up_for_linux_validation` converges to `ready_with_follow_up_dry_run` + `dispatch_linux_validation_with_follow_up`. + - `T3`: terminal verdict=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. + - `T4`: terminal verdict mismatch or drift/evidence issue converges to `contract_failed` + `abort_linux_validation_dispatch`. + - `T5`: GitHub output includes status/decision/exit_code/dispatch+manual-action/channel/run-id/run-url/follow-up/report paths. +- Depends on: `P2-59` + +## P2-61 Linux CI Workflow Linux Validation Verdict Gate +- Card ID: `P2-61` +- Priority: `P2` +- Problem: + - `P2-60` converges Linux validation dispatch behavior, but Linux CI still lacks one terminal verdict artifact that downstream release governance can consume without re-parsing dispatch internals. + - Without a dedicated Linux-validation-verdict gate, status handling for `dispatched/dispatch_failed/blocked/contract_failed` remains duplicated across consumers and can drift from terminal-verdict and manifest-drift contracts. +- Scope: + - Add one Linux validation verdict gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json` (`P2-60`) + - Emit normalized outputs: + - `linux_validation_verdict_status` (`validated`/`validated_with_follow_up`/`validation_failed`/`blocked`/`contract_failed`) + - `linux_validation_verdict_decision` (`accept_linux_validation`/`accept_linux_validation_with_follow_up`/`escalate_linux_validation_failure`/`hold_linux_validation_blocker`/`abort_linux_validation_verdict`) + - `linux_validation_verdict_exit_code` + - `linux_validation_passed` / `linux_validation_verdict_requires_manual_action` / `linux_validation_verdict_channel` + - Support optional verdict command execution with timeout control: + - optional override: `--linux-validation-verdict-command` + - timeout control: `--linux-validation-verdict-timeout-seconds` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: dispatch=`dispatched` + terminal=`ready_for_linux_validation` converges to `validated` + `accept_linux_validation`. + - `T2`: dispatch=`dispatched` + terminal=`ready_with_follow_up_for_linux_validation` converges to `validated_with_follow_up` + `accept_linux_validation_with_follow_up`. + - `T3`: dispatch=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. + - `T4`: dispatch=`dispatch_failed` converges to `validation_failed` + `escalate_linux_validation_failure`; any contract mismatch/drift/evidence issue converges to `contract_failed` + `abort_linux_validation_verdict`. + - `T5`: GitHub output includes verdict status/decision/exit_code/passed+manual-action/channel/dispatch status+decision/report paths. +- Depends on: `P2-60` + +## P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate +- Card ID: `P2-62` +- Priority: `P2` +- Problem: + - `P2-61` converges Linux validation execution into one verdict contract, but Linux CI still lacks a dedicated publish contract that downstream release governance can consume without re-parsing verdict internals. + - Without a publish gate, status handling for `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` can drift across dashboards, notifications, and final closure pipelines. +- Scope: + - Add one Linux validation verdict publish gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json` (`P2-61`) + - Emit normalized outputs: + - `linux_validation_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `linux_validation_verdict_publish_decision` (`announce_linux_validation_passed`/`announce_linux_validation_passed_with_follow_up`/`announce_linux_validation_blocker`/`abort_publish`) + - `linux_validation_verdict_publish_exit_code` + - `linux_validation_verdict_publish_should_notify` / `linux_validation_verdict_publish_requires_manual_action` / `linux_validation_verdict_publish_channel` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: verdict=`validated` converges to `published` + `announce_linux_validation_passed`. + - `T2`: verdict=`validated_with_follow_up` converges to `published_with_follow_up` + `announce_linux_validation_passed_with_follow_up`. + - `T3`: verdict=`blocked` converges to `blocked` + `announce_linux_validation_blocker`. + - `T4`: verdict contract mismatch/evidence issue converges to `contract_failed` + `abort_publish`. + - `T5`: GitHub output includes publish status/decision/exit_code/notify+manual-action/channel/run-id/run-url/follow-up/report paths. +- Depends on: `P2-61` + +## 8. 鎺ㄨ崘杩唬椤哄簭 + +鑻ュ彧鍏佽涓€杞竴杞帹杩涳紝寤鸿椤哄簭濡備笅锛? +1. `P0-01` +2. `P0-02` +3. `P0-03` +4. `P0-04` +5. `P0-05` +6. `P0-06` +7. `P0-07` +8. `P0-09` +9. `P0-08` +10. `P0-10` + +璇存槑锛? +- `P0-08` 涓嶉樆濉炰富鍔熻兘淇锛屽洜姝ゆ斁鍦?`P0-09` 鍚庨潰浜﹀彲 +- `P0-10` 浣滀负鏀跺彛鍗★紝搴旀斁鍦?Phase 0 灏鹃儴 + +## 9. 姣忚疆杩唬鐨勬渶灏忎骇鍑鸿姹? +姣忓畬鎴愪竴寮犲崱锛岃嚦灏戝悓姝ヤ互涓嬩骇鐗╋細 + +1. 浠g爜鍙樻洿 +2. 娴嬭瘯鍙樻洿 +3. 鏂囨。鍙樻洿 +4. 鎵嬪伐楠屾敹鍛戒护 +5. 椋庨櫓涓庡墿浣欑洸鍖? +寤鸿姣忓紶鍗$殑浜や粯璇存槑缁熶竴閲囩敤锛? +```text +銆愪换鍔″崱銆慞0-XX +銆愯В鍐炽€戔€︹€?銆愬畬鎴愩€戔€︹€?銆愭祴璇曘€戔€︹€?銆愰闄┿€戔€︹€?銆愪笅涓€鍗°€戔€︹€?``` + +## 10. 褰撳墠鏈€閫傚悎绔嬪嵆寮€宸ョ殑鍗″崟 + +鑻ョ珛鍒昏 Codex 涓嬪垁锛屾帹鑽愮涓€鎵瑰紑宸ュ崱锛? +1. `P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦` +2. `P0-02 /resume 鐪熼棴鐜痐 +3. `P0-03 auth/config 涓婚摼淇涓庣粺涓€` +4. `P0-04 feature flag 杩愯鏃朵慨澶峘 +5. `P0-05 review 鍛戒护浜у搧鍖朻 + +鐞嗙敱锛? +- 杩?5 寮犲崱鐩存帴鍐冲畾椤圭洰鏄惁浠庘€滃彲璺戜唬鐮佲€濊繘鍏モ€滃彲鏂藉伐浜у搧鈥?- 瀹冧滑閮藉睘浜庨珮纭畾鎬с€侀珮鏀剁泭銆佸彲鏈湴楠岃瘉鐨勪换鍔?- 鍏舵祴璇曞洖鎶ョ巼鏈€楂橈紝涓旇兘涓哄悗缁钩鍙板寲鍗″崟鎵竻闅滅 + +## 11. 鏂囨。缁存姢瑙勫垯 + +- 鑻ユ煇寮犱换鍔″崱杩涘叆寮€鍙戯紝鏂板鎵ц璁板綍鏃朵紭鍏堟洿鏂版湰鏂囦欢锛岃€屼笉鏄钩琛屾柊寤衡€滃悓涓婚灏忔枃妗b€?- 鑻ヤ竴寮犱换鍔″崱琚户缁媶鍒嗭紝浣跨敤瀛愮紪鍙凤細 + - `P0-03A` + - `P0-03B` +- 鑻ョ瓥鐣ュ彉鍖栧奖鍝嶄换鍔¤竟鐣岋紝鍏堟洿鏂颁笂娓告垬鐣ユ枃妗o紝鍐嶅洖鍐欐湰鏂囦欢 + +## 12. 缁撹 + +鏈枃妗g殑鐩爣涓嶆槸鈥滃垪寰呭姙浜嬮」鈥濓紝鑰屾槸寤虹珛涓€涓爺鍙戝彲鎵ц銆佹祴璇曞彲楠屾敹銆佺鐞嗗彲璺熻釜鐨勬柦宸ラ潰銆? +浠庣幇鍦ㄥ紑濮嬶紝鍚庣画鎵€鏈夊紑宸ュ缓璁兘搴斿熀浜庝换鍔″崱锛岃€屼笉鏄熀浜庢ā绯婃剰鍥俱€? +鎹㈣█涔嬶細 + +> 鍏堟寜鍗″紑宸ワ紝鍐嶆寜鍗¢獙灏革紝鍐嶆寜鍗℃帹杩涳紝涓嶅啀鍑劅瑙夊仛閲嶆瀯銆? +## 13. Execution Log (2026-04-29) + +- Card: `P0-01 Runtime Backbone` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `claude_code/mcp/server.py` +- Test scripts landed: + - `tests/test_main_runtime.py` (runtime bootstrap + mcp serve wiring coverage) + - `tests/test_runtime_bootstrap.py` (entrypoint consistency + mcp registry injection) +- Not executed locally per release policy: + - Unit/component/integration tests will be executed in Linux unified validation stage. + +## 14. Execution Log (2026-04-29, P0-02) + +- Card: `P0-02 /resume Runtime Closure` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` +- Test scripts landed: + - `tests/test_resume_runtime.py` +- Not executed locally per release policy: + - Resume-related unit/component/integration checks will run in Linux unified validation stage. + +## 15. Execution Log (2026-04-29, P0-03) + +- Card: `P0-03 Auth/Config Runtime Unification` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/auth/__init__.py` +- Test scripts landed: + - `tests/test_commands_auth_runtime.py` + - `tests/test_main_runtime.py` (auth key precedence coverage extension) +- Not executed locally per release policy: + - Auth/config unit/component/integration checks will run in Linux unified validation stage. + +## 16. Execution Log (2026-04-29, P0-04) + +- Card: `P0-04 Feature Flag Runtime Repair` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/features.py` +- Test scripts landed: + - `tests/test_features_runtime.py` +- Not executed locally per release policy: + - Feature flag unit/component checks will run in Linux unified validation stage. + +## 17. Execution Log (2026-04-29, P0-05) + +- Card: `P0-05 Review Command Productization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/review/__init__.py` + - `claude_code/services/review_service.py` +- Test scripts landed: + - `tests/test_review_command_runtime.py` +- Not executed locally per release policy: + - Review command unit/component/integration checks will run in Linux unified validation stage. + +## 18. Execution Log (2026-04-29, P0-06) + +- Card: `P0-06 Session/History/Memory Boundary Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_session_history_memory_runtime.py` + - `tests/test_context_builder_runtime.py` (boundary assertions extension) + - `tests/test_main_runtime.py` (runtime service wiring extension) +- Not executed locally per release policy: + - Session/history/memory unit/component/integration checks will run in Linux unified validation stage. + +## 19. Execution Log (2026-04-29, P0-07) + +- Card: `P0-07 Hooks Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_hooks_runtime.py` + - `tests/test_main_runtime.py` (hooks manager runtime bootstrap wiring update) +- Not executed locally per release policy: + - Hooks runtime unit/component/integration checks will run in Linux unified validation stage. + +## 20. Execution Log (2026-04-29, P0-09) + +- Card: `P0-09 Temp Artifact Hygiene` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `tests/test_plugins_runtime.py` + - `.gitignore` +- Test scripts landed: + - `tests/test_plugins_runtime.py` (fixture teardown cleanup assertion + idempotent cleanup coverage) +- Not executed locally per release policy: + - Plugin runtime cleanup and residue-isolation checks will run in Linux unified validation stage. + +## 21. Execution Log (2026-04-29, P0-08) + +- Card: `P0-08 Windows Bootstrap & Interpreter Probe Stabilization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `README.md` +- Test scripts landed: + - `tests/test_doctor_runtime.py` +- Not executed locally per release policy: + - Doctor interpreter-detection unit/component checks and Windows stub regression cases will run in Linux unified validation stage. + +## 22. Execution Log (2026-04-29, P0-10) + +- Card: `P0-10 Phase 0 Regression Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `pytest.ini` + - `scripts/run_phase0_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_phase0_regression_contract.py` +- Not executed locally per release policy: + - Phase 0 gate contract and curated regression-entry checks will run in Linux unified validation stage. + +## 23. Execution Log (2026-04-29, P1-01) + +- Card: `P1-01 Daemon/API Control Plane` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` + - `claude_code/server/__init__.py` + - `claude_code/main.py` + - `scripts/run_p1_control_plane_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_server_runtime.py` (`T1` schema validation, `T2` service behavior, `T3` client->server->runtime chain, `T4` daemon down/timeout/session-not-found) + - `tests/test_main_runtime.py` (daemon CLI bootstrap wiring coverage) +- Not executed locally per release policy: + - Daemon/API unit/component/integration/regression checks will run in Linux unified validation stage. + +## 24. Execution Log (2026-04-29, P1-02) + +- Card: `P1-02 Event Journal` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/event_journal.py` + - `claude_code/services/__init__.py` + - `claude_code/tasks/manager.py` + - `claude_code/tasks/factory.py` + - `claude_code/tasks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/server/control_plane.py` + - `claude_code/main.py` + - `scripts/run_p1_event_journal_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_event_journal_runtime.py` (`T1` schema/versioning, `T2` writer/reader correctness, `T3` query->tool->task event chain, `T4` partial write/corruption recovery) + - `tests/test_server_runtime.py` (event list/replay daemon API coverage) + - `tests/test_main_runtime.py` (runtime bootstrap wiring includes event journal) +- Not executed locally per release policy: + - Event journal unit/component/integration/regression checks will run in Linux unified validation stage. + +## 25. Execution Log (2026-04-29, P1-03) + +- Card: `P1-03 SQLite Runtime State Backend` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tasks/repository.py` (SQLite runtime repository + schema version/migration guard) + - `claude_code/tasks/factory.py` (runtime backend default switched to `sqlite`) + - `claude_code/tasks/__init__.py` (SQLite repository exports) + - `claude_code/engine/session.py` (SQLite session store + manager constructor hook) + - `claude_code/services/event_journal.py` (SQLite event journal + schema version/migration guard) + - `claude_code/services/__init__.py` (SQLite event journal exports) + - `claude_code/engine/query.py` (event journal duck-typing for backend-agnostic write path) + - `claude_code/main.py` (runtime bootstrap switched to unified `.claude/runtime_state.db`) + - `scripts/run_p1_sqlite_state_backend_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_sqlite_runtime_state_backend.py` (`T1` schema init/version guard, `T2` repository/session/event correctness, `T3` runtime chain wiring) + - `tests/test_tasks_backend_contract.py` (SQLite repository contract + schema guard extension) + - `tests/test_tasks_factory_runtime.py` (default runtime backend contract update to `sqlite`) + - `tests/test_tasks_manager_runtime.py` (SQLite lifecycle persistence/recovery extension) + - `tests/test_event_journal_runtime.py` (SQLite event journal schema/query/replay guard extension) + - `tests/test_main_runtime.py` (runtime bootstrap mocks updated for SQLite state wiring) +- Not executed locally per release policy: + - SQLite runtime state backend unit/component/integration/regression checks will run in Linux unified validation stage. + +## 26. Execution Log (2026-04-30, P1-04) + +- Card: `P1-04 Active Memory Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/memory_service.py` (scoped memory namespaces + scope snapshot API) + - `claude_code/engine/query.py` (active memory hit audit event + runtime memory scope propagation) + - `claude_code/main.py` (system prompt active memory snapshot injection + runtime memory scope wiring) + - `claude_code/tools/base.py` (tool context memory scope) + - `claude_code/tools/agent/__init__.py` (agent-level memory scope propagation) + - `claude_code/agents/builtin.py` (builtin agent memory scope defaults) + - `scripts/run_p1_active_memory_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_memory_scope_runtime.py` (`T1` scope namespace isolation, scoped snapshot boundaries) + - `tests/test_active_memory_runtime.py` (`T2` system-prompt memory injection, `T3` query memory-hit event chain, runtime scope propagation) +- Not executed locally per release policy: + - Active memory unit/component/integration/regression checks will run in Linux unified validation stage. + +## 27. Execution Log (2026-04-30, P1-05) + +- Card: `P1-05 Hook/Permission/Audit Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/permissions.py` (structured permission evaluation reason output for audit chain) + - `claude_code/services/hooks_manager.py` (hook result serialization helper) + - `claude_code/engine/query.py` (permission request/decision events + hook execution audit events) + - `scripts/run_p1_hook_permission_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_hook_permission_audit_runtime.py` (`T2` permission+hook+audit linkage, `T3` tool execution audit chain, `T4` deny/error partial-failure path) +- Not executed locally per release policy: + - Hook/permission/audit convergence unit/component/integration/regression checks will run in Linux unified validation stage. + +## 28. Execution Log (2026-04-30, P1-06) + +- Card: `P1-06 CLI Thin Client Migration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` (daemon thin-client mode for query/pipe path, daemon-required hard-fail option, local fallback branch) + - `scripts/run_p1_cli_thin_client_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_cli_daemon_thin_client_runtime.py` (`T2` client adapter mode resolution contract) + - `tests/test_main_runtime.py` (daemon thin-client success/fallback/fail-fast and pipe-mode chain coverage) +- Not executed locally per release policy: + - CLI thin-client adapter and daemon-fallback regression checks will run in Linux unified validation stage. + +## 29. Execution Log (2026-04-30, P2-01) + +- Card: `P2-01 Multi-Agent Supervisor` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/multi_agent_supervisor.py` (agent DAG scheduler, budget/timeout/retry policy, dependency artifact passing, parallel fan-out and aggregate close) + - `claude_code/services/__init__.py` (supervisor exports) + - `claude_code/tools/agent/__init__.py` (orchestrate mode + workflow schema + supervisor integration with structured artifact summary) + - `scripts/run_p2_multi_agent_supervisor_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_multi_agent_supervisor_runtime.py` (`T1` graph cycle guard, `T2` budget/timeout state machine, `T3` parallel scheduling, `T4` retry-based failure recovery; plus agent tool orchestrate path and config error branch) +- Not executed locally per release policy: + - Multi-agent supervisor unit/component/integration/regression checks will run in Linux unified validation stage. + +## 30. Execution Log (2026-04-30, P2-02) + +- Card: `P2-02 Artifact Bus & Conflict Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/artifact_bus.py` (artifact schema contract for patch/note/diff/report/finding, producer payload parsing, merge strategies `fail|append|replace`, conflict error path) + - `claude_code/services/multi_agent_supervisor.py` (artifact bus integration for dependency artifact passing, workflow-level artifact snapshot, node artifact conflict/scheme failure handling) + - `claude_code/services/__init__.py` (artifact bus exports) + - `scripts/run_p2_artifact_bus_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_artifact_bus_runtime.py` (`T1` schema validation, `T2` producer/consumer dependency view, `T4` conflict-fail path, append merge behavior) + - `tests/test_multi_agent_supervisor_runtime.py` (artifact bus summary extension + conflict merge failure branch in DAG execution) +- Not executed locally per release policy: + - Artifact bus unit/component/integration/regression checks will run in Linux unified validation stage. + +## 31. Execution Log (2026-04-30, P2-03) + +- Card: `P2-03 IDE Integration (VS Code first)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` (added `/api/v1/ide/workspace` endpoint for IDE aggregate snapshot: diff, changed files, tasks, sessions, findings) + - `claude_code/server/ide_adapter.py` (VS Code client adapter and normalized workspace snapshot model) + - `claude_code/server/__init__.py` (IDE adapter exports) + - `scripts/run_p2_ide_integration_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_ide_integration_runtime.py` (`T2` client protocol adapter normalization, `T3` IDE -> daemon core chain for diff/task/session/findings) +- Not executed locally per release policy: + - IDE integration unit/component/integration/manual acceptance checks will run in Linux unified validation stage. + +## 32. Execution Log (2026-04-30, P2-04) + +- Card: `P2-04 GitHub/CI Workflow Agent` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/github_ci_workflow.py` (issue->plan->code->test->review->PR state machine, headless CI mode, command failure classification, structured workflow report) + - `claude_code/server/control_plane.py` (added `POST /api/v1/workflows/github-ci` endpoint + client helper for workflow execution) + - `claude_code/services/__init__.py` (GitHub/CI workflow service exports) + - `scripts/run_p2_github_ci_workflow_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_github_ci_workflow_runtime.py` (`T2` workflow state machine, `T3` fake repo/fake PR main chain, `T4` permission-denied/repo-dirty/network-failure branches) + - `tests/test_server_runtime.py` (daemon API route coverage for workflow success/failure mapping) +- Not executed locally per release policy: + - GitHub/CI workflow unit/component/integration/regression checks will run in Linux unified validation stage. + +## 33. Execution Log (2026-04-30, P2-05) + +- Card: `P2-05 Org Policy & Audit` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/org_policy_audit.py` (org policy matcher, secret redaction, approval queue transitions, audit event query/report) + - `claude_code/server/control_plane.py` (added org policy endpoints and client helpers: evaluate/approve/list/report) + - `claude_code/services/__init__.py` (Org policy audit service exports) + - `scripts/run_p2_org_policy_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_org_policy_audit_runtime.py` (`T1` policy matcher+redaction, `T2` approval transitions, `T3` audit report chain, `T4` policy conflict block) + - `tests/test_server_runtime.py` (daemon API coverage for org policy evaluate/approval/audit/report and failure mapping) +- Not executed locally per release policy: + - Org policy/audit unit/component/integration/regression checks will run in Linux unified validation stage. + +## 34. Execution Log (2026-04-30, P2-06) + +- Card: `P2-06 Linux Unified Verification Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_linux_unified_gate.py` (aggregated Linux-stage command manifest for Phase 0 -> Phase 2 runtime chain) + - `README.md` (added unified gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (added P2-06 card + execution log) +- Test scripts landed: + - `tests/test_linux_unified_gate_runtime.py` (`T1` manifest file existence contract, `T2` command generation order/format contract) +- Not executed locally per release policy: + - Unified gate contract checks and all listed runtime tests will run in Linux unified validation stage. + +## 35. Execution Log (2026-04-30, P2-07) + +- Card: `P2-07 Agent Runtime Parity (sync/background)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tools/base.py` (ToolContext adds conversation/provider/task_manager/tool_registry fields for runtime propagation) + - `claude_code/engine/query.py` (ToolContext now carries conversation/provider/task_manager/registry into tool execution path) + - `claude_code/tasks/manager.py` (`create_agent_task` supports metadata injection for audit/replay) + - `claude_code/tools/agent/__init__.py` (sync/background sub-agent parity: runtime provider/model resolution, registry propagation, task-manager reuse, background metadata contract) + - `scripts/run_p2_agent_runtime_parity_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-07 regression test to unified manifest) + - `README.md` (adds P2-07 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-07 card and execution log) +- Test scripts landed: + - `tests/test_agent_background_context_runtime.py` (`T2` sync/background context parity, `T3` background task metadata audit fields, `T4` provider/model inheritance regression) +- Not executed locally per release policy: + - Agent runtime parity unit/component/integration checks will run in Linux unified validation stage. + +## 36. Execution Log (2026-04-30, P2-08) + +- Card: `P2-08 Custom Agents Directory Loader` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/agents/loader.py` (custom agent markdown loader with YAML frontmatter parsing and cwd->git-root directory merge semantics) + - `claude_code/tools/agent/__init__.py` (lazy custom agent load, custom>builtin resolution, and merged unknown-type availability hints) + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-08 regression test to unified manifest) + - `README.md` (adds P2-08 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-08 card and execution log) +- Test scripts landed: + - `tests/test_custom_agents_loader_runtime.py` (`T1` frontmatter parsing, `T2` nested override merge, `T3` custom override builtin, `T4` invalid schema tolerance and unknown-type availability message) +- Not executed locally per release policy: + - Custom-agent loader unit/component/integration checks will run in Linux unified validation stage. + + +## 37. Execution Log (2026-04-30, P2-09) + +- Card: `P2-09 Linux Unified Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (Linux-stage executor over unified manifest; supports dry-run/print/continue-on-failure/report-dir and writes junit + summary json) + - `scripts/run_linux_unified_gate.py` (adds P2-09 execution gate contract test to unified manifest) + - `README.md` (adds P2-09 execution gate command entries) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-09 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (`T1` unified manifest order contract, `T2` junit path and extra-args forwarding contract) +- Not executed locally per release policy: + - Linux unified execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 38. Execution Log (2026-04-30, P2-10) + +- Card: `P2-10 JetBrains IDE Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/ide_adapter.py` (added `JetBrainsClientAdapter` + `JetBrainsWorkspaceSnapshot` + findings->inspections mapping with severity highlight normalization) + - `claude_code/server/__init__.py` (exports JetBrains adapter symbols) + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-10 adapter contract test to unified manifest) + - `README.md` (adds P2-10 gate command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-10 card and execution log) +- Test scripts landed: + - `tests/test_p2_jetbrains_ide_integration_runtime.py` (`T1` adapter snapshot normalization, `T2` findings->inspection highlight mapping, `T4` line type coercion tolerance) +- Not executed locally per release policy: + - JetBrains IDE adapter unit/component checks will run in Linux unified validation stage. + +## 39. Execution Log (2026-04-30, P2-11) + +- Card: `P2-11 Linux Sharded Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (adds deterministic shard slicing via `--shard-total` + `--shard-index`, shard-aware dry-run header, shard metadata in summary payload) + - `README.md` (adds Linux shard-parallel invocation example) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-11 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds shard slicing contract, invalid shard argument guards, summary shard metadata contract) +- Not executed locally per release policy: + - P2-11 shard execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 40. Execution Log (2026-04-30, P2-12) + +- Card: `P2-12 Linux Shard Aggregation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_aggregation_gate.py` (discovers shard summaries via direct path/glob/artifacts dir, validates summary schema, merges shard totals, emits merged summary and global pass/fail) + - `scripts/run_linux_unified_gate.py` (adds P2-12 aggregation contract test to unified manifest) + - `README.md` (adds Linux shard aggregation gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-12 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` (`T1` summary discovery dedupe, `T2` summary field validation, `T3` successful shard aggregation, `T4` missing-shard and total drift failure contract) + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds empty-shard summary payload contract used by aggregation stage) +- Not executed locally per release policy: + - P2-12 aggregation gate contract checks and Linux multi-shard recovery validation will run in Linux unified validation stage. + +## 41. Execution Log (2026-04-30, P2-13) + +- Card: `P2-13 Linux Final Report Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_report_publish_gate.py` (loads merged shard summary, validates publish contract, emits final_report.json + final_report.md, enforces computed-vs-reported status consistency) + - `scripts/run_linux_unified_gate.py` (adds P2-13 publish gate contract test to unified manifest) + - `README.md` (adds P2-13 final report publish gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-13 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_report_publish_gate_runtime.py` (`T1` merged summary required fields/types, `T2` happy-path payload contract, `T3` status mismatch failure contract, `T4` markdown report section contract) +- Not executed locally per release policy: + - P2-13 publish gate contract checks and Linux final-report recovery validation will run in Linux unified validation stage. + +## 42. Execution Log (2026-04-30, P2-14) + +- Card: `P2-14 Linux Unified Pipeline Orchestration Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_pipeline_gate.py` (orchestrates execution->aggregation->publish chain, supports stage skip controls, dry-run/print-commands, fail-fast, and pytest args forwarding to execution stage) + - `scripts/run_linux_unified_gate.py` (adds P2-14 pipeline contract test to unified manifest) + - `README.md` (adds P2-14 one-command Linux pipeline examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-14 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` (`T1` default 3-stage command plan with execution summary feed, `T2` skip-execution external summary passthrough, `T3` all-stage-skip empty plan contract) +- Not executed locally per release policy: + - P2-14 pipeline gate contract checks and Linux orchestrated recovery validation will run in Linux unified validation stage. + +## 43. Execution Log (2026-04-30, P2-15) + +- Card: `P2-15 Linux Shard Plan Generation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_plan_gate.py` (builds deterministic shard execution plan from unified manifest; emits per-shard report/summary/command payload; supports dry-run/print-commands/pytest args forwarding) + - `scripts/run_linux_unified_gate.py` (adds P2-15 shard plan contract test to unified manifest) + - `README.md` (adds P2-15 Linux shard plan command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-15 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_plan_gate_runtime.py` (`T1` deterministic shard slicing contract, `T2` command+args forwarding contract, `T3` invalid shard_total rejection contract) +- Not executed locally per release policy: + - P2-15 shard plan gate contract checks and Linux fan-out orchestration validation will run in Linux unified validation stage. + +## 44. Execution Log (2026-04-30, P2-16) + +- Card: `P2-16 Linux CI Matrix Export Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_matrix_gate.py` (loads P2-15 shard plan, validates shard/total contract, emits CI-consumable matrix payload and summary-path collection, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-16 matrix gate contract test to unified manifest) + - `README.md` (adds P2-16 CI matrix gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-16 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` (`T1` matrix include order/field contract, `T2` empty-shard filter contract, `T3` GitHub output value contract, `T4` totals mismatch rejection contract) +- Not executed locally per release policy: + - P2-16 CI matrix export contract checks and Linux fan-out/fan-in orchestration validation will run in Linux unified validation stage. + +## 45. Execution Log (2026-04-30, P2-17) + +- Card: `P2-17 Linux CI Workflow Plan Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_plan_gate.py` (loads P2-16 ci_matrix, validates matrix/summary_paths contracts, emits CI-consumable workflow plan with fan-out matrix and fan-in aggregation/publish commands, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-17 workflow plan gate contract test to unified manifest) + - `README.md` (adds P2-17 workflow plan gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-17 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` (`T1` fan-out/fan-in payload contract, `T2` GitHub output contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-17 CI workflow plan contract checks and Linux fan-out/fan-in/final-publish orchestration validation will run in Linux unified validation stage. + +## 46. Execution Log (2026-04-30, P2-18) + +- Card: `P2-18 Linux CI Workflow YAML Render Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` (loads P2-17 ci_workflow_plan, validates fan-out/fan-in contract, renders GitHub Actions workflow YAML, emits render metadata for audit traceability) + - `scripts/run_linux_unified_gate.py` (adds P2-18 workflow YAML render contract test to unified manifest) + - `README.md` (adds P2-18 workflow YAML gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-18 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` (`T1` workflow YAML fan-out/fan-in render contract, `T2` render metadata contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-18 workflow YAML render contract checks and Linux CI-end orchestration validation will run in Linux unified validation stage. + +## 47. Execution Log (2026-04-30, P2-19) + +- Card: `P2-19 Linux CI Workflow Drift Sync Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_sync_gate.py` (rebuilds expected workflow/metadata from P2-17 plan, validates drift against rendered artifacts, supports diff output and optional in-place sync write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-19 workflow sync contract test to unified manifest) + - `README.md` (adds P2-19 drift check/sync command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-19 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` (`T1` missing artifact drift contract, `T2` workflow content drift+diff contract, `T3` generated_at relaxed-mode contract, `T4` strict generated_at mismatch rejection) +- Not executed locally per release policy: + - P2-19 workflow sync contract checks and Linux CI drift-governance validation will run in Linux unified validation stage. + +## 48. Execution Log (2026-04-30, P2-20) + +- Card: `P2-20 Linux CI Workflow Command Guard Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` (validates command string/parts integrity, script target + required flag contract, fan-out/fan-in path binding, optional canonical write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-20 command guard contract test to unified manifest) + - `README.md` (adds P2-20 command guard gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-20 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` (`T1` valid payload pass contract, `T2` command string/parts mismatch rejection + normalization contract, `T3` aggregation summary order mismatch rejection, `T4` publish path mismatch rejection) +- Not executed locally per release policy: + - P2-20 command-integrity contract checks and Linux CI command-governance validation will run in Linux unified validation stage. + +## 49. Execution Log (2026-04-30, P2-21) + +- Card: `P2-21 Linux CI Workflow Governance Convergence Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_gate.py` (aggregates P2-19 drift sync + P2-20 command guard + metadata lineage checks into one governance verdict and categorized failed_checks) + - `scripts/run_linux_unified_gate.py` (adds P2-21 governance gate contract test to unified manifest) + - `README.md` (adds P2-21 governance gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-21 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` (`T1` full aligned pass contract, `T2` workflow drift failure category, `T3` command guard failure category, `T4` metadata lineage mismatch category) +- Not executed locally per release policy: + - P2-21 governance convergence contract checks and Linux CI final-governance validation will run in Linux unified validation stage. + +## 50. Execution Log (2026-04-30, P2-22) + +- Card: `P2-22 Linux CI Workflow Governance Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` (publishes P2-21 governance report into CI decision payload, markdown report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-22 governance publish contract test to unified manifest) + - `README.md` (adds P2-22 governance publish gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-22 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` (`T1` aligned governance pass contract, `T2` failed-checks deny-execution contract, `T3` failed_checks mismatch structural-failure contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-22 governance publish contract checks and Linux CI publish-stage validation will run in Linux unified validation stage. + +## 51. Execution Log (2026-04-30, P2-23) + +- Card: `P2-23 Linux CI Workflow Execution Decision Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_decision_gate.py` (converts P2-22 publish payload into execute/blocked decision contract with on-block exit policy, artifact-presence guard, markdown/json report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-23 execution decision gate contract test to unified manifest) + - `README.md` (adds P2-23 decision-gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-23 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` (`T1` aligned pass -> execute contract, `T2` failed-governance -> blocked contract, `T3` blocked+skip policy exit-code contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-23 execution-decision contract checks and Linux CI handoff validation will run in Linux unified validation stage. + +## 52. Execution Log (2026-04-30, P2-24) + +- Card: `P2-24 Linux CI Workflow Dispatch Readiness Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` (converts P2-23 execution decision payload into dispatch-ready contract, canonical gh workflow dispatch command, markdown/json reports, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-24 dispatch gate contract test to unified manifest) + - `README.md` (adds P2-24 dispatch gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-24 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` (`T1` execute -> ready dispatch contract, `T2` blocked -> no-dispatch contract, `T3` decision contract mismatch rejection, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-24 dispatch-readiness contract checks and Linux CI dispatch handoff validation will run in Linux unified validation stage. + +## 53. Execution Log (2026-04-30, P2-25) + +- Card: `P2-25 Linux CI Workflow Dispatch Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` (validates P2-24 dispatch contract, executes ready dispatch command with timeout handling, and emits execution JSON/markdown/github-output contract) + - `scripts/run_linux_unified_gate.py` (adds P2-25 dispatch execution gate contract test to unified manifest) + - `README.md` (adds P2-25 dispatch execution gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-25 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` (`T1` ready dry-run contract, `T2` blocked pass-through contract, `T3` command contract mismatch rejection, `T4` dispatch failure reason code contract, `T5` github-output field contract) +- Not executed locally per release policy: + - P2-25 dispatch execution contract checks and Linux CI dispatch execution validation will run in Linux unified validation stage. + +## 54. Execution Log (2026-04-30, P2-26) + +- Card: `P2-26 Linux CI Workflow Full Pipeline Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (initially composed P2-17 -> P2-31 into single pipeline entry with stage skips, strategy passthrough, dry-run/print, fail-fast policy, terminal publish, release handoff, and release trigger stage) + - `scripts/run_linux_unified_gate.py` (adds P2-26 pipeline gate contract test to unified manifest) + - `README.md` (updates P2-26 one-command workflow pipeline usage examples to include terminal publish coverage) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-26 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (`T1` default stage-chain contract through release trigger, `T2` strategy passthrough contract, `T3` partial skip-stage chain contract, `T4` all-skip empty-plan contract, `T5` terminal-publish-only stage contract) +- Not executed locally per release policy: + - P2-26 pipeline orchestration contract checks (including release trigger stage) and Linux CI end-to-end workflow gate validation will run in Linux unified validation stage. + +## 55. Execution Log (2026-04-30, P2-27) + +- Card: `P2-27 Linux CI Workflow Dispatch Traceability Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` (builds dispatch trace contract from P2-25 report, extracts run reference, emits poll command, supports optional --poll-now run status query and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain with P2-27 dispatch trace stage and new trace control flags) + - `scripts/run_linux_unified_gate.py` (adds P2-27 dispatch trace contract test to unified manifest) + - `README.md` (adds P2-27 gate usage and updated pipeline examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-27 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` (`T1` dispatched run reference extraction + poll command contract, `T2` missing run reference failure contract, `T3` blocked pass-through no-poll contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-27 stage presence and trace flag passthrough contract coverage) +- Not executed locally per release policy: + - P2-27 dispatch trace contract checks and Linux CI run-tracking validation will run in Linux unified validation stage. + +## 56. Execution Log (2026-04-30, P2-28) + +- Card: `P2-28 Linux CI Workflow Dispatch Completion Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` (converges P2-27 trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain with P2-28 completion stage and new completion control flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-28 completion contract test to unified manifest) + - `README.md` (adds P2-28 gate usage and updates P2-26 full-chain examples to P2-28) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-28 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` trace poll command mismatch rejection, `T4` github-output contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-28 stage presence and completion flag passthrough contract coverage) +- Not executed locally per release policy: + - P2-28 dispatch completion contract checks and Linux CI terminal convergence validation will run in Linux unified validation stage. + +## 57. Execution Log (2026-04-30, P2-29) + +- Card: `P2-29 Linux CI Workflow Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` (converts P2-28 completion artifact into terminal publish verdict with promote/hold decision, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_linux_unified_gate.py` (adds P2-29 terminal publish contract test to unified manifest) + - `README.md` (adds P2-29 terminal publish gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-29 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` (`T1` success->publish/pass+promote contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-29 terminal publish contract checks and Linux CI terminal-release handoff validation will run in Linux unified validation stage. + +## 58. Execution Log (2026-04-30, P2-30) + +- Card: `P2-30 Linux CI Workflow Release Handoff Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` (converts P2-29 terminal publish artifact into release handoff contract with promote/hold/reject action, target environment + channel semantics, contract mismatch guards, markdown/json/github-output outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain to compose P2-17 -> P2-30 and adds release handoff output flags + skip switch + environment/channel passthrough) + - `scripts/run_linux_unified_gate.py` (adds P2-30 release handoff contract test to unified manifest) + - `README.md` (adds P2-30 release handoff gate invocation examples and updates P2-26 full-chain examples to P2-30) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-30 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` (`T1` passed->ready_for_release contract, `T2` in_progress->awaiting_completion contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-30 stage presence, environment/channel passthrough, and release-handoff-only stage contract coverage) +- Not executed locally per release policy: + - P2-30 release handoff contract checks and Linux CI release-gate handoff validation will run in Linux unified validation stage. + +## 59. Execution Log (2026-04-30, P2-31) + +- Card: `P2-31 Linux CI Workflow Release Trigger Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` (loads P2-30 handoff contract, validates trigger readiness, executes/dry-runs release command, emits trigger JSON/markdown/github-output report) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose P2-17 -> P2-31 and adds release-trigger flags/outputs/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-31 release trigger contract test to unified manifest) + - `README.md` (adds P2-31 gate usage and updates P2-26 full-chain examples to P2-31) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-31 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` (`T1` ready_for_release dry-run contract, `T2` awaiting_completion pass-through contract, `T3` handoff contract mismatch rejection, `T4` trigger command failure convergence, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-31 stage presence, release-trigger flag passthrough, and release-trigger-only stage contract coverage) +- Not executed locally per release policy: + - P2-31 release trigger contract checks and Linux CI release dispatch execution validation will run in Linux unified validation stage. + +## 60. Execution Log (2026-04-30, P2-32) + +- Card: `P2-32 Linux CI Workflow Release Traceability Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` (loads P2-31 release trigger artifact, validates trigger contract, extracts release run reference, emits optional poll command, supports optional --poll-now status query, and outputs JSON/markdown/github-output report) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-32`, adds release-trace outputs/flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-32 release trace contract test to unified manifest) + - `README.md` (adds P2-32 gate usage and updates P2-26 full-chain examples to P2-32) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-32 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` (`T1` triggered->run reference extraction contract, `T2` missing run reference failure contract, `T3` trigger_failed passthrough contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-32 stage presence, release-trace flag passthrough, and release-trace-only stage contract coverage) +- Not executed locally per release policy: + - P2-32 release trace contract checks and Linux CI release run-tracking validation will run in Linux unified validation stage. + +## 61. Execution Log (2026-04-30, P2-33) + +- Card: `P2-33 Linux CI Workflow Release Completion Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` (converges P2-32 release trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-33`, adds release-completion outputs/flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-33 release completion contract test to unified manifest) + - `README.md` (adds P2-33 gate usage and updates P2-26 full-chain examples to P2-33) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-33 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` release poll command mismatch rejection, `T4` github-output contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-33 stage presence, release-completion flag passthrough, and release-completion-only stage contract coverage) +- Not executed locally per release policy: + - P2-33 release completion contract checks and Linux CI release terminal convergence validation will run in Linux unified validation stage. + +## 62. Execution Log (2026-04-30, P2-34) + +- Card: `P2-34 Linux CI Workflow Release Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` (converts P2-33 release completion artifact into release terminal publish verdict with finalize/hold decision, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-34`, adds release-terminal-publish outputs and `--skip-release-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-34 release terminal publish contract test to unified manifest) + - `README.md` (adds P2-34 gate usage and updates P2-26 full-chain examples to P2-34) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-34 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` (`T1` success->passed+finalize contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-34 stage presence, skip flag coverage, and release-terminal-publish-only stage contract) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-34 runtime contract checks and Linux CI release terminal publish validation will run in Linux unified validation stage. + +## 63. Execution Log (2026-04-30, P2-35) + +- Card: `P2-35 Linux CI Workflow Release Finalization Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` (converts P2-34 release terminal publish artifact into final release closure contract with `finalize/hold/abort` decision, hold policy support, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-35`, adds release-finalization outputs and `--skip-release-finalization` switch, plus `--on-release-hold` policy passthrough) + - `scripts/run_linux_unified_gate.py` (adds P2-35 release finalization contract test to unified manifest) + - `README.md` (adds P2-35 gate usage and updates P2-26 full-chain examples to P2-35) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-35 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` (`T1` passed->finalized contract, `T2` hold-policy-fail contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-35 stage presence, hold-policy passthrough, and release-finalization-only stage contract) +- Not executed locally per release policy: + - P2-35 runtime contract checks and Linux CI release finalization validation will run in Linux unified validation stage. + +## 64. Execution Log (2026-04-30, P2-36) + +- Card: `P2-36 Linux CI Workflow Release Closure Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` (converts P2-35 release finalization artifact into terminal release closure contract with `closed/pending/failed/contract_failed` status, `ship/hold/rollback` decision, consistency guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-36`, adds release-closure outputs and `--skip-release-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-36 release closure contract test to unified manifest) + - `README.md` (adds P2-36 gate usage and updates P2-26 full-chain examples to P2-36) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-36 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` (`T1` finalized->closed contract, `T2` awaiting_release->pending contract, `T3` finalization mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-36 stage presence, skip flag coverage, and release-closure-only stage contract) +- Not executed locally per release policy: + - P2-36 runtime contract checks and Linux CI release closure publish validation will run in Linux unified validation stage. + +## 65. Execution Log (2026-04-30, P2-37) + +- Card: `P2-37 Linux CI Workflow Release Archive Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` (converts P2-36 release closure artifact into terminal release archive contract with `ready/pending/failed/contract_failed` status, `publish/hold/block` decision, evidence-manifest existence checks, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-37`, adds release-archive outputs and `--skip-release-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-37 release archive contract test to unified manifest) + - `README.md` (adds P2-37 gate usage and updates P2-26 full-chain examples to P2-37) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-37 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` (`T1` closed+evidence->ready contract, `T2` pending->hold contract, `T3` closure mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-37 stage presence and release-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-37 runtime contract checks and Linux CI release evidence archive validation will run in Linux unified validation stage. + +## 66. Execution Log (2026-04-30, P2-38) + +- Card: `P2-38 Linux CI Workflow Release Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` (converts P2-37 release archive artifact into terminal release verdict contract with `published/awaiting_archive/blocked/contract_failed` status, `ship/hold/block` decision, evidence consistency guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-38`, adds release-verdict outputs and `--skip-release-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-38 release verdict contract test to unified manifest) + - `README.md` (adds P2-38 gate usage and updates P2-26 full-chain examples to P2-38) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-38 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` (`T1` ready->published contract, `T2` pending->awaiting_archive contract, `T3` archive mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-38 stage presence, skip flag coverage, and release-verdict-only stage contract) +- Not executed locally per release policy: + - P2-38 runtime contract checks and Linux CI final release verdict convergence validation will run in Linux unified validation stage. + +## 67. Execution Log (2026-04-30, Gate Contract Coverage Closure) + +- Card: `Phase 1 Gate Runtime Contract Closure (P1-01 ~ P1-06)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_linux_unified_gate.py` (extends Linux unified manifest with P1 gate runtime contract tests) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds closure execution log) +- Test scripts landed: + - `tests/test_p1_control_plane_gate_runtime.py` (P1-01 manifest file existence + pytest command contract) + - `tests/test_p1_event_journal_gate_runtime.py` (P1-02 manifest file existence + pytest command contract) + - `tests/test_p1_sqlite_state_backend_gate_runtime.py` (P1-03 gate command chain + file existence contract) + - `tests/test_p1_active_memory_gate_runtime.py` (P1-04 gate command chain + file existence contract) + - `tests/test_p1_hook_permission_audit_gate_runtime.py` (P1-05 gate command chain + file existence contract) + - `tests/test_p1_cli_thin_client_gate_runtime.py` (P1-06 gate command chain + file existence contract) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root tests --json` passed. + - `py -3 scripts/check_syntax.py --root scripts --json` passed. +- Not executed locally per release policy: + - All new P1 gate runtime contract tests will be executed in Linux unified validation stage. + +## 68. Execution Log (2026-05-01, P2-39) + +- Card: `P2-39 Linux CI Workflow Release Incident Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` (converts P2-38 release verdict artifact into terminal incident dispatch contract with `not_required/ready_dry_run/dispatched/dispatch_failed/contract_failed` status, verdict consistency guard, incident command execution fallback, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-39`, adds release-incident outputs, incident command flags, and `--skip-release-incident` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-39 release incident contract test to unified manifest) + - `README.md` (adds P2-39 gate usage and updates P2-26 full-chain examples to P2-39) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-39 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` (`T1` published->not_required contract, `T2` blocked->ready_dry_run contract, `T3` verdict mismatch->contract_failed contract, `T4` incident command failure convergence, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-39 stage presence, incident flag passthrough, and release-incident-only stage contract coverage) +- Not executed locally per release policy: + - P2-39 runtime contract checks and Linux CI release incident dispatch validation will run in Linux unified validation stage. + +## 69. Execution Log (2026-05-01, Gate Contract Coverage Closure II) + +- Card: `Gate Runtime Contract Closure II (Phase 0 + P2 gate manifest coverage)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `tests/test_phase0_gate_runtime.py` (adds contract coverage for `scripts/run_phase0_gate.py` manifest + pytest command builder) + - `scripts/run_linux_unified_gate.py` (extends unified manifest to include: + - `tests/test_phase0_gate_runtime.py` + - `tests/test_linux_unified_gate_runtime.py` + - `tests/test_p2_multi_agent_supervisor_gate_runtime.py` + - `tests/test_p2_artifact_bus_gate_runtime.py` + - `tests/test_p2_ide_integration_gate_runtime.py` + - `tests/test_p2_github_ci_workflow_gate_runtime.py` + - `tests/test_p2_org_policy_audit_gate_runtime.py` + - `tests/test_p2_agent_runtime_parity_gate_runtime.py` + - `tests/test_p2_custom_agents_loader_gate_runtime.py` + - `tests/test_p2_jetbrains_ide_integration_gate_runtime.py`) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. + - Gate contract scan passed: `NO_MISSING_GATE_TESTS`. + - Unified manifest scan passed: `NO_MISSING_UNIFIED_MANIFEST_TESTS`. +- Not executed locally per release policy: + - All newly added gate runtime contract tests remain pending for Linux unified validation stage. + +## 70. Execution Log (2026-05-01, P2-40) + +- Card: `P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` (converts P2-39 release incident artifact into terminal release verdict contract with `released/awaiting_archive/blocked_incident_ready_dry_run/blocked_incident_dispatched/blocked_incident_failed/blocked/contract_failed` status and `ship/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-40`, adds release-terminal-verdict outputs, stage wiring, and `--skip-release-terminal-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-40 release terminal verdict contract test to unified manifest) + - `README.md` (adds P2-40 gate usage and updates P2-26 full-chain examples to P2-40) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-40 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` (`T1` published->released contract, `T2` blocked+ready_dry_run->blocked_incident_ready_dry_run contract, `T3` verdict/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-40 stage presence, command path assertions, skip flag propagation, and release-terminal-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-40 runtime contract checks and Linux CI final terminal verdict convergence validation will run in Linux unified validation stage. + +## 71. Execution Log (2026-05-01, P2-41) + +- Card: `P2-41 Linux CI Workflow Release Delivery Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` (converts P2-40 release terminal verdict artifact into final delivery contract with `shipped/pending_follow_up/blocked_incident/blocked_incident_failed/blocked/contract_failed` status and `deliver/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-41`, adds release-delivery outputs, stage wiring, and `--skip-release-delivery` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-41 release delivery contract test to unified manifest) + - `README.md` (adds P2-41 gate usage and updates P2-26 full-chain examples to P2-41) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-41 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` (`T1` released->shipped contract, `T2` blocked_incident_ready_dry_run->blocked_incident contract, `T3` terminal/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-41 stage presence, command path assertions, skip flag propagation, and release-delivery-only stage contract coverage) +- Not executed locally per release policy: + - P2-41 runtime contract checks and Linux CI final delivery closure convergence validation will run in Linux unified validation stage. + +## 72. Execution Log (2026-05-01, P2-42) + +- Card: `P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` (converts P2-41 release delivery artifact into terminal publish contract with `published/pending_follow_up/blocked/contract_failed` status and `announce_release/announce_hold/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-42`, adds release-delivery-terminal-publish outputs, stage wiring, and `--skip-release-delivery-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-42 release delivery terminal publish contract test to unified manifest) + - `README.md` (adds P2-42 gate usage and updates P2-26 full-chain examples to P2-42) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-42 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` (`T1` shipped->published contract, `T2` blocked_incident->blocked contract, `T3` delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-42 stage presence, command path assertions, skip flag propagation, and release-delivery-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-42 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage. + +## 73. Execution Log (2026-05-01, P2-43) + +- Card: `P2-43 Linux CI Workflow Release Delivery Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` (converts P2-42 release delivery terminal publish artifact into final verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_release/open_follow_up/escalate_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-43`, adds release-delivery-final-verdict outputs, stage wiring, and `--skip-release-delivery-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-43 release delivery final verdict contract test to unified manifest) + - `README.md` (adds P2-43 gate usage and updates P2-26 full-chain examples to P2-43) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-43 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` (`T1` published->completed contract, `T2` pending_follow_up->requires_follow_up contract, `T3` terminal/delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-43 stage presence, command path assertions, skip flag propagation, and release-delivery-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-43 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage. + +## 74. Execution Log (2026-05-01, P2-44) + +- Card: `P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` (converts P2-43 release delivery final verdict artifact into follow-up dispatch contract with `closed/follow_up_required/escalated/contract_failed` status and `no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-44`, adds release-follow-up-dispatch outputs, stage wiring, and `--skip-release-follow-up-dispatch` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-44 release follow-up dispatch contract test to unified manifest) + - `README.md` (adds P2-44 gate usage and updates P2-26 full-chain examples to P2-44) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-44 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` (`T1` completed->closed contract, `T2` requires_follow_up->follow_up_required contract, `T3` final-verdict mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-44 stage presence, command path assertions, skip flag propagation, and release-follow-up-dispatch-only stage contract coverage) +- Not executed locally per release policy: + - P2-44 runtime contract checks and Linux CI follow-up dispatch convergence validation will run in Linux unified validation stage. + +## 75. Execution Log (2026-05-01, P2-45) + +- Card: `P2-45 Linux CI Workflow Release Follow-Up Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` (converts P2-44 release follow-up dispatch artifact into follow-up closure contract with `closed/queued_dry_run/queued/queue_failed/contract_failed` status and `no_action/queue_follow_up/queue_escalation/abort_queue` decision, plus consistency guards, optional follow-up queue command execution, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-45`, adds release-follow-up-closure outputs, stage wiring, follow-up command flags, and `--skip-release-follow-up-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-45 release follow-up closure contract test to unified manifest) + - `README.md` (adds P2-45 gate usage and updates P2-26 full-chain examples to P2-45) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-45 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` (`T1` closed->no_action contract, `T2` follow_up_required->queued_dry_run contract, `T3` dispatch mismatch->contract_failed contract, `T4` follow-up queue command failure->queue_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-45 stage presence, command path assertions, follow-up flag propagation, and release-follow-up-closure-only stage contract coverage) +- Not executed locally per release policy: + - P2-45 runtime contract checks and Linux CI follow-up closure convergence validation will run in Linux unified validation stage. + +## 76. Execution Log (2026-05-01, P2-46) + +- Card: `P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` (converts P2-45 release follow-up closure artifact into terminal publish contract with `published/pending_queue/queue_failed/contract_failed` status and `announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-46`, adds release-follow-up-terminal-publish outputs, stage wiring, and `--skip-release-follow-up-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-46 release follow-up terminal publish contract test to unified manifest) + - `README.md` (adds P2-46 gate usage and updates P2-26 full-chain examples to P2-46) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-46 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` (`T1` closed->published contract, `T2` queued_dry_run->pending_queue contract, `T3` closure mismatch->contract_failed contract, `T4` queue_failed->queue_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-46 stage presence, command path assertions, skip flag propagation, and release-follow-up-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-46 runtime contract checks and Linux CI follow-up terminal publish convergence validation will run in Linux unified validation stage. + +## 77. Execution Log (2026-05-01, P2-47) + +- Card: `P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` (converts P2-46 release follow-up terminal publish artifact into final follow-up verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-47`, adds release-follow-up-final-verdict outputs, stage wiring, and `--skip-release-follow-up-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-47 release follow-up final verdict contract test to unified manifest) + - `README.md` (adds P2-47 gate usage and updates P2-26 full-chain examples to P2-47) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-47 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` (`T1` published+announce_closed->completed contract, `T2` pending_queue->requires_follow_up contract, `T3` queue_failed->blocked contract, `T4` contract mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-47 stage presence, command path assertions, skip flag propagation, and release-follow-up-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-47 runtime contract checks and Linux CI follow-up final verdict convergence validation will run in Linux unified validation stage. + +## 78. Execution Log (2026-05-01, P2-48) + +- Card: `P2-48 Linux CI Workflow Release Final Outcome Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` (converges P2-43 + P2-47 final verdict artifacts into one terminal release outcome contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-48`, adds release-final-outcome outputs, stage wiring, and `--skip-release-final-outcome` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-48 release final outcome contract test to unified manifest) + - `README.md` (adds P2-48 gate usage and updates P2-26 full-chain examples to P2-48) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-48 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` (`T1` released contract, `T2` released_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-48 stage presence, command path assertions, skip flag propagation, and release-final-outcome-only stage contract coverage) +- Not executed locally per release policy: + - P2-48 runtime contract checks and Linux CI final release outcome convergence validation will run in Linux unified validation stage. + +## 79. Execution Log (2026-05-01, P2-49) + +- Card: `P2-49 Linux CI Workflow Release Final Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` (converges P2-48 final outcome artifact into terminal publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-49`, adds release-final-terminal-publish outputs, stage wiring, and `--skip-release-final-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-49 release final terminal publish contract test to unified manifest) + - `README.md` (adds P2-49 gate usage and updates P2-26 full-chain examples to P2-49) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-49 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-49 stage presence, command path assertions, skip flag propagation, and release-final-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-49 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage. + +## 80. Execution Log (2026-05-01, P2-50) + +- Card: `P2-50 Linux CI Workflow Release Final Handoff Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` (converges P2-49 final terminal publish artifact into release handoff contract with `completed/completed_with_follow_up/blocked/contract_failed` status and `handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-50`, adds release-final-handoff outputs, stage wiring, and `--skip-release-final-handoff` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-50 release final handoff contract test to unified manifest) + - `README.md` (adds P2-50 gate usage and updates P2-26 full-chain examples to P2-50) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-50 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` (`T1` published->completed contract, `T2` published_with_follow_up->completed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-50 stage presence, command path assertions, skip flag propagation, and release-final-handoff-only stage contract coverage) +- Not executed locally per release policy: + - P2-50 runtime contract checks and Linux CI final handoff convergence validation will run in Linux unified validation stage. + +## 81. Execution Log (2026-05-01, P2-51) + +- Card: `P2-51 Linux CI Workflow Release Final Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` (converges P2-50 final handoff artifact into release closure contract with `closed/closed_with_follow_up/blocked/contract_failed` status and `close_release/close_with_follow_up/close_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-51`, adds release-final-closure outputs, stage wiring, and `--skip-release-final-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-51 release final closure contract test to unified manifest) + - `README.md` (adds P2-51 gate usage and updates P2-26 full-chain examples to P2-51) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-51 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` (`T1` completed->closed contract, `T2` completed_with_follow_up->closed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-51 stage presence, command path assertions, skip flag propagation, and release-final-closure-only stage contract coverage) +- Not executed locally per release policy: + - P2-51 runtime contract checks and Linux CI final closure convergence validation will run in Linux unified validation stage. + +## 82. Execution Log (2026-05-01, P2-52) + +- Card: `P2-52 Linux CI Workflow Release Final Closure Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` (converges P2-51 release final closure artifact into final closure publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-52`, adds release-final-closure-publish outputs, stage wiring, and `--skip-release-final-closure-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-52 release final closure publish contract test to unified manifest) + - `README.md` (adds P2-52 gate usage and updates P2-26 full-chain examples to P2-52) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-52 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` (`T1` closed->published contract, `T2` closed_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-52 stage presence, command path assertions, skip flag propagation, and release-final-closure-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-52 runtime contract checks and Linux CI final closure publish convergence validation will run in Linux unified validation stage. + +## 83. Execution Log (2026-05-01, P2-53) + +- Card: `P2-53 Linux CI Workflow Release Final Archive Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` (converges P2-52 release final closure publish artifact into final archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-53`, adds release-final-archive outputs, stage wiring, and `--skip-release-final-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-53 release final archive contract test to unified manifest) + - `README.md` (adds P2-53 gate usage and updates P2-26 full-chain examples to P2-53) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-53 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-53 stage presence, command path assertions, skip flag propagation, and release-final-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-53 runtime contract checks and Linux CI final archive convergence validation will run in Linux unified validation stage. + +## 84. Execution Log (2026-05-01, P2-54) + +- Card: `P2-54 Linux CI Workflow Release Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` (converges P2-53 final archive artifact into final verdict contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-54`, adds release-final-verdict outputs, stage wiring, and `--skip-release-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-54 release final verdict contract test to unified manifest) + - `README.md` (adds P2-54 gate usage and updates P2-26 full-chain examples to P2-54) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-54 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` (`T1` archived->released contract, `T2` archived_with_follow_up->released_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-54 stage presence, command path assertions, skip flag propagation, and release-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-54 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage. + +## 85. Execution Log (2026-05-01, P2-55) + +- Card: `P2-55 Linux CI Workflow Release Final Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` (converges P2-54 final verdict artifact into final verdict publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-55`, adds release-final-verdict-publish outputs, stage wiring, and `--skip-release-final-verdict-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-55 release final verdict publish contract test to unified manifest) + - `README.md` (adds P2-55 gate usage and updates P2-26 full-chain examples to P2-55) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-55 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-55 stage presence, command path assertions, skip flag propagation, and release-final-verdict-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-55 runtime contract checks and Linux CI final verdict publish convergence validation will run in Linux unified validation stage. + +## 86. Execution Log (2026-05-01, P2-56) + +- Card: `P2-56 Linux CI Workflow Release Final Publish Archive Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` (converges P2-55 final verdict publish artifact into final publish archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-56`, adds release-final-publish-archive outputs, stage wiring, and `--skip-release-final-publish-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-56 release final publish archive contract test to unified manifest) + - `README.md` (adds P2-56 gate usage and updates P2-26 full-chain examples to P2-56) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-56 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-56 stage presence, command path assertions, skip flag propagation, and release-final-publish-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-56 runtime contract checks and Linux CI final publish archive convergence validation will run in Linux unified validation stage. + +## 87. Execution Log (2026-05-01, P2-57) + +- Card: `P2-57 Linux Gate Manifest Drift Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_gate_manifest_drift_gate.py` (adds closure guard that scans `run_*_gate.py` -> `test_*_gate_runtime.py` mapping and validates Linux unified manifest coverage + orphan manifest entries, emits JSON/Markdown + GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-57 runtime contract test to Linux unified manifest) + - `README.md` (adds P2-57 standalone usage while keeping P2-26 full-chain wording as `P2-17 -> P2-56`) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-57 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` (`T1` gate scan include/exclude contract, `T2` runtime-test mapping contract, `T3` baseline drift report passed contract, `T4` github-output field contract) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-57 runtime contract checks and Linux gate drift closure convergence validation will run in Linux unified validation stage. + +## 88. Execution Log (2026-05-01, P2-58) + +- Card: `P2-58 Linux CI Workflow Pipeline Closure to P2-57` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain description to `P2-17 -> P2-57`, wires `workflow_gate_manifest_drift` stage to call `scripts/run_p2_linux_gate_manifest_drift_gate.py`, adds `--gate-manifest-drift-json-output` / `--gate-manifest-drift-markdown-output`, and adds `--skip-gate-manifest-drift`) + - `README.md` (updates P2-26 full-chain wording to `P2-17 -> P2-57` and adds P2-57-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-58 execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-57 stage presence in default/partial chain, command assertions for drift-stage output paths, skip-flag propagation, and P2-57-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-58 runtime contract checks and Linux CI workflow pipeline closure convergence validation will run in Linux unified validation stage. + +## 89. Execution Log (2026-05-01, P2-59) + +- Card: `P2-59 Linux CI Workflow Terminal Verdict Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` (converges P2-56 release final publish archive report and P2-57 gate manifest drift report into one terminal verdict contract with `ready_for_linux_validation/ready_with_follow_up_for_linux_validation/blocked/contract_failed` status and `proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-59`, adds terminal-verdict outputs, stage wiring, and `--skip-terminal-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-59 terminal verdict contract test to unified manifest) + - `README.md` (adds P2-59 standalone usage, updates P2-26 full-chain examples to `P2-17 -> P2-59`, and adds P2-59-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-59 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` (`T1` archived+drift-passed->ready contract, `T2` archived-with-follow-up+drift-passed->ready-with-follow-up contract, `T3` blocked+drift-passed->blocked contract, `T4` archive mismatch or drift-failed->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-59 stage presence in default/partial chain, command assertions for terminal-verdict report paths, compatibility propagation for `skip_terminal_verdict`, and P2-59-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-59 runtime contract checks and Linux CI terminal verdict closure convergence validation will run in Linux unified validation stage. + +## 90. Execution Log (2026-05-01, P2-60) + +- Card: `P2-60 Linux CI Workflow Linux Validation Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py` (converges P2-59 terminal verdict into Linux validation dispatch contract with `ready_dry_run/ready_with_follow_up_dry_run/dispatched/dispatch_failed/blocked/contract_failed` status and `dispatch_linux_validation/dispatch_linux_validation_with_follow_up/hold_linux_validation_blocker/abort_linux_validation_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-60`, adds Linux validation dispatch outputs, stage wiring, timeout flag, and `--skip-linux-validation-dispatch` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-60 Linux validation dispatch contract test to unified manifest) + - `README.md` (adds P2-60 standalone usage, updates full-chain examples to `P2-17 -> P2-60`, and adds P2-60-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-60 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_dispatch_gate_runtime.py` (`T1` ready->ready_dry_run contract, `T2` ready_with_follow_up->ready_with_follow_up_dry_run contract, `T3` blocked->blocked contract, `T4` mismatch/drift/evidence issue->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-60 stage presence in default/partial chain, command assertions for Linux validation dispatch paths/timeout, and P2-60-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 91. Execution Log (2026-05-01, P2-61) + +- Card: `P2-61 Linux CI Workflow Linux Validation Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py` (converges P2-60 Linux validation dispatch artifact into one final Linux validation verdict contract with `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` status and `accept_linux_validation/accept_linux_validation_with_follow_up/escalate_linux_validation_failure/hold_linux_validation_blocker/abort_linux_validation_verdict` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-61`, adds Linux validation verdict outputs, stage wiring, and `--skip-linux-validation-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-61 Linux validation verdict contract test to unified manifest) + - `README.md` (adds P2-61 standalone usage, updates full-chain examples to `P2-17 -> P2-61`, and adds P2-61-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-61 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_gate_runtime.py` (`T1` ready_dry_run->validated contract, `T2` ready_with_follow_up_dry_run->validated_with_follow_up contract, `T3` dispatch_failed->validation_failed contract, `T4` dispatch/evidence mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-61 stage presence in default/partial chain, command assertions for Linux validation verdict paths, skip propagation for `skip_linux_validation_verdict`, and P2-61-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 92. Execution Log (2026-05-02, P2-62) + +- Card: `P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py` (converges P2-61 Linux validation verdict artifact into one publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-62`, adds Linux validation verdict publish outputs, stage wiring, and `--skip-linux-validation-verdict-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-62 Linux validation verdict publish contract test to unified manifest) + - `README.md` (adds P2-62 standalone usage, updates full-chain examples to `P2-17 -> P2-62`, and adds P2-62-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-62 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_publish_gate_runtime.py` (`T1` validated->published contract, `T2` validated_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-62 stage presence in default/partial chain, command assertions for Linux validation verdict publish paths, skip propagation for `skip_linux_validation_verdict_publish`, and P2-62-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 93. Execution Log (2026-05-02, P2-63) + +- Card: `P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_terminal_publish_gate.py` (converges P2-62 Linux validation verdict publish artifact into one terminal publish contract with `terminal_published/terminal_published_with_follow_up/terminal_blocked/terminal_contract_failed` status and `announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-63`, adds Linux validation terminal publish outputs, stage wiring, and `--skip-linux-validation-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-63 Linux validation terminal publish contract test to unified manifest) + - `README.md` (adds P2-63 standalone usage, updates full-chain examples to `P2-17 -> P2-63`, and adds P2-63-only pipeline command) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_terminal_publish_gate_runtime.py` (`T1` published->terminal_published contract, `T2` published_with_follow_up->terminal_published_with_follow_up contract, `T3` blocked->terminal_blocked contract, `T4` publish contract mismatch/evidence issue->terminal_contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-63 stage presence in default/partial chain, command assertions for Linux validation terminal publish paths, skip propagation for `skip_linux_validation_terminal_publish`, and P2-63-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. diff --git a/.claude/blob_f42ff8202c41966786c80f08d59c37b9660d6bc6.txt b/.claude/blob_f42ff8202c41966786c80f08d59c37b9660d6bc6.txt new file mode 100644 index 0000000..f42ff82 --- /dev/null +++ b/.claude/blob_f42ff8202c41966786c80f08d59c37b9660d6bc6.txt @@ -0,0 +1,2974 @@ +# Claude Code Python 鎵ц璁″垝涓庝换鍔″崱鍗曪紙2026-04-29锛? +## 1. 鏂囨。瀹氫綅 + +- 鏂囨。鎬ц川锛氭墽琛岃鍒?+ 浠诲姟鍗″崟 +- 涓婃父鏂囨。锛歔CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md](D:/Download/gaming/new_program/claude-code-python/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md) +- 浣跨敤鏂瑰紡锛? - 浜у搧銆佹灦鏋勩€佺爺鍙戙€佹祴璇曢兘浠ユ湰鏂囦欢浣滀负杩戞湡瀹炴柦娓呭崟 + - 姣忓紶浠诲姟鍗″繀椤昏兘鐙珛寤哄垎鏀€佺嫭绔嬪紑鍙戙€佺嫭绔嬮獙璇併€佺嫭绔嬪悎骞? - 鑻ヤ竴寮犲崱鏃犳硶鍦ㄤ竴涓槑纭竟鐣屽唴瀹屾垚锛屽垯缁х画鍚戜笅鎷? +## 2. 褰撳墠鎷嗚В鍘熷垯 + +### 2.1 鎷嗗崱鍘熷垯 + +姣忓紶鍗″繀椤诲悓鏃跺洖绛?5 涓棶棰橈細 + +1. 瑙e喅浠€涔堥棶棰橈紵 +2. 瑕佸仛浠€涔堢爺鍙戝姛鑳斤紵 +3. 瑕佸仛鍒颁粈涔堢▼搴︽墠绠楀畬鎴愶紵 +4. 娴嬭瘯浠g爜瑕佽ˉ鍒颁粈涔堢▼搴︼紵 +5. 鏈変粈涔堝墠缃緷璧栧拰鏄庣‘闈炵洰鏍囷紵 + +### 2.2 瀹屾垚瀹氫箟锛圖efinition of Done锛? +姣忓紶鍗¢粯璁ゅ畬鎴愭爣鍑嗭細 + +- 浠g爜涓婚摼鍙繍琛?- 鏂板鎴栦慨鏀规祴璇曢€氳繃 +- 瀵瑰簲鏂囨。鍚屾鏇存柊 +- 涓嶅紩鍏ユ柊鐨勬湭瑙i噴鑴忕姸鎬?- 鑷冲皯瀹屾垚涓€杞湰鍦版墜宸ラ獙鏀? +### 2.3 娴嬭瘯鍒嗗眰鏍囧噯 + +涓洪伩鍏嶁€滃啓浜嗙偣娴嬭瘯鈥濅絾娌℃湁楠屾敹浠峰€硷紝缁熶竴閲囩敤浠ヤ笅娣卞害鍒嗗眰锛? +| 灞傜骇 | 鍚嶇О | 鐩殑 | 鏈€浣庤姹?| +|---|---|---|---| +| T1 | Unit | 绾嚱鏁般€佺姸鎬佹満銆佽緭鍏ヨ緭鍑鸿竟鐣?| 姝e悜 + 璐熷悜 + 杈圭晫 | +| T2 | Component | 鍗曟ā鍧楃紪鎺掋€佸甫 fake/mocks 鐨勮涓洪獙璇?| 鑷冲皯瑕嗙洊 1 鏉′富閾?+ 1 鏉″紓甯搁摼 | +| T3 | Integration | 璺ㄦā鍧楄仈鍔紝楠岃瘉鐪熷疄渚濊禆杈圭晫 | 鑷冲皯瑕嗙洊 1 鏉$鍒扮涓氬姟閾?| +| T4 | Regression | 澶嶇幇鍘嗗彶缂洪櫡锛岄槻姝㈠洖褰?| 姣忎釜宸茬煡 bug 鑷冲皯 1 鏉″洖褰掔敤渚?| +| T5 | Manual Smoke | 浜哄伐蹇€熼獙鏀?| 鍛戒护绾?鍏ュ彛绾у疄闄呰繍琛?| + +### 2.4 娴嬭瘯浜х墿瑕佹眰 + +姣忓紶浠g爜鍗¤嚦灏戞弧瓒筹細 + +- 鏂板鎴栦慨鏀?`T1/T2` 娴嬭瘯 +- 鑻ュ彉鏇村奖鍝?CLI銆乻ession銆乼ask銆丮CP銆乭ooks銆乤gent 涓婚摼锛屽垯蹇呴』琛?`T3` +- 鑻ヤ换鍔″崱婧愪簬宸茬煡缂洪櫡锛屽垯蹇呴』琛?`T4` +- 杈撳嚭 1 浠芥渶灏忔墜宸ラ獙鏀跺懡浠ゆ竻鍗? +## 3. 瀹炴柦闃舵鎬昏 + +## 3.1 Phase 0锛氭琛€涓庢敹鏁? +鐩爣锛? +- 淇帀鈥滅湅璧锋潵鏈夈€佸疄闄呮病鎵撻€氣€濈殑鍋囬棴鐜?- 寤虹珛鍞竴涓昏繍琛屾椂鍏ュ彛 +- 寤虹珛绋冲畾鐨勬湰鍦板紑鍙?娴嬭瘯鏂藉伐闈? +## 3.2 Phase 1锛氬钩鍙板寲 + +鐩爣锛? +- 浠庡崟杩涚▼ CLI 婕旇繘涓哄彲澶嶇敤鐨勬墽琛屽悗绔?- 琛ラ綈 event journal銆乻tate backend銆乤ctive memory + +## 3.3 Phase 2锛氳秴杞? +鐩爣锛? +- 寤虹珛澶?Agent 鎺у埗闈€両DE 瀹㈡埛绔€丆I 宸ヤ綔娴併€佺粍缁囩骇娌荤悊鑳藉姏 + +## 4. 渚濊禆椤哄簭鍥? +```text +P0-01 Runtime Backbone + -> P0-02 Session Resume + -> P0-03 Auth/Config + -> P0-04 Feature Flags + -> P0-05 Review Command + -> P0-06 Session/History/Memory Boundaries + -> P0-07 Hooks Runtime + -> P0-08 Windows Bootstrap + -> P0-09 Temp Artifact Hygiene + -> P0-10 Phase0 Regression Gate + +P1-01 Daemon/API Control Plane + -> P1-02 Event Journal + -> P1-03 SQLite State Backend + -> P1-04 Active Memory Runtime + -> P1-05 Hook Policy/Audit Convergence + -> P1-06 CLI Thin Client Migration + +P2-01 Agent Supervisor + -> P2-02 Artifact Bus + -> P2-03 IDE Integration + -> P2-04 GitHub/CI Workflow + -> P2-05 Org Policy & Audit + -> P2-06 Linux Unified Verification Gate + -> P2-07 Agent Runtime Parity + -> P2-08 Custom Agents Directory Loader + -> P2-09 Linux Unified Execution Gate + -> P2-10 JetBrains IDE Integration + -> P2-11 Linux Sharded Execution Gate + -> P2-12 Linux Shard Aggregation Gate + -> P2-13 Linux Final Report Publish Gate + -> P2-14 Linux Unified Pipeline Orchestration Gate + -> P2-15 Linux Shard Plan Generation Gate + -> P2-16 Linux CI Matrix Export Gate + -> P2-17 Linux CI Workflow Plan Gate + -> P2-18 Linux CI Workflow YAML Render Gate + -> P2-19 Linux CI Workflow Drift Sync Gate + -> P2-20 Linux CI Workflow Command Guard Gate + -> P2-21 Linux CI Workflow Governance Convergence Gate + -> P2-22 Linux CI Workflow Governance Publish Gate + -> P2-23 Linux CI Workflow Execution Decision Gate + -> P2-24 Linux CI Workflow Dispatch Readiness Gate + -> P2-25 Linux CI Workflow Dispatch Execution Gate + -> P2-26 Linux CI Workflow Full Pipeline Gate + -> P2-27 Linux CI Workflow Dispatch Traceability Gate + -> P2-28 Linux CI Workflow Dispatch Completion Gate + -> P2-29 Linux CI Workflow Terminal Publish Gate + -> P2-30 Linux CI Workflow Release Handoff Gate + -> P2-31 Linux CI Workflow Release Trigger Gate + -> P2-32 Linux CI Workflow Release Traceability Gate + -> P2-33 Linux CI Workflow Release Completion Gate + -> P2-34 Linux CI Workflow Release Terminal Publish Gate + -> P2-35 Linux CI Workflow Release Finalization Gate + -> P2-36 Linux CI Workflow Release Closure Publish Gate + -> P2-37 Linux CI Workflow Release Archive Publish Gate + -> P2-38 Linux CI Workflow Release Verdict Publish Gate + -> P2-39 Linux CI Workflow Release Incident Dispatch Gate + -> P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate + -> P2-41 Linux CI Workflow Release Delivery Closure Gate + -> P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate + -> P2-43 Linux CI Workflow Release Delivery Final Verdict Gate + -> P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate + -> P2-45 Linux CI Workflow Release Follow-Up Closure Gate + -> P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate + -> P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate + -> P2-48 Linux CI Workflow Release Final Outcome Gate + -> P2-49 Linux CI Workflow Release Final Terminal Publish Gate + -> P2-50 Linux CI Workflow Release Final Handoff Gate + -> P2-51 Linux CI Workflow Release Final Closure Gate + -> P2-52 Linux CI Workflow Release Final Closure Publish Gate + -> P2-53 Linux CI Workflow Release Final Archive Gate + -> P2-54 Linux CI Workflow Release Final Verdict Gate + -> P2-55 Linux CI Workflow Release Final Verdict Publish Gate + -> P2-56 Linux CI Workflow Release Final Publish Archive Gate + -> P2-57 Linux Gate Manifest Drift Closure Gate + -> P2-59 Linux CI Workflow Terminal Verdict Closure Gate + -> P2-60 Linux CI Workflow Linux Validation Dispatch Gate + -> P2-61 Linux CI Workflow Linux Validation Verdict Gate + -> P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate + -> P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate +``` + +## 5. Phase 0 浠诲姟鍗″崟 + +## P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦 + +- 鍗″彿锛歚P0-01` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `main.py`銆乣app.py`銆乻ession/task/hooks/memory 鍒濆鍖栭摼鏉″垎鏁? - 瀛愮郴缁熷瓨鍦ㄤ絾鏈舰鎴愬敮涓€杩愯鏃堕棴鐜?- 鐮斿彂鍔熻兘锛? - 寤虹珛缁熶竴 runtime bootstrap + - 鏄庣‘ `main -> Application -> QueryEngine -> Session -> TaskManager -> Hooks -> Memory` + - 娑堥櫎閲嶅鍒濆鍖栬矾寰勪笌闅愬紡鍏ㄥ眬鐘舵€佷緷璧?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - `claude_code/app.py` + - `claude_code/engine/query.py` + - 瑙嗛渶瑕佹柊澧?`claude_code/runtime/` 鎴栫瓑浠锋ā鍧?- 瀹屾垚绋嬪害锛? - REPL銆乻ingle query銆乸ipe銆丮CP serve 鍏辩敤鍚屼竴 bootstrap 娴? - runtime 鍏抽敭瀵硅薄鏈夊敮涓€鏋勫缓璺緞 + - 鍚庣画鍗″崟涓嶅啀渚濊禆鈥滃悇鑷伔鍋峰垵濮嬪寲鈥?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歜ootstrap 鍙傛暟瑙f瀽銆乺untime object 鏋勫缓杈圭晫 + - `T2`锛氫笉鍚岃繍琛屾ā寮忎笅 runtime 缁勪欢瑁呴厤涓€鑷存€? - `T3`锛歊EPL / pipe / single query 鑷冲皯鍚勬湁 1 鏉′富閾?smoke + - `T4`锛氳鐩栤€滄煇妯″紡婕忓垵濮嬪寲鏌愭湇鍔♀€濈殑鍘嗗彶鏂偣 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_runtime_bootstrap.py` + - 鎵╁睍 `tests/test_main_runtime.py` +- 鎵嬪伐楠屾敹锛? - `python -m claude_code.main --help` + - `python -m claude_code.main --doctor` + - `echo hi | python -m claude_code.main --pipe` +- 闈炵洰鏍囷細 + - 鏈崱涓嶅紩鍏?daemon/API + - 鏈崱涓嶅鐞嗙粍缁囩骇绛栫暐 +- 鍓嶇疆渚濊禆锛氭棤 + +## P0-02 /resume 鐪熼棴鐜? +- 鍗″彿锛歚P0-02` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `QueryEngine.resume_session()` 宸插瓨鍦紝浣?`/resume` 鍛戒护娌℃湁鐪熸璋冪敤鎭㈠閾捐矾 + - session 鎭㈠鏄€滄枃妗堝姛鑳解€濓紝涓嶆槸鈥滆繍琛屾椂鍔熻兘鈥?- 鐮斿彂鍔熻兘锛? - 灏?`/resume [session_id]` 鐪熸鎺ュ叆 `QueryEngine.resume_session()` + - 鏃犲弬鏁版椂鍒楁渶杩?session锛涙湁鍙傛暟鏃舵仮澶嶆寚瀹?session 骞舵帴缁璇? - 鏄庣‘鎭㈠鍚庣殑 session id銆乵essage list銆亀orking directory 琛屼负 +- 瀹炴柦鑼冨洿锛? - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/repl/__init__.py` +- 瀹屾垚绋嬪害锛? - `/resume` 涓嶅啀鍙繑鍥炶鏄庢枃瀛? - 鎭㈠鍚庝笅涓€杞?query 鑳芥帴鐫€鍘嗗彶娑堟伅杩愯 + - 鎭㈠澶辫触鏃舵湁鍙瘖鏂敊璇俊鎭?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ession list / load 杈圭晫 + - `T2`锛歝ommand -> engine 鎭㈠閾捐矾 + - `T3`锛氬垱寤?session銆佹寔涔呭寲銆佹仮澶嶃€佺户缁棶绛旂殑涓€鏉″畬鏁撮摼 + - `T4`锛氭棤 session / session_id 涓嶅瓨鍦?/ 浼氳瘽鏂囦欢鎹熷潖 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_query_engine_runtime.py` + - 鎵╁睍 `tests/test_main_runtime.py` + - 鏂板 `tests/test_resume_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鍚姩涓€娆′細璇濆苟鐢熸垚鎸佷箙鍖? - 鏂拌繘绋嬫墽琛?`/resume` + - 纭鎭㈠鍚庣户缁璇?- 闈炵洰鏍囷細 + - 涓嶅紩鍏ヨ法鏈哄櫒鎭㈠ + - 涓嶅紩鍏?session 鎼滅储 UI +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-03 auth/config 涓婚摼淇涓庣粺涓€ + +- 鍗″彿锛歚P0-03` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `logout` / `auth` 浣跨敤鏈鍏ョ殑 `ConfigManager` + - `Config` 涓?`ConfigManager` 鍙岃建骞跺瓨锛岃璇佺姸鎬佸垽瀹氫笉绋冲畾 +- 鐮斿彂鍔熻兘锛? - 閫夊畾 canonical config 璇诲啓璺緞 + - 淇 `/login`銆乣/logout`銆乣/auth` + - 缁熶竴 env/config/key masking 瑙勫垯 +- 瀹炴柦鑼冨洿锛? - `claude_code/commands/auth/__init__.py` + - `claude_code/config.py` + - `claude_code/utils/config_manager.py` + - 瑙嗛渶瑕佽皟鏁?`claude_code/main.py` +- 瀹屾垚绋嬪害锛? - `auth` 閾捐矾鏃?`NameError` + - 璁よ瘉鐘舵€佽緭鍑轰笌瀹為檯 env/config 涓€鑷? - logout 鑳芥竻鐞?runtime 鍙鐨?key 鏉ユ簮 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nv > config > default 浼樺厛绾? - `T2`锛歛uth commands 鍦ㄤ笉鍚?key 鏉ユ簮涓嬬殑琛屼负 + - `T3`锛氬懡浠ょ骇鐘舵€佸彉鍖栭獙璇? - `T4`锛氭棤 key / config 缂哄け / key mask 杈撳嚭 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_commands_auth_runtime.py` + - 鎵╁睍 `tests/test_main_runtime.py` +- 鎵嬪伐楠屾敹锛? - 璁剧疆 env key + - 杩愯 `/auth` + - 杩愯 `/logout` + - 鍐嶆杩愯 `/auth` +- 闈炵洰鏍囷細 + - 涓嶅紩鍏?OAuth 鐪熺櫥褰曟祦 + - 涓嶆帴绗笁鏂?secrets manager +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-04 feature flag 杩愯鏃朵慨澶? +- 鍗″彿锛歚P0-04` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `FeatureConfig` 涓?frozen dataclass锛屼絾 `enable()/disable()` 璇曞浘鍘熷湴淇敼 + - 褰撳墠 flag 鏈哄埗瀵规湭鏉ュぇ鍔熻兘鍒囨崲涓嶅彲闈?- 鐮斿彂鍔熻兘锛? - 閲嶆瀯 feature registry 鐨勫唴閮ㄧ姸鎬佽〃杈? - 淇濇寔 env fallback锛屽悓鏃跺厑璁哥▼搴忓唴瀹夊叏鍒囨崲 + - 鏄庣‘ feature 璇诲彇涓庡啓鍏ヨ涔?- 瀹炴柦鑼冨洿锛? - `claude_code/features.py` +- 瀹屾垚绋嬪害锛? - `enable()` / `disable()` 涓嶆姏 `FrozenInstanceError` + - registry 鍐呭琛屼负涓€鑷? - flags 鍙綔涓哄悗缁ぇ鍗$殑 gated rollout 鏈哄埗 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nable / disable / env override / unknown feature + - `T2`锛歳egistry 瀹炰緥鍦ㄥ娆¤皟鐢ㄤ腑鐨勭姸鎬佷竴鑷存€? - `T4`锛氬鐜板綋鍓?`FrozenInstanceError` 鐨勫洖褰掔敤渚?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_features_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鏈湴鑴氭湰鍒囨崲 feature 骞惰鍙?- 闈炵洰鏍囷細 + - 涓嶅紩鍏ヨ繙绋?flag service +- 鍓嶇疆渚濊禆锛氭棤 + +## P0-05 review 鍛戒护浜у搧鍖? +- 鍗″彿锛歚P0-05` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `/review` 鍙槸寮曞鏂囨锛屼笉鎵ц鐪熷疄瀹℃煡 + - 鐢ㄦ埛鏃犳硶渚濊禆璇ュ懡浠ゆ鏌?diff / 鏂囦欢椋庨櫓 +- 鐮斿彂鍔熻兘锛? - 鏀寔 review 褰撳墠 git diff + - 鏀寔 review 鎸囧畾鏂囦欢 + - 缁熶竴 findings 杈撳嚭鏍煎紡锛氶棶棰樸€佷綅缃€佷弗閲嶅害銆佸缓璁? - 鏄庣‘鈥滄棤鍙戠幇鈥濊緭鍑?- 瀹炴柦鑼冨洿锛? - `claude_code/commands/review/__init__.py` + - 瑙嗛渶瑕佹柊澧?`claude_code/services/review_service.py` + - 鍙帴鍏?`git diff` / read-only analyzer +- 瀹屾垚绋嬪害锛? - `/review` 涓嶅啀杩斿洖鍗犱綅 prompt + - 鑳藉褰撳墠鏀瑰姩鎴栬緭鍏ユ枃浠剁粰鍑虹粨鏋勫寲缁撴灉 + - 杈撳嚭鍙綔涓哄悗缁?PR/CI review 鐨勯洀褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬弬鏁拌В鏋愩€佹棤杈撳叆榛樿琛屼负 + - `T2`锛歴ervice 瀵?fake diff 鐨?findings 鐢熸垚 + - `T3`锛氫复鏃?git repo 涓殑 review 涓婚摼 + - `T4`锛氭棤 diff / 鏂囦欢涓嶅瓨鍦?/ 绌虹粨鏋?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_review_command_runtime.py` +- 鎵嬪伐楠屾敹锛? - 淇敼涓€涓枃浠跺悗鎵ц `/review` + - 鎸囧畾鏂囦欢鎵ц `/review path` +- 闈炵洰鏍囷細 + - 涓嶅仛缁勭粐绾?code review dashboard + - 涓嶅己琛屾帴 GitHub API +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-06 session/history/memory 鑱岃矗鏀舵暃 + +- 鍗″彿锛歚P0-06` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - `Session`銆乣HistoryManager`銆乣SessionMemory` 鑱岃矗浜ゅ弶 + - 鐭湡瀵硅瘽銆侀暱鏈熻蹇嗐€佸綊妗e巻鍙茶竟鐣屼笉娓?- 鐮斿彂鍔熻兘锛? - 鏄庣‘涓夊眰鑱岃矗锛? - `Session`锛氬綋鍓嶄細璇濅簨瀹? - `History`锛氬凡缁撴潫/宸蹭繚瀛樼殑娑堟伅褰掓。 + - `Memory`锛氶暱鏈熺粨鏋勫寲鐭ヨ瘑 + - 娑堥櫎閲嶅淇濆瓨涓庝笉涓€鑷村姞杞借矾寰? - 涓?P1 active memory 閾哄簳 +- 瀹炴柦鑼冨洿锛? - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` +- 瀹屾垚绋嬪害锛? - 涓夎€呰亴璐d笌璋冪敤杈圭晫鍐欐竻 + - 褰撳墠涓婚摼鍙繚鐣欎竴涓?message persistence source of truth + - memory 涓嶅啀闅愬紡鎵挎媴 session/history 瑙掕壊 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ave/load/export/search 杈圭晫 + - `T2`锛歴ession -> persisted session 琛屼负 + - `T3`锛歝ontext builder 璇诲彇 memory/session 鐨勮仈鍔? - `T4`锛氶槻姝㈤噸澶嶆秷鎭€侀噸澶嶆寔涔呭寲銆侀敊璇仮澶?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_session_history_memory_runtime.py` + - 鎵╁睍 `tests/test_context_builder_runtime.py` +- 鎵嬪伐楠屾敹锛? - 鐢熸垚浼氳瘽 + - 閲嶅惎鍔犺浇 + - 楠岃瘉 memory 涓嶈璇啓涓鸿亰澶╁巻鍙?- 闈炵洰鏍囷細 + - 涓嶅仛鍚戦噺鏁版嵁搴? - 涓嶅仛闀挎湡妫€绱㈠寮?- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-07 hooks 鎺ュ叆涓昏繍琛屾椂 + +- 鍗″彿锛歚P0-07` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - hooks command銆乭ooks manager銆乼ool/command 鎵ц閾炬病鏈夌湡姝h仈鍔? - hooks 鐩墠鏇村儚闈欐€侀厤缃紝鑰屼笉鏄繍琛屾椂鑳藉姏 +- 鐮斿彂鍔熻兘锛? - 鍦?tool execution 鍓嶅悗瑙﹀彂 hooks + - 鍦?command 鎵ц鍓嶅悗瑙﹀彂 hooks + - 鍦ㄩ敊璇矾寰勮Е鍙?`on_error` + - 绾﹀畾 hook context payload +- 瀹炴柦鑼冨洿锛? - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - 瑙嗛渶瑕佹帴鍏?command dispatch 灞?- 瀹屾垚绋嬪害锛? - hooks 閰嶇疆鑳界湡瀹炵敓鏁? - disabled hook銆佽秴鏃?hook銆佸け璐?hook 琛屼负娓呮櫚 + - 瀵逛富閾惧け璐ヤ笉浜х敓涓嶅彲鎺у壇浣滅敤 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歨ook load/save/parse + - `T2`锛歵rigger before_tool / after_tool / on_error + - `T3`锛氱湡瀹?tool 璋冪敤閾捐Е鍙?hook + - `T4`锛歨ook timeout / hook command failure / disabled hook +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_hooks_runtime.py` +- 鎵嬪伐楠屾敹锛? - 閰嶇疆涓€涓畝鍗?echo hook + - 鎵ц read / grep / bash + - 纭 hook 琚Е鍙?- 闈炵洰鏍囷細 + - 涓嶅仛 org-wide hook registry +- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-08 Windows 鍚姩涓庤В閲婂櫒鎺㈡祴绋冲畾鍖? +- 鍗″彿锛歚P0-08` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - 绯荤粺 `python` 鎸囧悜 WindowsApps stub锛岀敤鎴峰鏄撹窇鍒板亣瑙i噴鍣? - Windows 鐢ㄦ埛鍚姩閾捐剢寮?- 鐮斿彂鍔熻兘锛? - 鍦?`--doctor` 涓瘑鍒В閲婂櫒鏉ユ簮 + - 瀵?WindowsApps stub 缁欏嚭鏄庣‘璀﹀憡 + - 缁熶竴鎺ㄨ崘鍚姩鍣?/ wrapper 琛屼负 + - 鏂囨。鍚屾鏄庣‘ Windows 鎺ㄨ崘杩愯娉?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - `README.md` + - 鍙€夋柊澧?`scripts/` 涓嬭緟鍔╁惎鍔ㄨ剼鏈?- 瀹屾垚绋嬪害锛? - doctor 杈撳嚭鑳借瘖鏂В閲婂櫒椋庨櫓 + - Windows 鐢ㄦ埛涓嶄細璇互涓?stub 鍙敤 + - 椤圭洰鍏ュ彛鏂囨。娓呮鏍囨敞鎺ㄨ崘瑙i噴鍣?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氳В閲婂櫒璺緞璇嗗埆閫昏緫 + - `T2`锛歞octor 杈撳嚭鍒嗘敮 + - `T4`锛歐indowsApps stub 鍥炲綊鐢ㄤ緥 +- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_doctor_runtime.py` +- 鎵嬪伐楠屾敹锛? - 妯℃嫙 / 妫€鏌?doctor 杈撳嚭 +- 闈炵洰鏍囷細 + - 涓嶅仛瀹屾暣瀹夎鍣?- 鍓嶇疆渚濊禆锛歚P0-01` + +## P0-09 娴嬭瘯涓存椂浜х墿娓呯悊涓庨殧绂? +- 鍗″彿锛歚P0-09` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - `tests/.tmp_plugins/` 瀛樺湪娈嬬暀鐩綍 + - 娴嬭瘯闅旂鎬т笌浠撳簱鏁存磥鎬т笉瓒?- 鐮斿彂鍔熻兘锛? - 淇 plugin 娴嬭瘯鐨勬竻鐞嗛€昏緫 + - 澧炶ˉ `.gitignore` 鎴栨祴璇?teardown + - 纭繚 pytest 缁撴潫鍚庝笉鏂板鑴忕洰褰?- 瀹炴柦鑼冨洿锛? - `tests/test_plugins_runtime.py` + - `.gitignore` + - 瑙嗛渶瑕佽皟鏁?plugin fixture +- 瀹屾垚绋嬪害锛? - 鏈湴瀹屾暣 pytest 鍚庝笉鍐嶆畫鐣欐柊澧?`.tmp_*` / `.tmp_plugins/*` + - 娴嬭瘯浜掍笉姹℃煋 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歠ixture cleanup 鏂█ + - `T4`锛氶噸澶嶈繍琛?pytest 涓嶇疮璁℃畫鐣?- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_plugins_runtime.py` +- 鎵嬪伐楠屾敹锛? - 杩炵画璺戜袱娆?pytest + - 妫€鏌ュ伐浣滃尯鑴忔枃浠?- 闈炵洰鏍囷細 + - 涓嶆竻鐞嗙敤鎴风湡瀹炶繍琛屾暟鎹?- 鍓嶇疆渚濊禆锛氭棤 + +## P0-10 Phase 0 鍥炲綊闂ㄧ + +- 鍗″彿锛歚P0-10` +- 浼樺厛绾э細`P0` +- 瑙e喅闂锛? - 鐜版湁娴嬭瘯鍒嗗竷杈冩暎锛屾病鏈夊洿缁?Phase 0 涓婚摼寤虹珛鏄庣‘闂ㄧ +- 鐮斿彂鍔熻兘锛? - 瀹氫箟涓€缁勫繀椤婚€氳繃鐨?runtime regression tests + - 澧炲姞鎸変富閾惧垎缁勭殑 pytest marker 鎴栬剼鏈叆鍙? - 鏄庣‘鎻愪氦鍓嶅繀璺戝懡浠?- 瀹炴柦鑼冨洿锛? - `pytest.ini` + - `scripts/` 涓嬫牎楠岃剼鏈紙濡傞渶瑕侊級 + - 娴嬭瘯鏂囦欢鏍囪鏁寸悊 +- 瀹屾垚绋嬪害锛? - 鑳戒竴鏉″懡浠ゆ墽琛?P0 涓婚摼鍥炲綊 + - 鍥㈤槦鐭ラ亾鈥滃紑宸ュ墠/鍚堝苟鍓嶅繀椤昏窇浠€涔堚€?- 娴嬭瘯浠g爜瑕佹眰锛? - 鏈崱鏈川涓婃槸娴嬭瘯缁勭粐鍗? - 闇€瑕佽ˉ 1 涓祴璇曞叆鍙h剼鏈垨 marker 楠岃瘉 +- 寤鸿娴嬭瘯鏂囦欢锛? - 濡傞渶鏂板锛歚tests/test_phase0_regression_contract.py` +- 鎵嬪伐楠屾敹锛? - 鎵ц涓€鏉?phase0 gate 鍛戒护骞堕獙璇侀€氳繃 +- 闈炵洰鏍囷細 + - 涓嶈姹傛鏃舵帴 CI SaaS +- 鍓嶇疆渚濊禆锛? - `P0-02` ~ `P0-09` + +## 6. Phase 1 浠诲姟鍗″崟 + +## P1-01 daemon / API 鎺у埗闈? +- 鍗″彿锛歚P1-01` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - 褰撳墠 CLI 鏄帤瀹㈡埛绔紝IDE/desktop/web/CI 鏃犳硶鍏辩敤鎵ц鍚庣 +- 鐮斿彂鍔熻兘锛? - 鎻愪緵鏈湴 daemon/API 灞? - 鎶界 session銆乼ask銆乼ool execution銆乤rtifact 璁块棶鎺ュ彛 + - CLI 鏀逛负 daemon client 鐨勪笂灞傚叆鍙?- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/server/` 鎴栫瓑浠锋帶鍒堕潰妯″潡 + - 璋冩暣 `main.py` + - 澶嶇敤 `app.py` +- 瀹屾垚绋嬪害锛? - 鍗曟満鍙惎鍔?daemon + - CLI 涓?daemon 鍙叡浜竴濂楁墽琛屾牳蹇?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欰PI schema / request validation + - `T2`锛歴erver service 灞傝涓? - `T3`锛歝lient -> server -> runtime 涓€鏉″畬鏁撮摼 + - `T4`锛歞aemon 鏈惎鍔?/ 瓒呮椂 / 浼氳瘽涓嶅瓨鍦?- 鍓嶇疆渚濊禆锛歚P0-01`銆乣P0-10` + +## P1-02 event journal 浜嬩欢鏃ュ織 + +- 鍗″彿锛歚P1-02` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - prompt銆乼ool銆乼ask銆乸ermission 鍐崇瓥涓嶅彲鍥炴斁锛屼笉鍙璁?- 鐮斿彂鍔熻兘锛? - 瀹氫箟缁熶竴浜嬩欢妯″瀷 + - 鍐欏叆 prompt/message/tool/task/permission/artifact 浜嬩欢 + - 鎻愪緵鏌ヨ涓?replay 鍩虹鑳藉姏 +- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/services/event_journal.py` 鎴栫瓑浠锋ā鍧? - 鎺ュ叆 `QueryEngine`銆乣TaskManager` +- 瀹屾垚绋嬪害锛? - 鎵€鏈夊叧閿富閾惧姩浣滃彲浜х敓缁撴瀯鍖栦簨浠? - 鏀寔鎸?session 鏌ヨ浜嬩欢娴?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氫簨浠?schema銆佸簭鍒楀寲銆乿ersioning + - `T2`锛歸riter/reader correctness + - `T3`锛歲uery -> tool -> task 浜嬩欢閾? - `T4`锛氶儴鍒嗗啓鍏ュけ璐ャ€佷贡搴忔仮澶?- 鍓嶇疆渚濊禆锛歚P1-01` + +## P1-03 SQLite runtime state backend + +- 鍗″彿锛歚P1-03` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - file/memory backend 瀵规仮澶嶃€佹绱€佸瀹炰緥鍓嶅噯澶囦笉瓒?- 鐮斿彂鍔熻兘锛? - 涓?session/task/event 鎻愪緵 SQLite backend + - 淇濈暀 file backend 浣滀负鍏煎灞傛垨 fallback + - 鏄庣‘ migration 涓?schema version +- 瀹炴柦鑼冨洿锛? - `claude_code/tasks/factory.py` + - `claude_code/tasks/repository.py` + - session/event 鐩稿叧瀛樺偍妯″潡 +- 瀹屾垚绋嬪害锛? - 鑷冲皯鍙敤 SQLite 鎸佷箙鍖?task/session/event + - 鍗曟満閲嶅惎鍚庡彲鎭㈠鏍稿績杩愯鎬?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴chema init / migration + - `T2`锛歳epository CRUD contract + - `T3`锛氶噸鍚仮澶? - `T4`锛氭暟鎹簱鎹熷潖銆侀攣鍐茬獊銆侀噸澶嶅垵濮嬪寲 +- 鍓嶇疆渚濊禆锛歚P1-02` + +## P1-04 active memory 鎺ュ叆杩愯鏃? +- 鍗″彿锛歚P1-04` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - `SessionMemory` 鐜板湪鏄绔?KV锛屼笉褰卞搷涓诲璇濅笂涓嬫枃 +- 鐮斿彂鍔熻兘锛? - 璁?memory 涓?context builder/runtime 鑱斿姩 + - 瀹氫箟 memory scope锛歶ser/project/local + - 鏀寔鏄惧紡鍐欏叆銆佹寜闇€娉ㄥ叆銆佸懡涓粺璁?- 瀹炴柦鑼冨洿锛? - `claude_code/services/memory_service.py` + - `claude_code/context/builder.py` + - `claude_code/agents/builtin.py` +- 瀹屾垚绋嬪害锛? - memory 鎴愪负杩愯鏃剁湡瀹炶緭鍏ワ紝鑰岄潪鏃佽矾瀛樺偍 + - agent / main query 鍙€夋嫨涓嶅悓 memory scope +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歯amespace / key / search 琛屼负 + - `T2`锛歝ontext builder memory 娉ㄥ叆 + - `T3`锛歲uery 鎵ц鏃跺懡涓?memory + - `T4`锛氱┖ memory / 鎹熷潖 memory / scope 闅旂 +- 鍓嶇疆渚濊禆锛歚P0-06`銆乣P1-02` + +## P1-05 hooks / permission / audit 鏀舵暃 + +- 鍗″彿锛歚P1-05` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - hooks銆乸ermissions銆乪vent journal 浠嶆槸鍒嗙珛鑳藉姏 +- 鐮斿彂鍔熻兘锛? - 缁熶竴 hook 浜嬩欢銆乸ermission decision銆乤udit event + - 褰㈡垚涓€娆℃墽琛岀殑瀹屾暣瀹¤閾? - 涓烘湭鏉?org policy 閾哄簳 +- 瀹炴柦鑼冨洿锛? - `claude_code/permissions.py` + - `claude_code/services/hooks_manager.py` + - event journal 鐩稿叧妯″潡 +- 瀹屾垚绋嬪害锛? - 姣忔 tool call 鑷冲皯鍏峰 request -> decision -> hook -> result 鐨勯棴鐜褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歱ermission + hook + event 鑱斿姩 + - `T3`锛氱湡瀹?tool 鎵ц瀹¤閾? - `T4`锛歞eny / timeout / partial failure +- 鍓嶇疆渚濊禆锛歚P0-07`銆乣P1-02` + +## P1-06 CLI thin client 杩佺Щ + +- 鍗″彿锛歚P1-06` +- 浼樺厛绾э細`P1` +- 瑙e喅闂锛? - CLI 涓庢帶鍒堕潰鑰﹀悎杩囨繁锛屼笉鍒╀簬 IDE/CI 鍏辩敤 +- 鐮斿彂鍔熻兘锛? - CLI 鍙戣姹傜粰鏈湴 runtime server/daemon + - 淇濈暀鍗曡繘绋?fallback 妯″紡 + - 缁熶竴杈撳嚭涓庨敊璇涔?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` + - CLI/REPL client adapter +- 瀹屾垚绋嬪害锛? - CLI 浣滀负 runtime 鍓嶇瀛樺湪锛岃€岄潪鎵ц鍐呮牳鏈韩 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient adapter 琛屼负 + - `T3`锛欳LI -> daemon 瀹屾暣涓婚摼 + - `T4`锛歞aemon 涓嶅彲鐢?fallback +- 鍓嶇疆渚濊禆锛歚P1-01` + +## 7. Phase 2 浠诲姟鍗″崟 + +## P2-01 澶?Agent supervisor + +- 鍗″彿锛歚P2-01` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠 agent 鏇村儚鈥滃崟娆″瓙浠诲姟鎵ц鍣ㄢ€濓紝涓嶆槸鎺у埗闈?- 鐮斿彂鍔熻兘锛? - agent DAG + - budget / timeout / retry / ownership + - parent-child artifact passing +- 瀹屾垚绋嬪害锛? - 鑷冲皯鏀寔骞惰 agent + 姹囨€绘敹鍙?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歡raph / budget / timeout 鐘舵€佹満 + - `T2`锛歴upervisor 璋冨害 + - `T3`锛氬苟琛?agent 鎵ц + - `T4`锛氬瓙 agent 澶辫触鎭㈠ +- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02` + +## P2-02 artifact bus 涓庡啿绐佹敹鏁? +- 鍗″彿锛歚P2-02` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 澶?agent 杈撳嚭缂哄皯缁撴瀯鍖?artifact 浜ゆ崲灞?- 鐮斿彂鍔熻兘锛? - 缁熶竴 artifact schema + - patch / note / diff / report / finding 娴佽浆 + - 鍐茬獊妫€娴嬩笌鍚堝苟绛栫暐 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歛rtifact schema + - `T2`锛歱roducer/consumer + - `T3`锛氬 agent artifact 姹囨祦 + - `T4`锛氬啿绐佸悎骞跺け璐?- 鍓嶇疆渚濊禆锛歚P2-01` + +## P2-03 IDE 闆嗘垚锛圴S Code first锛? +- 鍗″彿锛歚P2-03` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠浠撳簱缂哄皯鐪熸鍙敤鐨?IDE workflow +- 鐮斿彂鍔熻兘锛? - VS Code client + - daemon 杩炴帴 + - diff銆乼ask銆乻ession銆乫indings 灞曠ず +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient protocol adapter + - `T3`锛欼DE -> daemon 鏍稿績閾? - `T5`锛氫汉宸ヤ氦浜掗獙鏀朵负涓?- 鍓嶇疆渚濊禆锛歚P1-06` + +## P2-04 GitHub / CI 宸ヤ綔娴?agent + +- 鍗″彿锛歚P2-04` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠鑳藉姏涓昏鍋滅暀鍦ㄦ湰鍦?CLI锛屾棤娉曡繘鍏ュ洟闃熷崗浣滈棴鐜?- 鐮斿彂鍔熻兘锛? - issue -> plan -> code -> test -> review -> PR 宸ヤ綔娴? - CI 鐜鐨?headless execution 妯″紡 +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歸orkflow state machine + - `T3`锛歠ake repo / fake PR 涓婚摼 + - `T4`锛氭潈闄愪笉瓒?/ repo 鐘舵€佷笉骞插噣 / 缃戠粶澶辫触 +- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02` + +## P2-05 缁勭粐绾х瓥鐣ヤ笌瀹¤闈㈡澘 + +- 鍗″彿锛歚P2-05` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 鍥㈤槦鍦烘櫙涓嬬己涔忔不鐞嗐€佸鎵广€佸璁°€佸畨鍏ㄨ竟鐣?- 鐮斿彂鍔熻兘锛? - policy model + - approval queue + - secret redaction + - audit query / reporting +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歱olicy matcher / redaction rules + - `T2`锛歛pproval state transitions + - `T3`锛氭墽琛屽璁¢摼闂幆 + - `T4`锛氱瓥鐣ュ啿绐?/ 鏈鎵规墽琛岄樆鏂?- 鍓嶇疆渚濊禆锛歚P1-05` + +## P2-06 Linux 缁熶竴楠屽案闂ㄧ + +- 鍗″彿锛歚P2-06` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - 褰撳墠姣忓紶鍗″悇鏈?gate 鑴氭湰锛屼絾缂哄皯涓€娆℃€х粺涓€鎵ц娓呭崟锛屼笉鍒╀簬 Linux 闃舵鍏ㄩ噺楠屽案 +- 鐮斿彂鍔熻兘锛? - 鑱氬悎 `P0-01` 鍒?`P2-05` 鍏ㄩ摼璺叧閿祴璇曡剼鏈竻鍗? - 鎻愪緵 Linux 缁熶竴鎵ц鍏ュ彛鑴氭湰 + - 寤虹珛 unified gate manifest 鐨?contract test锛岄伩鍏嶆竻鍗曟紓绉?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮anifest 瀛樺湪鎬т笌鏂囦欢鏄犲皠鏍¢獙 + - `T2`锛氬懡浠ょ敓鎴愰『搴忎笌鏍煎紡濂戠害 + - `T3`锛歀inux 闃舵鎸夋竻鍗曢€愭潯鎵ц锛堟湰鍦颁笉鎵ц锛? - `T4`锛氭柊澧?鍒犲噺娴嬭瘯鏂囦欢鏃跺绾﹀洖褰掓彁绀?- 鍓嶇疆渚濊禆锛歚P2-05` + +## P2-07 Agent 杩愯鏃朵竴鑷存€э紙sync/background parity锛? +- 鍗″彿锛歚P2-07` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `AgentTool` 鐨勫悓姝ヤ笌鍚庡彴璺緞鍦?runtime context 缁ф壙涓婂瓨鍦ㄦ紓绉婚闄? - 鍚庡彴 agent 浠诲姟缂哄皯 session/conversation 绛夊璁″厓鏁版嵁锛屼笉鍒╀簬鍥炴斁涓庢不鐞?- 鐮斿彂鍔熻兘锛? - 瀵归綈 sync/background 瀛?agent 鍦?`working_directory`銆乣tool_registry`銆乣permission_mode`銆乣provider/model`銆乣memory_scope` 鐨勮涓? - 灏?`session_id`銆乣conversation_id`銆乺untime context 瀛楁鍐欏叆鍚庡彴浠诲姟 metadata + - 璁╁悗鍙拌矾寰勪紭鍏堝鐢ㄤ笂灞?runtime `TaskManager`锛岄伩鍏嶅叏灞€鍗曚緥婕傜Щ +- 瀹炴柦鑼冨洿锛? - `claude_code/tools/base.py` + - `claude_code/engine/query.py` + - `claude_code/tools/agent/__init__.py` + - `claude_code/tasks/manager.py` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歴ync/background context 缁ф壙涓€鑷存€? - `T3`锛氬悗鍙颁换鍔?metadata 瀹¤瀛楁瀹屾暣鎬? - `T4`锛歱rovider/model 缁ф壙鍥炲綊锛堟湰鍦?provider 涓?Claude model alias 宸紓锛?- 鍓嶇疆渚濊禆锛歚P2-01`銆乣P2-06` + +## P2-08 鑷畾涔?Agent 鐩綍鍔犺浇闂幆锛?claude/agents锛? +- 鍗″彿锛歚P2-08` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - Python 鐗堟湰 `AgentTool` 鍙敮鎸佸唴缃?`BUILTIN_AGENTS`锛屾湭褰㈡垚 `.claude/agents/*.md` 鐨勫姞杞介棴鐜? - `subagent_type` 鎵句笉鍒版椂鍙娴嬫€т笉瓒筹紝鏃犳硶鍚屾椂鐪嬪埌 custom + builtin 鍙敤绫诲瀷 +- 鐮斿彂鍔熻兘锛? - 鏂板 `.claude/agents/*.md` loader锛屾敮鎸?YAML frontmatter + prompt body 瑙f瀽 + - 鏀寔浠庡綋鍓嶇洰褰曞悜涓婃悳绱㈠埌 git 鏍圭殑澶氱骇 `.claude/agents`锛屽苟鎸夆€滆繎璺緞瑕嗙洊杩滆矾寰勨€濆悎骞? - 鍦?`AgentTool` 涓帴鍏?custom agent lazy-load锛屼紭鍏堢骇 `custom > builtin` + - `Unknown agent type` 閿欒鎻愮ず杈撳嚭鑱斿悎鍙敤 agent 鍒楄〃 +- 瀹炴柦鑼冨洿锛? - `claude_code/agents/loader.py` + - `claude_code/tools/agent/__init__.py` + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歠rontmatter 瑙f瀽涓庡瓧娈靛綊涓€鍖栵紙tools/disallowedTools/memory/maxTurns锛? - `T2`锛氬绾х洰褰曡鐩栫瓥鐣ワ紙root -> nested锛? - `T3`锛欰gentTool custom 瑕嗙洊 builtin 瑙f瀽閾? - `T4`锛歩nvalid YAML / 缂哄瓧娈靛閿?+ unknown type 鑱斿悎鍙敤娓呭崟 +- 鍓嶇疆渚濊禆锛歚P2-07` + +## P2-09 Linux 缁熶竴楠屽案鎵ц闂ㄧ +- 鍗″彿锛歚P2-09` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-06` 褰撳墠鍙緭鍑哄懡浠ゆ竻鍗曪紝涓嶇洿鎺ユ彁渚?Linux 闃舵鐨勪竴閿墽琛屻€佹姤鍛婃矇娣€涓庡け璐ョ瓥鐣ユ帶鍒躲€? - 缁熶竴楠屽案閾剧己灏戞爣鍑嗗寲浜х墿锛坖unit xml + summary json锛夛紝涓嶅埄浜?CI 姹囨€讳笌鍥炴斁銆?- 鐮斿彂鍔熻兘锛? - 鏂板 Linux 鎵ц闂ㄧ鑴氭湰锛屽鐢?`run_linux_unified_gate.py` manifest銆? - 鎻愪緵 `--dry-run` / `--print-commands` / `--continue-on-failure` / `--report-dir` 鑳藉姏銆? - 椤哄簭鎵ц姣忔潯 pytest 鍛戒护骞朵骇鍑烘瘡鏉$敤渚嬬殑 `--junitxml`锛屾渶缁堝啓鍏ョ粺涓€ `summary.json`銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` + - `scripts/run_linux_unified_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭墽琛岄棬绂佸懡浠ょ敓鎴愪弗鏍艰窡闅?unified manifest 椤哄簭銆? - `T2`锛氶澶?pytest 鍙傛暟閫忎紶濂戠害涓?junit 璺緞濂戠害銆? - `T3`锛歀inux 闃舵鎵ц鍏ュ彛鍙?dry-run锛屼笉鍦ㄦ湰鍦拌繍琛屾祴璇曘€? - `T4`锛歮anifest 鍙樻洿鍚庢墽琛岄棬绂佸绾︽祴璇曞彲鍙婃椂鎷︽埅婕傜Щ銆?- 鍓嶇疆渚濊禆锛歚P2-06`銆乣P2-08` + +## P2-10 JetBrains IDE 闆嗘垚锛圛ntelliJ / PyCharm锛?- 鍗″彿锛歚P2-10` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-03` 浠呰鐩?`VS Code first` 閫傞厤锛孞etBrains 鐢熸€佺己灏戞爣鍑嗗寲蹇収閫傞厤灞傘€? - 鐜版湁 IDE payload 铏藉彲澶嶇敤锛屼絾缂哄皯 JetBrains 渚у彲鐩存帴娑堣垂鐨?inspection 缁撴瀯鏄犲皠銆?- 鐮斿彂鍔熻兘锛? - 鍦?`ide_adapter` 澧炲姞 `JetBrainsClientAdapter`銆? - 灏?daemon `workspace.findings` 鏄犲皠涓?JetBrains inspection 缁撴瀯锛坰everity -> highlight锛夈€? - 淇濇寔 daemon API 涓嶅彉锛岄€傞厤灞傚畬鎴愨€滃悓 payload锛屽 IDE 璇箟鈥濇敹鏁涖€?- 瀹炴柦鑼冨洿锛? - `claude_code/server/ide_adapter.py` + - `claude_code/server/__init__.py` + - `tests/test_p2_jetbrains_ide_integration_runtime.py` + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欽etBrains adapter 鍗忚褰掍竴鍖栵紙project_root/vcs/sessions/tasks锛夈€? - `T2`锛歠indings -> inspections 瀛楁鏄犲皠涓?severity/highlight 濂戠害銆? - `T3`锛氫繚鎸?daemon `ide/workspace` payload 鍏煎锛屼笉鏂板璺敱銆? - `T4`锛氶潪娉?line/severity 杈撳叆瀹归敊銆?- 鍓嶇疆渚濊禆锛歚P2-03`銆乣P2-09` + +## P2-11 Linux 鍒嗙墖骞惰楠屽案闂ㄧ +- 鍗″彿锛歚P2-11` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-09` 浠呮敮鎸佸崟杩涚▼椤哄簭鎵ц锛孡inux 闃舵鏃犳硶鐩存帴鎸?CI job 鍒嗙墖骞惰锛屾暣浣撳洖褰掕€楁椂楂樸€? - 缂哄皯鍒嗙墖绾?summary 鍏冩暟鎹紝涓嶅埄浜庡 job 鍥炴敹闃舵鍋氱粺涓€鑱氬悎銆?- 鐮斿彂鍔熻兘锛? - 鍦?`run_p2_linux_unified_execution_gate.py` 澧炲姞 `--shard-total` / `--shard-index`銆? - 鎸?unified manifest 椤哄簭鍋氱‘瀹氭€у垎鐗囬€夋嫨锛堝悓涓€杈撳叆濮嬬粓绋冲畾鍒囩墖锛夈€? - summary 澧炲姞 `shard_total` / `shard_index` / `manifest_total_tests` 瀛楁銆? - 淇濇寔 `--dry-run` / `--print-commands` 璺緞鍙褰撳墠鍒嗙墖瑕嗙洊鑼冨洿銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囬€夋嫨涓?manifest 椤哄簭濂戠害锛坉eterministic slicing锛夈€? - `T2`锛氶潪娉曞垎鐗囧弬鏁版嫆缁濆绾︼紙`shard_total < 1` / `shard_index` 瓒婄晫锛夈€? - `T3`锛歴ummary 鍒嗙墖鍏冩暟鎹绾︺€? - `T4`锛氶澶?pytest 鍙傛暟閫忎紶銆乯unit 璺緞濂戠害缁х画淇濇寔銆?- 鍓嶇疆渚濊禆锛歚P2-09` + +## P2-12 Linux 鍒嗙墖楠屽案姹囨€婚棬绂?- 鍗″彿锛歚P2-12` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-11` 宸叉敮鎸佸垎鐗囧苟琛屾墽琛岋紝浣嗙己灏?Linux 鍥炴敹闃舵鐨勭粺涓€姹囨€诲叆鍙o紝鏃犳硶缁欏嚭鍏ㄥ眬閫氳繃/澶辫触鍒ゅ畾銆? - 澶氫釜 shard 鐨?`summary.json` 缂哄皯涓€鑷存€ф牎楠岋紙缂虹墖/閲嶅鐗?manifest 婕傜Щ锛夛紝瀹规槗璁?CI 璇垽銆?- 鐮斿彂鍔熻兘锛? - 鏂板鍒嗙墖姹囨€婚棬绂佽剼鏈紝鏀寔 `--summary` / `--summary-glob` / `--artifacts-dir` 鑷姩鍙戠幇 summary 鏂囦欢銆? - 鍚堝苟 shard 缁撴灉骞惰緭鍑?`merged_summary.json`锛屾矇娣€ `overall_status`銆乣structural_issues`銆乣shards_missing` 涓庡叏灞€ totals銆? - 瀵瑰叧閿粨鏋勫仛濂戠害鏍¢獙锛氬瓧娈靛畬鏁存€с€乣shard_index` 鑼冨洿銆乣passed+failed<=total_tests`銆乵anifest/鍒嗙墖鎬绘暟涓€鑷存€с€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_aggregation_gate.py` + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` + - `tests/test_p2_linux_unified_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ummary 鍙戠幇绛栫暐鍘婚噸濂戠害锛坉irect + glob + artifacts-dir锛夈€? - `T2`锛歴ummary 鍩虹瀛楁鏍¢獙涓庣被鍨嬬害鏉熷绾︺€? - `T3`锛氬叏鍒嗙墖瑕嗙洊鏃剁殑鑱氬悎 totals 涓?passed 鐘舵€佸绾︺€? - `T4`锛氱己鐗?鎬绘暟婕傜Щ鏃剁殑 structural issue 涓?failed 鐘舵€佸绾︺€?- 鍓嶇疆渚濊禆锛歚P2-11` + +## P2-13 Linux 鏈€缁堟姤鍛婂彂甯冮棬绂?- 鍗″彿锛歚P2-13` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-12` 浠呰緭鍑?`merged_summary.json`锛岀己灏?Linux 鍥炴敹闃舵鍙洿鎺ュ綊妗d笌灞曠ず鐨勬渶缁堟姤鍛婁骇鐗┿€? - 瀵?`overall_status` 缂哄皯鈥滃啀璁$畻鏍¢獙鈥濅笌鍙戝竷濂戠害锛孋I 渚ч毦浠ョǔ瀹氭秷璐规渶缁堢粨璁恒€?- 鐮斿彂鍔熻兘锛? - 鏂板鏈€缁堟姤鍛婂彂甯冮棬绂佽剼鏈紝璇诲彇 `merged_summary.json` 骞跺仛瀛楁濂戠害鏍¢獙銆? - 杈撳嚭 `final_report.json` 涓?`final_report.md`锛堢姸鎬併€佸垎鐗囥€乼otals銆乻tructural issues銆乶otes锛夈€? - 瀵?`overall_status` 鎵ц computed-vs-reported 涓€鑷存€у鏍革紝涓嶄竴鑷存椂寮哄埗澶辫触銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_report_publish_gate.py` + - `tests/test_p2_linux_report_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮erged summary 蹇呭~瀛楁涓庣被鍨嬪绾︽牎楠屻€? - `T2`锛歨appy path 涓嬫渶缁堢姸鎬佷笌 totals 鍙戝竷濂戠害銆? - `T3`锛歳eported status 涓?computed status 涓嶄竴鑷存椂澶辫触濂戠害銆? - `T4`锛歁arkdown 鎶ュ憡缁撴瀯濂戠害锛圱otals/Shards/Structural Issues/Notes锛夈€?- 鍓嶇疆渚濊禆锛歚P2-12` + +## P2-14 Linux 缁熶竴楠屽案鍏ㄩ摼缂栨帓闂ㄧ +- 鍗″彿锛歚P2-14` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-09` / `P2-12` / `P2-13` 宸叉媶鍒嗕负澶氶樁娈佃剼鏈紝浣嗙己灏戜竴鏉?Linux 涓€閿覆鑱旀墽琛屽叆鍙c€? - 澶氶樁娈垫墜宸ユ嫾瑁呭懡浠ゆ槗婕傜Щ锛屽け璐ョ瓥鐣ヤ笌鍙傛暟閫忎紶鍦?CI 渚ч毦缁熶竴銆?- 鐮斿彂鍔熻兘锛? - 鏂板 pipeline gate锛屾寜 `execution -> aggregation -> publish` 缂栨帓鎵ц銆? - 鏀寔 `--skip-execution` / `--skip-aggregation` / `--skip-publish`锛岃鐩栧崟鏈轰笌澶?job 鍥炴敹鍦烘櫙銆? - 鏀寔 `--print-commands` / `--dry-run` 浜у嚭鍙璁″懡浠ゆ竻鍗曪紝鏀寔 `--fail-fast` 澶辫触鎺у埗銆? - 鏀寔 execution 闃舵 pytest 棰濆鍙傛暟閫忎紶锛坄--` 杞彂鍒?`P2-09`锛夈€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_pipeline_gate.py` + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁や笁闃舵鍛戒护鐢熸垚濂戠害锛坋xecution summary 鑷姩娉ㄥ叆 aggregation锛夈€? - `T2`锛歚skip-execution` 涓嬪閮?summary/glob/artifacts 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氬叏璺宠繃妯″紡杩斿洖绌?pipeline 濂戠害銆? - `T4`锛歱ytest 棰濆鍙傛暟閫忎紶濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-13` + +## P2-15 Linux 鍒嗙墖鎵ц璁″垝鐢熸垚闂ㄧ +- 鍗″彿锛歚P2-15` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-11`/`P2-14` 宸叉敮鎸佸垎鐗囨墽琛屼笌鍏ㄩ摼缂栨帓锛屼絾 Linux CI fan-out job 浠嶉渶鎵嬪伐鎷兼帴 `shard_index` 鍛戒护锛屽鏄撴紓绉汇€? - 缂哄皯鈥滄墽琛屽墠鈥濊鍒掍骇鐗╋紝CI 鍥炴敹闃舵闅句互瀵圭収鏍¢獙鍒嗙墖瑕嗙洊鏄惁瀹屾暣銆?- 鐮斿彂鍔熻兘锛? - 鏂板 shard plan gate锛屽熀浜?unified manifest 鐢熸垚纭畾鎬?`shard_plan.json`銆? - 姣忎釜鍒嗙墖杈撳嚭锛歚test_files`銆乣report_dir`銆乣summary_path`銆乣execution command`銆乣command_parts`銆? - 鏀寔 `--print-commands`/`--dry-run` 鐢ㄤ簬 CI 缂栨帓鎺㈡祴锛涙敮鎸?pytest 棰濆鍙傛暟閫忎紶锛坄--`锛夈€? - 瀵?`shard_total` 涓?manifest 鏂囦欢瀛樺湪鎬у仛鍓嶇疆濂戠害鏍¢獙锛岄潪娉曡緭鍏ョ洿鎺ュけ璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_plan_gate.py` + - `tests/test_p2_linux_shard_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囧垏鐗囩‘瀹氭€у绾︼紙涓?`P2-11` slicing 涓€鑷达級銆? - `T2`锛氬垎鐗囨墽琛屽懡浠や笌 pytest 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氶潪娉?`shard_total` 鎷掔粷濂戠害銆? - `T4`锛氳緭鍑?`summary_path/report_dir` 瀛楁濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-14` + +## P2-16 Linux CI Matrix 瀵煎嚭闂ㄧ +- 鍗″彿锛歚P2-16` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-15` 宸蹭骇鍑?`shard_plan.json`锛屼絾 Linux CI 浠嶉渶鎵嬪伐鎶婂垎鐗囪鍒掕浆涓?matrix 鍙橀噺锛屾槗浜х敓缂栨帓婕傜Щ銆? - fan-out 涓?fan-in 涔嬮棿缂哄皯缁熶竴鐨?`summary_paths` 鍥炴敹娓呭崟锛屽鑷村洖鏀堕樁娈靛弬鏁版嫾瑁呬笉绋冲畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 CI matrix gate锛屼粠 `shard_plan.json` 鐢熸垚鍙洿鎺ユ秷璐圭殑 `ci_matrix.json`銆? - 杈撳嚭鏍囧噯 `matrix.include` 鏉$洰锛坄shard_index/shard_total/command/command_parts/report_dir/summary_path`锛夈€? - 杈撳嚭 `summary_paths` 鍥炴敹娓呭崟锛屼緵 `P2-12` 鑱氬悎闃舵鐩存帴浣跨敤銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions output 鏂囦欢锛涙敮鎸?`--skip-empty-shards` 杩囨护绌哄垎鐗囥€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_matrix_gate.py` + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮atrix include 椤哄簭涓庡瓧娈靛绾︺€? - `T2`锛歚skip-empty-shards` 杩囨护濂戠害銆? - `T3`锛欸itHub output 瀛楁鐢熸垚濂戠害锛坢atrix/summary_paths/selected_shards锛夈€? - `T4`锛歚manifest_total_tests` 涓庡垎鐗?totals 涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-15` + +## P2-17 Linux CI Workflow 璁″垝闂ㄧ +- 鍗″彿锛歚P2-17` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-16` 宸蹭骇鍑?`ci_matrix.json`锛屼絾 fan-out/fan-in CI job 浠嶉渶浜哄伐鎷兼帴鍥炴敹鍛戒护锛屽彂甯冮摼瀛樺湪鎵嬪伐婕傜Щ椋庨櫓銆? - 缂哄皯缁熶竴 workflow 璁″垝浜х墿锛孋I 缂栨帓渚ф棤娉曠洿鎺ユ秷璐光€滃垎鐗囨墽琛?+ 姹囨€?+ 鍙戝竷鈥濆叏閾惧懡浠ゃ€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow plan gate锛屼粠 `ci_matrix.json` 鐢熸垚 `ci_workflow_plan.json`銆? - 杈撳嚭 `fan_out_jobs` 涓?`fan_out_matrix.include`锛堝惈 shard job command/command_parts/report/summary 淇℃伅锛夈€? - 杈撳嚭 `fan_in` 姹囨€讳笌鍙戝竷鍛戒护锛坅ggregation/publish command + command_parts锛夛紝鐩存帴瀵规帴 `P2-12`/`P2-13`銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions outputs锛坄fan_out_matrix`銆乣fan_in_summary_paths`銆乣aggregation_command`銆乣publish_command` 绛夛級銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_plan_gate.py` + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow payload 涓?fan-out/fan-in 瀛楁涓庡懡浠ゅ绾︺€? - `T2`锛欸itHub output 瀛楁鐢熸垚濂戠害銆? - `T3`锛歚summary_paths` 涓?`matrix.include` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-16` + +## P2-18 Linux CI Workflow YAML 娓叉煋闂ㄧ +- 鍗″彿锛歚P2-18` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-17` 宸蹭骇鍑?`ci_workflow_plan.json`锛屼絾 CI 骞冲彴浠嶉渶浜哄伐缂栧啓/缁存姢 workflow YAML锛宖an-out/fan-in 鍛戒护鏄撴紓绉汇€? - 缂哄皯鈥滆鍒掍骇鐗?-> 鍙墽琛?workflow 鏂囦欢鈥濈殑鏈€鍚庝竴璺筹紝瀵艰嚧闂ㄧ閾句粛瀛樺湪鎵嬪伐浠嬪叆鐐广€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow YAML gate锛屼粠 `ci_workflow_plan.json` 娓叉煋 `.github/workflows/linux_unified_gate.yml`銆? - 娓叉煋 fan-out matrix job锛堝垎鐗囨墽琛?+ summary artifact 涓婁紶锛変笌 fan-in job锛坅rtifact 鍥炴敹 + 鑱氬悎 + 鍙戝竷 + 鏈€缁堟姤鍛婁笂浼狅級銆? - 杈撳嚭娓叉煋鍏冩暟鎹?`ci_workflow_render.json`锛岃褰?source plan銆乫an-in 鍛戒护銆佹姤鍛婅矾寰勪笌鍒嗙墖缁熻锛屼緵瀹¤杩借釜銆? - 鏀寔 `--dry-run`/`--print-yaml` 蹇€熼獙鐪嬫覆鏌撶粨鏋滐紝涓嶅湪鏈湴瑙﹀彂娴嬭瘯鎵ц銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow YAML fan-out/fan-in 瀛楁涓庡懡浠ゆ覆鏌撳绾︺€? - `T2`锛歳ender metadata 瀛楁濂戠害锛坰ource/workflow path/summary stats锛夈€? - `T3`锛歚fan_in.summary_paths` 涓?`fan_out_matrix.include[*].summary_path` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-17` + +## P2-19 Linux CI Workflow 婕傜Щ鏍¢獙闂ㄧ +- 鍗″彿锛歚P2-19` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-18` 宸茶兘娓叉煋 workflow YAML 涓?metadata锛屼絾浠撳簱涓殑 `.github/workflows/linux_unified_gate.yml` 涓?`ci_workflow_render.json` 鍙兘琚墜宸ユ敼鍔ㄥ悗涓?`ci_workflow_plan.json` 鑴辫妭銆? - 缂哄皯鈥滆鍒掍骇鐗?-> 娓叉煋浜х墿涓€鑷存€ф牎楠屸€濋棬绂侊紝CI 鍙兘鍦ㄦ紓绉荤姸鎬佷笅杩愯锛屽鑷?fan-out/fan-in 鍛戒护涓庢姤鍛婅矾寰勫け鐪熴€?- 鐮斿彂鍔熻兘锛? - 鏂板 workflow sync gate锛屽熀浜?`ci_workflow_plan.json` 澶嶇畻鏈熸湜 workflow YAML 涓?metadata锛屽苟涓庡綋鍓嶄骇鐗╁仛涓€鑷存€ф瘮瀵广€? - 杈撳嚭 drift 鎽樿锛坵orkflow_drift/metadata_drift/drift_count锛夛紝鏀寔 `--print-diff` 鎵撳嵃 unified diff 渚夸簬瀹¤銆? - 鏀寔 `--write` 鍦ㄦ湰鍦版垨 CI 閲岃嚜鍔ㄥ洖鍐欐爣鍑嗕骇鐗╋紝鏀舵暃浜哄伐婕傜Щ銆? - 鏀寔 `--strict-generated-at` 寮€鍚?metadata `generated_at` 涓ユ牸姣斿锛涢粯璁ゅ拷鐣ヨ瀛楁浠呮牎楠岀粨鏋勪笌鏍稿績鍐呭銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_sync_gate.py` + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬綋 workflow/metadata 缂哄け鏃跺繀椤昏Е鍙?drift 鍒ゅ畾銆? - `T2`锛氬綋 workflow 鍐呭琚敼鍐欐椂锛屽繀椤昏緭鍑?drift 涓庡彲璇?diff 濂戠害銆? - `T3`锛氶粯璁ゆā寮忓拷鐣?metadata `generated_at` 婕傜Щ锛岄伩鍏嶆椂闂存埑鍣0璇姤銆? - `T4`锛氬紑鍚?strict 妯″紡鏃讹紝`generated_at` 婕傜Щ蹇呴』瑙﹀彂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-18` + +## P2-20 Linux CI Workflow 鍛戒护鎶ゆ爮闂ㄧ +- 鍗″彿锛歚P2-20` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-19` 宸茶兘鏍¢獙 plan->yaml/metadata 婕傜Щ锛屼絾灏氭湭绾︽潫 `ci_workflow_plan.json` 鍐?`command` 涓?`command_parts` 鐨勪竴鑷存€э紝瀛樺湪鈥滃瓧绗︿覆琚敼銆乸arts 鏈敼鈥濆鑷?CI 瀹為檯鎵ц鍋忕Щ鐨勯闄┿€? - fan-out 涓?fan-in 鍛戒护涓殑鍏抽敭璺緞锛坄report_dir`銆乣summary_paths`銆乣merged_summary/final_report`锛夌己灏戝己濂戠害缁戝畾锛屽彲鑳藉湪鎵嬪伐缂栬緫鍚庡紩鍏ラ潤榛橀敊璇€?- 鐮斿彂鍔熻兘锛? - 鏂板 command guard gate锛屾牎楠?`command`/`command_parts` 鍙€嗕竴鑷存€э紙`shlex.split(command)` == `command_parts`锛変笌 canonical shell 瑙勮寖鍖栬緭鍑恒€? - 鏍¢獙鑴氭湰鐩爣涓庡叧閿?flag 濂戠害锛? - fan-out 蹇呴』鎸囧悜 `run_p2_linux_unified_execution_gate.py`锛屼笖鎼哄甫 `--report-dir/--shard-total/--shard-index`銆? - fan-in aggregation 蹇呴』鎸囧悜 `run_p2_linux_shard_aggregation_gate.py`锛屼笖 `--summary` 椤哄簭涓ユ牸鍖归厤 `fan_in.summary_paths`銆? - fan-in publish 蹇呴』鎸囧悜 `run_p2_linux_report_publish_gate.py`锛屼笖杈撳嚭璺緞涓?`fan_in` 瀛楁涓€鑷淬€? - 鏀寔 `--write` 鍥炲啓 canonical command 瀛楃涓诧紝鏀舵暃璁″垝鏂囦欢鍛戒护鏍煎紡婕傜Щ銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬悎娉?workflow plan 蹇呴』閫氳繃 guard锛宍failed_commands=0`銆? - `T2`锛歚command`/`command_parts` 涓嶄竴鑷存椂蹇呴』瑙﹀彂鎷掔粷锛屽苟缁欏嚭瑙勮寖鍖栧懡浠ゃ€? - `T3`锛歛ggregation 鐨?`--summary` 椤哄簭涓?`fan_in.summary_paths` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛歱ublish 杈撳嚭璺緞涓?`fan_in.final_report_*` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆?- 鍓嶇疆渚濊禆锛歚P2-19` + +## P2-21 Linux CI Workflow 娌荤悊鏀舵暃闂ㄧ +- 鍗″彿锛歚P2-21` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-19` 涓?`P2-20` 宸插垎鍒鐩栤€滄覆鏌撴紓绉烩€濆拰鈥滃懡浠ゅ绾︹€濓紝浣嗙己灏戠粺涓€娌荤悊缁撹锛孋I 渚т粛闇€鎵嬪伐鎷兼帴澶氫唤鎶ュ憡鍒ゆ柇鏄惁鍙墽琛?workflow銆? - 缂哄皯 metadata 琛€缂樹竴鑷存€ч棬绂侊紙`source_ci_workflow_plan` 涓?`workflow_output_path`锛夛紝瀛樺湪鈥滀骇鐗╃湅浼奸€氳繃銆佹潵婧愬凡閿欎綅鈥濈殑闅愭€ч闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance gate锛屼竴娆℃墽琛屾眹鎬?`workflow_sync + command_guard + metadata lineage` 涓夌被妫€鏌ワ紝浜у嚭鍗曚竴 `overall_status`銆? - 杈撳嚭 `failed_checks` 鏄庣‘澶辫触绫诲埆锛歚workflow_sync_drift` / `command_guard_failed` / `lineage_mismatch`銆? - 灏?`command_guard` 鎽樿鍖栦负 `failed_commands/issue_count/normalization_required`锛屼究浜?CI 鑱氬悎灞曠ず銆? - 瀵?metadata 琛€缂樺仛寮烘牎楠岋細`source_ci_workflow_plan` 涓?`workflow_output_path` 蹇呴』涓庡綋鍓嶉棬绂佽緭鍏ヨ矾寰勪竴鑷淬€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow/command/lineage 鍏ㄩ儴涓€鑷存椂蹇呴』閫氳繃锛宍overall_status=passed`銆? - `T2`锛歸orkflow YAML 婕傜Щ鏃跺繀椤昏惤鍏?`workflow_sync_drift` 澶辫触绫汇€? - `T3`锛歝ommand 濂戠害寮傚父鏃跺繀椤昏惤鍏?`command_guard_failed` 澶辫触绫汇€? - `T4`锛歮etadata 琛€缂樺瓧娈典笌杈撳叆璺緞涓嶄竴鑷存椂蹇呴』钀藉叆 `lineage_mismatch` 澶辫触绫汇€?- 鍓嶇疆渚濊禆锛歚P2-20` + +## P2-22 Linux CI Workflow 娌荤悊鍙戝竷闂ㄧ +- 鍗″彿锛歚P2-22` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-21` 宸蹭骇鍑虹粺涓€娌荤悊缁撹锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽 `ci_workflow_governance.json` 鎵嶈兘鍐冲畾鈥滄槸鍚︽墽琛?workflow鈥濄€? - 缂哄皯鍥哄畾鐨勫彂甯冧骇鐗╀笌 GitHub output 濂戠害锛屽鑷?fan-in 闃舵闅句互绋冲畾娑堣垂娌荤悊缁撴灉涓庡け璐ョ被鍒€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance publish gate锛岃鍙?`P2-21` 鎶ュ憡鍚庣敓鎴愬彲娑堣垂鍙戝竷浜х墿锛? - `ci_workflow_governance_publish.json` + - `ci_workflow_governance_publish.md` + - 杈撳嚭绋冲畾鍐崇瓥瀛楁锛歚overall_status`銆乣should_execute_workflow`銆乣failed_checks`銆乣failed_check_count`銆? - 鏀寔 `--github-output` 瀵煎嚭 CI 鍙橀噺锛歚governance_status`銆乣governance_should_execute_workflow`銆乣governance_failed_checks` 绛夈€? - 澧炲姞鍙戝竷渚т竴鑷存€ф牎楠岋細鑻?`failed_checks` 鎴?`overall_status` 涓庨噸绠楃粨鏋滀笉涓€鑷达紝蹇呴』钀藉叆 `structural_issues` 骞跺垽瀹氬け璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗘姤鍛婂叏閲忎竴鑷存椂搴旇緭鍑?`overall_status=passed` 涓?`should_execute_workflow=true`銆? - `T2`锛氬綋娌荤悊鎶ュ憡鍖呭惈澶辫触妫€鏌ユ椂锛屽簲杈撳嚭 `overall_status=failed` 涓?`should_execute_workflow=false`銆? - `T3`锛氬綋娌荤悊鎶ュ憡鐨?`failed_checks` 涓庨噸绠楃粨鏋滀笉涓€鑷存椂锛屽繀椤昏Е鍙?`structural_issues` 骞跺け璐ャ€? - `T4`锛欸itHub output 瀵煎嚭瀛楁蹇呴』鍖呭惈鐘舵€併€佸け璐ュ垪琛ㄤ笌鍙戝竷浜х墿璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-21` + +## P2-23 Linux CI Workflow 鎵ц鍐崇瓥闂ㄧ +- 鍗″彿锛歚P2-23` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-22` 铏藉凡浜у嚭 `should_execute_workflow`锛屼絾 CI 涓嬫父浠嶇己鈥滅粺涓€鎵ц闂搁棬鈥濆皢娌荤悊缁撴灉杞垚鏄庣‘鐨?execute/blocked 缁撴灉涓庨€€鍑虹爜璇箟銆? - 缂哄皯瀵规墽琛屽墠鍏抽敭浜х墿锛坵orkflow plan / rendered workflow锛夌殑瀛樺湪鎬ф牎楠岋紝鍙兘鍑虹幇鈥滄不鐞嗛€氳繃浣嗘墽琛屽叆鍙g己澶扁€濈殑鏂摼銆?- 鐮斿彂鍔熻兘锛? - 鏂板 execution decision gate锛岃鍙?`P2-22` 鍙戝竷缁撴灉锛岀粺涓€杈撳嚭锛? - `ci_workflow_execution_decision.json` + - `ci_workflow_execution_decision.md` + - 鍥哄寲鍐崇瓥瀛楁锛歚decision`銆乣decision_status`銆乣should_execute_workflow`銆乣exit_code`銆乣reason_codes`銆乣missing_artifacts`銆? - 鏀寔闃绘柇绛栫暐寮€鍏筹細`--on-block fail|skip`锛岃 CI 鍙湪闃绘柇鏃堕€夋嫨 fail-fast 鎴?soft-skip銆? - 鏀寔 `--github-output` 杈撳嚭涓嬫父鍙橀噺锛屽噺灏?job 闂存墜宸ヨВ鏋愩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_decision_gate.py` + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗛€氳繃涓旀墽琛屼骇鐗╅綈澶囨椂蹇呴』杈撳嚭 `decision=execute`銆乣exit_code=0`銆? - `T2`锛氭不鐞嗗け璐ユ椂蹇呴』杈撳嚭 `decision=blocked` 涓旀惡甯﹀け璐ュ師鍥犵爜銆? - `T3`锛氶樆鏂瓥鐣ヤ负 `skip` 鏃讹紝闃绘柇鍐崇瓥搴旇繑鍥?`exit_code=0`銆? - `T4`锛欸itHub output 濂戠害闇€鍖呭惈 decision/status/should_execute/exit_code 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-22` + +## P2-24 Linux CI Workflow 璋冨害灏辩华闂ㄧ +- 鍗″彿锛歚P2-24` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-23` 宸蹭骇鍑?execute/blocked 鍐崇瓥锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐灏嗗喅绛栧瓧娈垫嫾瑁呬负瀹為檯璋冨害鍛戒护锛屽瓨鍦ㄥ懡浠や笉涓€鑷翠笌閿欒瑙﹀彂椋庨櫓銆? - 缂哄皯缁熶竴 dispatch 濂戠害鎶ュ憡锛屽鑷粹€滄槸鍚﹀彲鍙戣捣 `gh workflow run`鈥濈殑鍒ゆ柇鏃犳硶琚?job 闂寸ǔ瀹氬鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch readiness gate锛岃鍙?`P2-23` 鍐崇瓥浜х墿骞惰緭鍑猴細 + - `ci_workflow_dispatch.json` + - `ci_workflow_dispatch.md` + - 鍥哄寲璋冨害瀛楁锛歚dispatch_status`銆乣dispatch_mode`銆乣should_dispatch_workflow`銆乣dispatch_command`銆乣dispatch_command_parts`銆? - execute 鍐崇瓥鏃剁敓鎴愯鑼冨寲 `gh workflow run` 鍛戒护锛沚locked 鍐崇瓥鏃惰緭鍑虹┖鍛戒护骞朵繚鎸侀樆鏂師鍥犲彲杩芥函銆? - 鏀寔 `--github-output` 瀵煎嚭 dispatch 鐘舵€併€佸懡浠や笌鎶ュ憡璺緞锛屼緵涓嬫父 CI 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟xecute 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=ready` 涓庡悎娉?`gh workflow run` 鍛戒护銆? - `T2`锛歜locked 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=blocked` 涓?`dispatch_command` 涓虹┖銆? - `T3`锛氬喅绛栧绾﹀啿绐侊紙濡?decision=execute 浣?should_execute=false锛夋椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 dispatch 鐘舵€併€佽皟搴﹀懡浠や笌鎶ュ憡璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-23` + +## P2-25 Linux CI Workflow 璋冨害鎵ц闂ㄧ +- 鍗″彿锛歚P2-25` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-24` 浠呬骇鍑衡€滃彲璋冨害濂戠害鈥濓紝鏈粺涓€澶勭悊鐪熸 dispatch 鎵ц缁撴灉锛孋I 涓嬫父浠嶉渶閲嶅鎷兼帴鈥滄槸鍚︽墽琛?鎵ц澶辫触濡備綍鍥炴姤鈥濈殑閫昏緫銆? - 缂哄皯璋冨害鎵ц灞傜粺涓€鎶ュ憡锛屽鑷?execute/blocked/fail 涓夋€佹棤娉曠ǔ瀹氭矇娣€涓哄崟涓€瀹¤浜х墿銆?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch execution gate锛岃鍙?`P2-24` 璋冨害濂戠害骞惰緭鍑猴細 + - `ci_workflow_dispatch_execution.json` + - `ci_workflow_dispatch_execution.md` + - 鍥哄寲鎵ц瀛楁锛歚execution_status`銆乣execution_exit_code`銆乣dispatch_attempted`銆乣command_returncode`銆乣reason_codes`銆? - `dispatch_status=ready` 鏃舵墽琛?`dispatch_command_parts`锛堟敮鎸?`--dry-run` 璺宠繃鐪熷疄鎵ц锛夛紱`dispatch_status=blocked` 鏃朵繚鎸佹棤鎵ц浣嗕繚鐣欓樆鏂涔夈€? - 鏀寔 `--github-output` 瀵煎嚭鎵ц鐘舵€併€侀€€鍑虹爜涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歳eady + dry-run 蹇呴』浜у嚭 `execution_status=ready_dry_run` 涓?`execution_exit_code=0`銆? - `T2`锛歜locked 鍐崇瓥蹇呴』淇濇寔 `dispatch_attempted=false` 涓旈€€鍑虹爜閬靛惊涓婃父闃绘柇绛栫暐璇箟銆? - `T3`锛歞ispatch 濂戠害鍐茬獊锛堝 command 涓?command_parts 涓嶄竴鑷达級蹇呴』鎷掔粷銆? - `T4`锛氬懡浠ゆ墽琛屽け璐ユ椂蹇呴』钀藉湴 `execution_status=dispatch_failed` 骞舵惡甯﹀け璐ュ師鍥犵爜銆? - `T5`锛欸itHub output 濂戠害闇€鍖呭惈鎵ц鐘舵€併€佹槸鍚﹀皾璇曡皟搴︺€侀€€鍑虹爜涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-24` + +## P2-26 Linux CI Workflow 鍏ㄩ摼缂栨帓闂ㄧ +- 鍗″彿锛歚P2-26` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-17` 鑷?`P2-33` 鍚勯棬绂佸凡鍏峰锛屼絾 CI 渚т粛闇€浜哄伐鎷兼帴澶氭潯鍛戒护锛屽鏄撳嚭鐜伴樁娈甸仐婕忋€佸弬鏁版紓绉讳笌椤哄簭閿欒銆? - 缂哄皯鈥滃崟鍏ュ彛鈥濋棬绂佺粺涓€璋冨害 workflow plan/yaml/sync/governance/dispatch/terminal publish 鍏ㄩ摼锛屽鑷?Linux CI 缂栨帓閲嶅缁存姢鎴愭湰楂樸€?- 鐮斿彂鍔熻兘锛? - 鏂板 full pipeline gate锛屼竴娆$紪鎺?`P2-17 -> P2-33` 鍗佸叚闃舵锛? - plan -> yaml -> sync -> command_guard -> governance -> governance_publish -> decision -> dispatch -> dispatch_execution -> dispatch_trace -> dispatch_completion -> terminal_publish -> release_handoff -> release_trigger -> release_trace -> release_completion銆? - 鏀寔缁熶竴闃舵瑁佸壀 `--skip-*`锛屽厑璁稿湪 CI fan-in / rerun 鍦烘櫙鎸夐渶璺宠繃鍓嶅簭闃舵銆? - 鏀寔缁熶竴绛栫暐閫忎紶锛歚--strict-generated-at`銆乣--sync-write`銆乣--command-guard-write`銆乣--on-block`銆乣--workflow-ref`銆乣--dispatch-timeout-seconds`銆乣--dispatch-trace-poll-now`銆乣--completion-allow-in-progress`銆? - 鏀寔 `--print-commands/--dry-run` 棰勮涓?`--fail-fast` 鎵ц绛栫暐锛屾矇娣€鍗曚竴 Linux CI 缂栨帓鍏ュ彛銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁ら樁娈甸摼蹇呴』瀹屾暣瑕嗙洊 `P2-17 -> P2-33` 涓旈『搴忓浐瀹氥€? - `T2`锛氱瓥鐣ュ弬鏁帮紙strict/sync-write/command-guard-write/on-block/workflow-ref/timeout/trace-poll锛夊繀椤绘纭€忎紶鍒板搴斿瓙闂ㄧ銆? - `T3`锛氶儴鍒?`--skip-*` 鏃跺繀椤诲彧淇濈暀鍚庡崐閾鹃樁娈靛苟淇濇寔椤哄簭銆? - `T4`锛氬叏閮?`--skip-*` 鏃跺簲杩斿洖绌鸿鍒掞紙涓嶈Е鍙戦樁娈垫墽琛岋級銆? - `T5`锛氭敮鎸佷粎缁堝眬鍙戝竷闃舵锛坄terminal_publish`锛変笌 release completion 闃舵鐙珛鎵ц銆?- 鍓嶇疆渚濊禆锛歚P2-25` + +## P2-27 Linux CI Workflow 璋冨害杩借釜闂ㄧ +- 鍗″彿锛歚P2-27` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-25` 鍙鐩?dispatch 鍛戒护鎵ц鍥炴墽锛屼絾鏈敹鏁?workflow run 鐨勮拷韪绾︼紝CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽鏃ュ織鎻愬彇 `run_id`/`run_url`銆? - 缂哄皯缁熶竴 run poll 鍛戒护涓庤拷韪姸鎬侊紝瀵艰嚧鍚庣画 job 瀵光€滃凡璋冨害浣嗘湭瀹屾垚鈥濈殑鐘舵€佸垽鏂笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch trace gate锛岃鍙?`P2-25` 鎵ц鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_dispatch_trace.json` + - `ci_workflow_dispatch_trace.md` + - 浠?dispatch stdout/stderr 鎻愬彇 canonical GitHub Actions run URL 涓?`run_id`锛岀敓鎴愬彲澶嶇敤 poll 鍛戒护锛坄gh run view --json ...`锛夈€? - 鏀寔 `--poll-now` 绔嬪嵆鎵ц涓€娆?run 鐘舵€佹煡璇紝缁熶竴 `run_tracking_ready` / `run_in_progress` / `run_completed_success` / `run_completed_failure` / `run_poll_failed` 鍒ゅ畾銆? - 鏀寔 `--github-output` 瀵煎嚭杩借釜鐘舵€併€乺un 鏍囪瘑銆乸oll 缁撴灉涓庢姤鍛婅矾寰勶紝渚?CI 涓嬫父鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚dispatched` 涓斿惈 run URL 鏃跺繀椤绘彁鍙?`run_id/run_url` 骞剁敓鎴?poll 鍛戒护銆? - `T2`锛歚dispatched` 浣嗙己 run URL 鏃跺繀椤昏惤鍏?`run_tracking_missing` 涓旇繑鍥炲け璐ラ€€鍑虹爜銆? - `T3`锛歚blocked`/`ready_dry_run` 璺緞蹇呴』淇濇寔鏃?poll 灏濊瘯骞堕€忎紶涓婃父璇箟銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈杩借釜鐘舵€併€乺un_id/run_url銆佹姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-26` +## P2-28 Linux CI Workflow 璋冨害瀹屾垚鏀舵暃闂ㄧ +- 鍗″彿锛歚P2-28` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-27` 浠呮彁渚涘崟娆?trace 涓庡彲閫夊崟娆?poll锛屾湭鎻愪緵鈥滅瓑寰呰嚦缁堟€佲€濈殑缁熶竴鏀舵暃濂戠害锛汣I 涓嬫父浠嶉渶鑷寰幆 `gh run view` 骞跺鐞嗚秴鏃?杩涜涓?澶辫触鍒嗘敮銆? - 缂哄皯缁熶竴 completion verdict锛屽鑷?`run_in_progress`銆乣run_completed_failure`銆乣run_poll_failed`銆乣run_tracking_missing` 鍦ㄤ笉鍚?job 涓€€鍑鸿涔変笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch completion gate锛岃鍙?`P2-27` trace 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_dispatch_completion.json` + - `ci_workflow_dispatch_completion.md` + - 鏀寔杞绛夊緟鍙傛暟锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`锛屽皢 run 鐘舵€佺粺涓€鏀舵暃涓猴細 + - `run_completed_success` + - `run_completed_failure` + - `run_poll_failed` + - `run_await_timeout` + - 浠ュ強閫忎紶鍨?`blocked` / `ready_dry_run` / `run_tracking_missing` + - 鎻愪緵 `--allow-in-progress` 绛栫暐寮€鍏筹細瓒呮椂浠?in-progress 鏃跺彲閫夋槸鍚﹁繑鍥?exit 0锛屾弧瓒充笉鍚?CI 缂栨帓绛栫暐銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€乸oll 杩唬銆乺un_id/url銆佹姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` + - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_tracking_ready` 涓?poll 杩斿洖 completed+success 鏃讹紝蹇呴』鏀舵暃涓?`run_completed_success` 涓?`completion_exit_code=0`銆? - `T2`锛氭寔缁?in-progress 鍒?`max_polls` 鍚庯紝蹇呴』钀?`run_await_timeout`锛屽苟鎸?`--allow-in-progress` 鍐冲畾閫€鍑虹爜銆? - `T3`锛歵race 涓?`poll_command` 涓?`poll_command_parts` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乪xit code銆乸oll 娆℃暟銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-27` + +## P2-29 Linux CI Workflow 缁堝眬鍙戝竷闂ㄧ +- 鍗″彿锛歚P2-29` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-28` 宸茬粰鍑?completion verdict锛屼絾涓嬫父鍙戝竷娴佺▼浠嶉渶鑷灏?`completion_status` 鏄犲皠涓哄彲鍙戝竷璇箟锛坧romote/hold/fail锛夛紝瀵艰嚧鏀跺彛绛栫暐涓嶄竴鑷淬€? - 缂哄皯缁熶竴缁堝眬 publish 濂戠害锛岄毦浠ョǔ瀹氬鐢ㄥ湪 release gate 涓庡悗缁幆澧冩帹骞跨瓥鐣ヤ腑銆?- 鐮斿彂鍔熻兘锛? - 鏂板 terminal publish gate锛岃鍙?`P2-28` completion 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_terminal_publish.json` + - `ci_workflow_terminal_publish.md` + - 灏?completion 璇箟缁熶竴鏀舵暃涓?`publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_promote`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`run_completed_success` 蹇呴』 `completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_completed_success` + `completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `publish_status=passed` 涓?`should_promote=true`銆? - `T2`锛歚run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `publish_status=in_progress` 涓?`publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-28` + +## P2-30 Linux CI Workflow Release Handoff Gate +- 鍗″彿锛歚P2-30` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-29` 宸茬粰鍑?publish verdict锛屼絾 release job 浠嶉渶鑷瑙f瀽 `publish_status/should_promote` 缁勮瑙﹀彂鏉′欢锛屽鑷存帹骞胯涔夊垎鍙夈€? - 缂哄皯缁熶竴 handoff 濂戠害锛屼笉鍒╀簬鍚庣画閮ㄧ讲骞冲彴鎴栧鎵规祦澶嶇敤 Linux CI 缁堝眬缁撹銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release handoff gate锛岃鍙?`P2-29` terminal publish 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_handoff.json` + - `ci_workflow_release_handoff.md` + - 灏?publish 璇箟缁熶竴鏀舵暃涓?`handoff_status`锛坄ready_for_release`/`awaiting_completion`/`blocked`/`failed`/`contract_failed`锛変笌 `should_trigger_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`publish_status=passed` 蹇呴』 `publish_exit_code=0` 涓?`should_promote=true`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--target-environment`銆乣--release-channel` 閫忎紶鍙戝竷鐩爣璇箟锛屽苟鏀寔 `--github-output` 瀵煎嚭 handoff 鐘舵€併€乺elease action銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` + - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚publish_status=passed` + `should_promote=true` 鏃跺繀椤绘敹鏁涗负 `handoff_status=ready_for_release` 涓?`should_trigger_release=true`銆? - `T2`锛歚publish_status=in_progress` 鏃跺繀椤绘敹鏁涗负 `handoff_status=awaiting_completion` 涓?`release_exit_code=0`銆? - `T3`锛歵erminal publish 濂戠害鍐茬獊锛堝 passed 浣?should_promote=false锛夋椂蹇呴』鏀舵暃涓?`handoff_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 handoff 鐘舵€併€乺elease action銆乼arget env/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-29` + +## P2-31 Linux CI Workflow Release Trigger Gate +- 鍗″彿锛歚P2-31` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-30` 宸蹭骇鍑?`should_trigger_release` 璇箟锛屼絾浠嶇己灏戔€滅湡姝f墽琛?release dispatch鈥濈殑缁熶竴鎵ц闂ㄧ锛汣I 涓嬫父闇€鑷鎷艰鍛戒护骞跺鐞嗚秴鏃?澶辫触鍒嗘敮锛岃Е鍙戣涓轰笉涓€鑷淬€? - 缂哄皯缁熶竴 trigger 鎵ц鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 杩借釜銆佸洖婧笌瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trigger gate锛岃鍙?`P2-30` release handoff 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_trigger.json` + - `ci_workflow_release_trigger.md` + - 褰?`should_trigger_release=true` 鏃讹紝鑷姩鏋勫缓骞舵墽琛?release dispatch command锛堟敮鎸佽嚜瀹氫箟 `--release-command` 瑕嗙洊榛樿鍛戒护锛夈€? - 褰?`should_trigger_release=false` 鏃讹紝鎸?handoff 鐘舵€佹敹鏁涗负 `release_trigger_status`锛坄awaiting_completion`/`blocked`/`handoff_failed`锛夛紝骞朵繚鎸侀浂瑙﹀彂鍓綔鐢ㄣ€? - 鍥哄寲濂戠害鏍¢獙锛歨andoff 鐘舵€併€乤ction銆乪xit code 涓庤Е鍙戝竷灏斿€煎繀椤讳竴鑷达紱涓嶄竴鑷村嵆鎷掔粷鎵ц銆? - 鏀寔 `--github-output` 瀵煎嚭 trigger 鐘舵€併€佽Е鍙戝皾璇曘€乪xit code銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` + - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚ready_for_release + should_trigger_release=true` 鍦?dry-run 璺緞蹇呴』鏀舵暃涓?`release_trigger_status=ready_dry_run`銆? - `T2`锛歚awaiting_completion` 璺緞蹇呴』淇濇寔 `release_trigger_status=awaiting_completion` 涓斾笉瑙﹀彂鍛戒护鎵ц銆? - `T3`锛歨andoff 濂戠害鍐茬獊锛堝 `ready_for_release` 浣?`should_trigger_release=false`锛夊繀椤昏鎷掔粷鍔犺浇銆? - `T4`锛氳Е鍙戝懡浠ら潪闆惰繑鍥炲繀椤绘敹鏁涗负 `release_trigger_status=trigger_failed` 骞惰褰曞け璐?reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trigger 鐘舵€併€乤ttempted銆乪xit code銆乪nv/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-30` + +## P2-32 Linux CI Workflow Release Traceability Gate +- 鍗″彿锛歚P2-32` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-31` 宸插畬鎴?release trigger 鎵ц锛屼絾缂哄皯 release run 杩借釜濂戠害锛涗笅娓?job 闇€瑕侀噸澶嶈В鏋?stdout/stderr 鎵嶈兘鎷垮埌 `run_id/run_url`锛岃拷韪涔変笉涓€鑷淬€? - 缂哄皯缁熶竴 release trace 鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 瀹屾垚鎬佹敹鏁涖€佸璁¤拷婧笌璺?job 澶嶇敤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trace gate锛岃鍙?`P2-31` release trigger 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_trace.json` + - `ci_workflow_release_trace.md` + - 褰?`release_trigger_status=triggered` 鏃讹紝缁熶竴鎻愬彇 release run 寮曠敤骞舵敹鏁涗负锛? - `release_run_id` / `release_run_url` + - `release_tracking_status`锛坄release_run_tracking_ready`/`release_run_tracking_missing`锛? - 鍙€?poll 鍛戒护濂戠害锛坄release_poll_command` + `release_poll_command_parts`锛? - 鏀寔 `--poll-now` 绔嬪嵆鏌ヨ release run 鐘舵€侊紝鏀舵暃涓猴細 + - `release_run_in_progress` / `release_run_completed_success` / `release_run_completed_failure` / `release_run_poll_failed` + - 瀵归潪瑙﹀彂鐘舵€侊紙`ready_dry_run` / `awaiting_completion` / `blocked` / `handoff_failed` / `trigger_failed`锛夌粺涓€閫忎紶鎴愭棤鍓綔鐢?trace 璇箟銆? - 鏀寔 `--github-output` 瀵煎嚭 trace 鐘舵€併€乻hould_poll銆乺un_id/url銆乸oll 鐘舵€佷笌鎶ュ憡璺緞銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` + - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚triggered` 鎶ュ憡蹇呴』鎻愬彇鍑?run 寮曠敤锛屽苟鏀舵暃涓?`release_tracking_status=release_run_tracking_ready`銆? - `T2`锛歚triggered` 浣嗙己灏?run 寮曠敤鏃跺繀椤绘敹鏁涗负 `release_tracking_status=release_run_tracking_missing` 涓?`release_trace_exit_code=1`銆? - `T3`锛歚trigger_failed` 璺緞蹇呴』閫忎紶涓?`release_tracking_status=release_trigger_failed` 涓斾笉瑙﹀彂 poll銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trace 鐘舵€併€乻hould_poll銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-31` + +## P2-33 Linux CI Workflow Release Completion Gate +- 鍗″彿锛歚P2-33` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-32` 宸插叿澶?release run trace 濂戠害锛屼絾鈥滅瓑寰?run 瀹屾垚鈥濅粛闇€涓嬫父 job 鑷杞锛岀己灏戠粺涓€瀹屾垚鎬佹敹鏁涖€? - 鏃犵粺涓€ release completion 鎶ュ憡锛屽鑷?release 鎴愬姛/澶辫触/瓒呮椂/杩涜涓殑鏈€缁堣涔夊湪 CI 鍚勯樁娈典笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 release completion gate锛岃鍙?`P2-32` release trace 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_completion.json` + - `ci_workflow_release_completion.md` + - 鏀寔 bounded poll loop锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`銆? - 鏀舵暃 completion_status锛? - `release_run_completed_success` + - `release_run_completed_failure` + - `release_run_in_progress` + - `release_run_await_timeout` + - `release_run_poll_failed` + - 鍙?blocked/dry-run/trigger_failed 绛夐€忎紶鐘舵€併€? - 鏀寔 `--allow-in-progress`锛氬厑璁歌秴鏃朵粛 in-progress 鏃惰繑鍥?exit 0銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€佽疆璇俊鎭€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` + - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_tracking_ready` + poll success 蹇呴』鏀舵暃涓?`release_run_completed_success`銆? - `T2`锛氭寔缁?in-progress 涓旀湭鍏佽 in-progress 蹇呴』鏀舵暃涓?`release_run_await_timeout` 涓旈€€鍑虹爜闈為浂銆? - `T3`锛歳elease poll command 涓?parts 涓嶄竴鑷存椂蹇呴』鎷掔粷鍔犺浇銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乸oll 淇℃伅銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-32` + +## P2-34 Linux CI Workflow Release Terminal Publish Gate +- 鍗″彿锛歚P2-34` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-33` 宸茬粰鍑?release completion verdict锛屼絾涓嬫父 release 鏈€缁堟敹鍙d粛闇€鑷瑙f瀽 completion 璇箟锛屽鏄撲骇鐢?finalize/hold/fail 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release terminal publish 濂戠害锛屼笉鍒╀簬鍚庣画 release governance 涓庣幆澧冩帹杩涚瓥鐣ュ鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal publish gate锛岃鍙?`P2-33` release completion 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_terminal_publish.json` + - `ci_workflow_release_terminal_publish.md` + - 灏?release completion 璇箟缁熶竴鏀舵暃涓?`release_publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_finalize_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_run_completed_success` 蹇呴』 `release_completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚夸簬 Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_completed_success` + `release_completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=passed` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=in_progress` 涓?`release_publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-33` + +## P2-35 Linux CI Workflow Release Finalization Gate +- 鍗″彿锛歚P2-35` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-34` 宸茶緭鍑?release terminal publish verdict锛屼絾鈥滄渶缁堟槸鍚︽敹鍙?release 鐢熷懡鍛ㄦ湡鈥濈殑绛栫暐浠嶅垎鏁e湪涓嬫父 job锛屽鏄撳嚭鐜?finalize/hold/abort 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 finalization 濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈电粰鍑虹ǔ瀹氱殑 release closure 缁撴灉涓庡彲瀹¤鍑哄彛銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release finalization gate锛岃鍙?`P2-34` release terminal publish 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_finalization.json` + - `ci_workflow_release_finalization.md` + - 灏?release terminal publish 璇箟缁熶竴鏀舵暃涓猴細 + - `release_finalization_status`锛坄finalized` / `awaiting_release` / `failed` / `contract_failed`锛? - `release_finalization_decision`锛坄finalize` / `hold` / `abort`锛? - `release_finalization_exit_code` + - 鏀寔 `--on-hold` 绛栫暐锛坄pass`/`fail`锛夋帶鍒?blocked/in_progress 鏀跺彛閫€鍑虹爜璇箟銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_publish_status=passed` 蹇呴』 `release_publish_exit_code=0` 涓?`should_finalize_release=true`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` + - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_publish_status=passed` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_finalization_status=finalized` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_publish_status=in_progress` 涓?`--on-hold=fail` 鏃跺繀椤绘敹鏁涗负 `release_finalization_status=awaiting_release` 涓旈€€鍑虹爜闈?0銆? - `T3`锛歵erminal publish 濂戠害涓嶄竴鑷达紙濡?passed 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_finalization_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-34` + +## P2-36 Linux CI Workflow Release Closure Publish Gate +- 鍗″彿锛歚P2-36` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-35` 宸茶緭鍑?finalization verdict锛屼絾瀵瑰娑堣垂锛堥€氱煡銆佸綊妗c€佷笅娓哥郴缁熸帴鍏ワ級浠嶉渶鑷鎷兼帴瀛楁锛宑losure 璇箟瀹规槗鍑虹幇 close/hold/rollback 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release closure 鍙戝竷濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈靛澶栨彁渚涚ǔ瀹氬崟鍑哄彛锛坢achine-readable + human-readable锛夌粨鏋溿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release closure gate锛岃鍙?`P2-35` release finalization 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_closure.json` + - `ci_workflow_release_closure.md` + - 灏?finalization 璇箟缁熶竴鏀舵暃涓猴細 + - `release_closure_status`锛坄closed` / `pending` / `failed` / `contract_failed`锛? - `release_closure_decision`锛坄ship` / `hold` / `rollback`锛? - `release_closure_exit_code` + - `should_close_release` 涓?`should_notify` + - 鍥哄寲 finalization 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_finalization_status=finalized` 蹇呴』 `release_finalization_decision=finalize` 涓?`release_finalization_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_finalization_status=finalized` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_closure_status=closed` 涓?`release_closure_decision=ship`銆? - `T2`锛歚release_finalization_status=awaiting_release` 鏃跺繀椤绘敹鏁涗负 `release_closure_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歠inalization 濂戠害涓嶄竴鑷达紙濡?finalized 浣?decision!=finalize锛夋椂蹇呴』鏀舵暃涓?`release_closure_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-35` + +## P2-37 Linux CI Workflow Release Archive Publish Gate +- 鍗″彿锛歚P2-37` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-36` 宸茶緭鍑?release closure verdict锛屼絾 release evidence锛坈losure/finalization/trace/dispatch source artifacts锛変粛鏈舰鎴愮粺涓€ machine-readable 褰掓。濂戠害銆? - 缂哄皯缁堢偣褰掓。鍗曞嚭鍙f椂锛孡inux CI 涓嬫父锛堥€氱煡銆佸璁°€佸綊妗g郴缁燂級浠嶉渶鑷鎷兼帴璺緞涓庡垽瀹氾紝瀛樺湪鍙戝竷缁撹涓庤瘉鎹紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release archive gate锛岃鍙?`P2-36` release closure 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_archive.json` + - `ci_workflow_release_archive.md` + - 灏?closure 璇箟缁熶竴鏀舵暃涓猴細 + - `release_archive_status`锛坄ready` / `pending` / `failed` / `contract_failed`锛? - `release_archive_decision`锛坄publish` / `hold` / `block`锛? - `release_archive_exit_code` + - `should_publish_archive` + - 鏋勫缓 evidence manifest锛堝寘鍚?closure 涓庡叾涓婃父 source_* 鎶ュ憡璺緞瀛樺湪鎬э級锛屽彂鐜扮己澶卞嵆寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 archive 鐘舵€併€佸喅绛栥€乵issing artifacts 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_closure_status=closed` 涓?evidence 瀹屾暣鏃跺繀椤绘敹鏁涗负 `release_archive_status=ready` 涓?`release_archive_decision=publish`銆? - `T2`锛歚release_closure_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_archive_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歝losure 濂戠害涓嶄竴鑷达紙濡?closed 浣?decision!=ship锛夋椂蹇呴』鏀舵暃涓?`release_archive_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 archive 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-36` + +## P2-38 Linux CI Workflow Release Verdict Publish Gate +- 鍗″彿锛歚P2-38` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-37` 宸茶緭鍑?release archive verdict锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄槸鍚︽斁琛屽彂甯冣€濈殑鍗曞嚭鍙e垽璇嶏紝缁堟€?ship/hold/block 璇箟鍒嗘暎鍦ㄥ瀛楁涓€? - 缂哄皯鏈€缁?verdict 濂戠害鏃讹紝涓嬫父娴佹按绾夸笌鍙戝竷鐪嬫澘浠嶉渶鑷鎷艰鍒ゅ畾閫昏緫锛屽瓨鍦ㄥ嚭鍙h涔夋紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release verdict gate锛岃鍙?`P2-37` release archive 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_verdict.json` + - `ci_workflow_release_verdict.md` + - 灏?archive 璇箟缁熶竴鏀舵暃涓猴細 + - `release_verdict_status`锛坄published` / `awaiting_archive` / `blocked` / `contract_failed`锛? - `release_verdict_decision`锛坄ship` / `hold` / `block`锛? - `release_verdict_exit_code` + - `should_ship_release` 涓?`should_open_incident` + - 鍥哄寲 archive 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_archive_status=ready` 蹇呴』 `release_archive_decision=publish` 涓?`release_archive_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 浜屾妫€鏌?evidence manifest锛屽彂鐜扮己澶辨椂寮哄埗 `contract_failed` 骞惰ˉ鍏?reason code銆? - 鏀寔 `--github-output` 瀵煎嚭 verdict 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_archive_status=ready` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_verdict_status=published` 涓?`release_verdict_decision=ship`銆? - `T2`锛歚release_archive_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_verdict_status=awaiting_archive` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歛rchive 濂戠害涓嶄竴鑷达紙濡?ready 浣?decision!=publish锛夋椂蹇呴』鏀舵暃涓?`release_verdict_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 verdict 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-37` + +## P2-39 Linux CI Workflow Release Incident Dispatch Gate +- 鍗″彿锛歚P2-39` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-38` 宸茶緭鍑?release verdict 涓?`should_open_incident` 璇箟锛屼絾 Linux CI 浠嶇己灏戠粺涓€鈥滀綍鏃?濡備綍瑙﹀彂 incident鈥濈殑鎵ц濂戠害锛屼笅娓搁渶瑕佽嚜琛屾嫾鎺ュ懡浠や笌澶辫触璇箟銆? - 缂哄皯 incident dispatch 鍗曞嚭鍙f椂锛岀粓鐐瑰憡璀﹂摼璺紙issue/incident system锛夊鏄撳嚭鐜拌Е鍙戞紡鎶ャ€侀噸澶嶈Е鍙戞垨澶辫触涓嶅彲瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release incident gate锛岃鍙?`P2-38` release verdict 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_incident.json` + - `ci_workflow_release_incident.md` + - 灏?verdict 璇箟缁熶竴鏀舵暃涓猴細 + - `incident_dispatch_status`锛坄not_required` / `ready_dry_run` / `dispatched` / `dispatch_failed` / `contract_failed`锛? - `should_dispatch_incident` + - `incident_dispatch_exit_code` + - 鍥哄寲 verdict 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_verdict_status=published` 蹇呴』 `should_open_incident=false`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 auto incident command锛堥粯璁?`gh issue create`锛変笌 `--incident-command` 瑕嗙洊锛屽け璐ユ椂缁熶竴鏀舵暃 error reason銆? - 鏀寔 `--github-output` 瀵煎嚭 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` + - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`incident_dispatch_status=not_required`銆? - `T2`锛歚release_verdict_status=blocked` 涓?`should_open_incident=true` 鏃讹紝dry-run 蹇呴』鏀舵暃涓?`incident_dispatch_status=ready_dry_run`銆? - `T3`锛歷erdict 濂戠害涓嶄竴鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`incident_dispatch_status=contract_failed`銆? - `T4`锛歩ncident command 澶辫触鏃跺繀椤绘敹鏁涗负 `incident_dispatch_status=dispatch_failed` 骞惰ˉ鍏?failure reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-38` +## P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate +- 鍗″彿锛歚P2-40` +- 浼樺厛绾э細`P2` +- 瑙e喅闂锛? - `P2-39` 宸茶緭鍑?incident dispatch 濂戠害锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄渶缁堝彂甯冨垽璇嶁€濇敹鍙o紱涓嬫父浠嶉渶鑷鎷兼帴 ship/hold/escalate/block 璇箟銆? - 缂哄皯 terminal verdict 鍗曞嚭鍙f椂锛宺elease 缁堝眬鐪嬫澘涓庤嚜鍔ㄥ寲闂幆锛堥€氱煡銆佷汉宸ユ帴绠°€侀樆鏂級鏄撳嚭鐜拌涔夋紓绉讳笌閲嶅鍒ゅ畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal verdict gate锛岃鍙?`P2-39` release incident 鎶ュ憡骞惰緭鍑猴細 + - `ci_workflow_release_terminal_verdict.json` + - `ci_workflow_release_terminal_verdict.md` + - 灏?release+incident 璇箟缁熶竴鏀舵暃涓猴細 + - `release_terminal_verdict_status`锛坄released` / `awaiting_archive` / `blocked_incident_ready_dry_run` / `blocked_incident_dispatched` / `blocked_incident_failed` / `blocked` / `contract_failed`锛? - `release_terminal_verdict_decision`锛坄ship` / `hold` / `escalate` / `block`锛? - `release_terminal_verdict_exit_code` + - `terminal_should_ship_release` + - `terminal_requires_follow_up` + - 鍥哄寲 `P2-38` + `P2-39` 濂戠害涓€鑷存€ф牎楠岋紙渚嬪 `release_verdict_status=published` 蹇呴』 `should_open_incident=false` 涓?`incident_dispatch_status=not_required`锛夛紝涓嶄竴鑷村己鍒?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + `incident_dispatch_status=not_required` 鏃跺繀椤绘敹鏁涗负 `release_terminal_verdict_status=released`銆? - `T2`锛歚release_verdict_status=blocked` + `incident_dispatch_status=ready_dry_run` 鏃讹紝蹇呴』鏀舵暃涓?`release_terminal_verdict_status=blocked_incident_ready_dry_run` 涓?decision=`escalate`銆? - `T3`锛氬绾︿笉涓€鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed`銆? - `T4`锛氳瘉鎹己澶辨椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed` 骞惰ˉ鍏?missing artifact reason銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-39` + +## P2-41 Linux CI Workflow Release Delivery Closure Gate +- ???`P2-41` +- ????`P2` +- ????? + - `P2-40` ??? terminal verdict ???? Linux CI ?????????????????????????????????? + - ?? delivery closure gate ??????? ship/hold/escalate/block ???????????????????????????? +- ????? + - ?? release delivery gate??? `P2-40` release terminal verdict ?????? + - `ci_workflow_release_delivery.json` + - `ci_workflow_release_delivery.md` + - ? terminal verdict + incident ???????? + - `release_delivery_status`?`shipped` / `pending_follow_up` / `blocked_incident` / `blocked_incident_failed` / `blocked` / `contract_failed`? + - `release_delivery_decision`?`deliver` / `hold` / `escalate` / `block`? + - `release_delivery_exit_code` + - `delivery_should_ship_release` + - `delivery_requires_human_action` + - `delivery_should_announce_blocker` + - ?? `P2-40` ?????????? `release_terminal_verdict_status=released` ?? `incident_dispatch_status=not_required`??????? `contract_failed`? + - ?? `--github-output` ?? delivery ??????exit code??????????incident ??/url?run_id/url ?????? +- ????? + - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- ??????? + - `T1`?`release_terminal_verdict_status=released` + `incident_dispatch_status=not_required` ?????? `release_delivery_status=shipped` ? decision=`deliver`? + - `T2`?`release_terminal_verdict_status=blocked_incident_ready_dry_run` ?????? `release_delivery_status=blocked_incident` ? decision=`escalate`? + - `T3`???????? `released` ? `incident_dispatch_status=ready_dry_run`??????? `release_delivery_status=contract_failed`? + - `T4`??????????? `release_delivery_status=contract_failed` ??? missing artifact reason? + - `T5`?GitHub output ?????? delivery ??????exit code????????incident ??/url?run_id/url ?????? +- ?????`P2-40` + + +## P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate +- Card ID: `P2-42` +- Priority: `P2` +- Problem: + - `P2-41` converges to delivery semantics, but Linux CI still lacks one terminal publish contract for downstream notification/hand-off consumers. + - Without one terminal publish output, follow-up automation and human takeover paths must re-interpret delivery fields independently. +- Scope: + - Add terminal publish gate that consumes `P2-41` delivery report and emits: + - `ci_workflow_release_delivery_terminal_publish.json` + - `ci_workflow_release_delivery_terminal_publish.md` + - Converge to normalized outputs: + - `release_delivery_terminal_publish_status` (`published`/`pending_follow_up`/`blocked`/`contract_failed`) + - `release_delivery_terminal_publish_decision` (`announce_release`/`announce_hold`/`announce_blocker`/`abort_publish`) + - `release_delivery_terminal_publish_exit_code` + - `terminal_publish_should_notify` / `terminal_publish_should_create_follow_up` / `terminal_publish_channel` + - Export `--github-output` fields for Linux CI terminal job consumption. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_status=shipped` converges to `published` + `announce_release`. + - `T2`: `release_delivery_status=blocked_incident` converges to `blocked` + `announce_blocker`. + - `T3`: delivery/incident contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/channel/run_id/run_url. +- Depends on: `P2-41` + +## P2-43 Linux CI Workflow Release Delivery Final Verdict Gate +- Card ID: `P2-43` +- Priority: `P2` +- Problem: + - `P2-42` converges release delivery to terminal publish semantics, but Linux CI still lacks one final closure verdict contract for release close/follow-up/escalation takeover. + - Without one final verdict output, downstream consumers must re-interpret terminal publish + delivery + incident fields independently. +- Scope: + - Add final verdict gate that consumes `P2-42` delivery terminal publish report and emits: + - `ci_workflow_release_delivery_final_verdict.json` + - `ci_workflow_release_delivery_final_verdict.md` + - Converge to normalized outputs: + - `release_delivery_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) + - `release_delivery_final_verdict_decision` (`close_release`/`open_follow_up`/`escalate_blocker`/`abort_close`) + - `release_delivery_final_verdict_exit_code` + - `final_should_close_release` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` + - Export `--github-output` fields for Linux CI terminal closeout job consumption. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_terminal_publish_status=published` converges to `completed` + `close_release`. + - `T2`: `release_delivery_terminal_publish_status=pending_follow_up` converges to `requires_follow_up` + `open_follow_up`. + - `T3`: terminal publish / delivery / incident contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/final-action booleans/announcement_target/run_id/run_url. +- Depends on: `P2-42` + +## P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate +- Card ID: `P2-44` +- Priority: `P2` +- Problem: + - `P2-43` converges release delivery to final verdict semantics, but Linux CI still lacks one terminal follow-up dispatch contract for queue/escalation takeover consumers. + - Without one follow-up dispatch output, downstream automation must re-interpret final verdict + incident + publish fields independently. +- Scope: + - Add follow-up dispatch gate that consumes `P2-43` delivery final verdict report and emits: + - `ci_workflow_release_follow_up_dispatch.json` + - `ci_workflow_release_follow_up_dispatch.md` + - Converge to normalized outputs: + - `release_follow_up_dispatch_status` (`closed`/`follow_up_required`/`escalated`/`contract_failed`) + - `release_follow_up_dispatch_decision` (`no_action`/`dispatch_follow_up`/`dispatch_escalation`/`abort_dispatch`) + - `release_follow_up_dispatch_exit_code` + - `follow_up_required` / `escalation_required` / `dispatch_target` + - Export `--github-output` fields for Linux CI final hand-off consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_delivery_final_verdict_status=completed` converges to `closed` + `no_action`. + - `T2`: `release_delivery_final_verdict_status=requires_follow_up` converges to `follow_up_required` + `dispatch_follow_up`. + - `T3`: final-verdict contract mismatch converges to `contract_failed`. + - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. + - `T5`: GitHub output includes status/decision/exit_code/follow-up/escalation booleans/dispatch_target/run_id/run_url. +- Depends on: `P2-43` + +## P2-45 Linux CI Workflow Release Follow-Up Closure Gate +- Card ID: `P2-45` +- Priority: `P2` +- Problem: + - `P2-44` converges release delivery final verdict to follow-up dispatch semantics, but Linux CI still lacks one terminal follow-up closure contract for backlog ownership/queue intake consumers. + - Without one follow-up closure output, downstream automation must re-interpret dispatch status + target + escalation flags and cannot safely distinguish dry-run readiness vs actual queue success. +- Scope: + - Add follow-up closure gate that consumes `P2-44` release follow-up dispatch report and emits: + - `ci_workflow_release_follow_up_closure.json` + - `ci_workflow_release_follow_up_closure.md` + - Converge to normalized outputs: + - `release_follow_up_closure_status` (`closed`/`queued_dry_run`/`queued`/`queue_failed`/`contract_failed`) + - `release_follow_up_closure_decision` (`no_action`/`queue_follow_up`/`queue_escalation`/`abort_queue`) + - `release_follow_up_closure_exit_code` + - `should_queue_follow_up` / `follow_up_queue_attempted` / `follow_up_task_queued` / `escalation_task_queued` + - Export `--github-output` fields for Linux CI terminal follow-up hand-off consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_dispatch_status=closed` converges to `closed` + `no_action`. + - `T2`: `release_follow_up_dispatch_status=follow_up_required` converges to `queued_dry_run` + `queue_follow_up` in dry-run mode. + - `T3`: follow-up dispatch contract mismatch converges to `contract_failed`. + - `T4`: executable follow-up queue command failure converges to `queue_failed`. + - `T5`: GitHub output includes status/decision/exit_code/queue booleans/target/queue_url/run_id/run_url. +- Depends on: `P2-44` + +## P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate +- Card ID: `P2-46` +- Priority: `P2` +- Problem: + - `P2-45` converges follow-up dispatch to closure semantics, but Linux CI still lacks one terminal publish contract that cleanly separates `closed`/`queued`/`pending_queue`/`queue_failed` outcomes for downstream notification and hand-off jobs. + - Without one terminal publish output, downstream automation must re-interpret closure status + queue attempt fields and cannot consume a single stable final follow-up signal. +- Scope: + - Add follow-up terminal publish gate that consumes `P2-45` release follow-up closure report and emits: + - `ci_workflow_release_follow_up_terminal_publish.json` + - `ci_workflow_release_follow_up_terminal_publish.md` + - Converge to normalized outputs: + - `release_follow_up_terminal_publish_status` (`published`/`pending_queue`/`queue_failed`/`contract_failed`) + - `release_follow_up_terminal_publish_decision` (`announce_closed`/`announce_queued`/`announce_pending_queue`/`announce_queue_failure`/`abort_publish`) + - `release_follow_up_terminal_publish_exit_code` + - `follow_up_terminal_publish_should_notify` / `follow_up_terminal_requires_manual_action` / `follow_up_terminal_publish_channel` + - Export `--github-output` fields for Linux CI terminal follow-up publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_closure_status=closed` converges to `published` + `announce_closed`. + - `T2`: `release_follow_up_closure_status=queued_dry_run` converges to `pending_queue` + `announce_pending_queue`. + - `T3`: closure contract mismatch converges to `contract_failed`. + - `T4`: `release_follow_up_closure_status=queue_failed` converges to `queue_failed` + `announce_queue_failure`. + - `T5`: GitHub output includes status/decision/exit_code/notify/manual-action/channel/closure-status/queue-url/run_id/run_url. +- Depends on: `P2-45` + +## P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate +- Card ID: `P2-47` +- Priority: `P2` +- Problem: + - `P2-46` converges follow-up closure to terminal publish semantics, but Linux CI still lacks one final verdict contract that cleanly separates `completed`/`requires_follow_up`/`blocked`/`contract_failed` outcomes for post-release ownership and escalation consumers. + - Without one final verdict output, downstream automation must re-interpret terminal publish + closure + queue fields and cannot consume one stable close/open/escalate signal. +- Scope: + - Add follow-up final verdict gate that consumes `P2-46` release follow-up terminal publish report and emits: + - `ci_workflow_release_follow_up_final_verdict.json` + - `ci_workflow_release_follow_up_final_verdict.md` + - Converge to normalized outputs: + - `release_follow_up_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) + - `release_follow_up_final_verdict_decision` (`close_follow_up`/`keep_follow_up_open`/`escalate_queue_failure`/`abort_close`) + - `release_follow_up_final_verdict_exit_code` + - `final_should_close_follow_up` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` + - Export `--github-output` fields for Linux CI terminal follow-up ownership consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: `release_follow_up_terminal_publish_status=published` + `announce_closed` converges to `completed` + `close_follow_up`. + - `T2`: `release_follow_up_terminal_publish_status=pending_queue` converges to `requires_follow_up` + `keep_follow_up_open`. + - `T3`: `release_follow_up_terminal_publish_status=queue_failed` converges to `blocked` + `escalate_queue_failure`. + - `T4`: terminal publish / closure contract mismatch converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/final booleans/announcement-target/terminal-channel/queue-url/run_id/run_url. +- Depends on: `P2-46` + +## P2-48 Linux CI Workflow Release Final Outcome Gate +- Card ID: `P2-48` +- Priority: `P2` +- Problem: + - `P2-43` delivers release final verdict semantics and `P2-47` delivers follow-up final verdict semantics, but Linux CI still lacks one single terminal outcome contract that downstream release governance and notification consumers can consume without re-stitching two reports. + - Without one final outcome output, downstream automation must merge delivery/follow-up statuses itself and risks drift on ship-with-follow-up vs blocked escalation semantics. +- Scope: + - Add release final outcome gate that consumes `P2-43` release delivery final verdict report and `P2-47` release follow-up final verdict report and emits: + - `ci_workflow_release_final_outcome.json` + - `ci_workflow_release_final_outcome.md` + - Converge to normalized outputs: + - `release_final_outcome_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_outcome_decision` (`ship_and_close`/`ship_with_follow_up_open`/`escalate_blocker`/`abort_outcome`) + - `release_final_outcome_exit_code` + - `final_should_ship_release` / `final_follow_up_open` / `final_should_page_owner` / `final_outcome_target` + - Export `--github-output` fields for Linux CI terminal release ownership consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: delivery=`completed` + follow-up=`completed` converges to `released` + `ship_and_close`. + - `T2`: delivery=`completed` + follow-up=`requires_follow_up` converges to `released_with_follow_up` + `ship_with_follow_up_open`. + - `T3`: delivery or follow-up=`blocked` converges to `blocked` + `escalate_blocker`. + - `T4`: delivery/follow-up contract mismatch (decision/flags/run-id) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/ship+follow-up flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-43`, `P2-47` + +## P2-49 Linux CI Workflow Release Final Terminal Publish Gate +- Card ID: `P2-49` +- Priority: `P2` +- Problem: + - `P2-48` already converges one final release outcome artifact, but Linux CI still lacks one terminal publish contract dedicated to final stakeholder-facing announcement semantics. + - Without a dedicated terminal publish gate, downstream consumers still need custom mapping from outcome states to channel/action semantics, increasing drift risk on `released_with_follow_up` vs `blocked` paths. +- Scope: + - Add release final terminal publish gate that consumes `P2-48` release final outcome report and emits: + - `ci_workflow_release_final_terminal_publish.json` + - `ci_workflow_release_final_terminal_publish.md` + - Converge to normalized outputs: + - `release_final_terminal_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_terminal_publish_decision` (`announce_release_closure`/`announce_release_with_follow_up`/`announce_blocker`/`abort_publish`) + - `release_final_terminal_publish_exit_code` + - `final_terminal_should_notify` / `final_terminal_requires_manual_action` / `final_terminal_channel` + - Export `--github-output` fields for Linux CI terminal publish/notification consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final outcome=`released` converges to `published` + `announce_release_closure`. + - `T2`: final outcome=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_with_follow_up`. + - `T3`: final outcome=`blocked` converges to `blocked` + `announce_blocker`. + - `T4`: final outcome contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/run-id/run-url/report paths. +- Depends on: `P2-48` + +## P2-50 Linux CI Workflow Release Final Handoff Gate +- Card ID: `P2-50` +- Priority: `P2` +- Problem: + - `P2-49` already converges final terminal publish semantics, but Linux CI still lacks one terminal handoff contract that downstream closure/archive consumers can consume directly without remapping publish statuses. + - Without this dedicated handoff gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/handoff ownership semantics. +- Scope: + - Add release final handoff gate that consumes `P2-49` release final terminal publish report and emits: + - `ci_workflow_release_final_handoff.json` + - `ci_workflow_release_final_handoff.md` + - Converge to normalized outputs: + - `release_final_handoff_status` (`completed`/`completed_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_handoff_decision` (`handoff_release_closure`/`handoff_release_with_follow_up`/`handoff_blocker`/`abort_handoff`) + - `release_final_handoff_exit_code` + - `final_handoff_should_archive_release` / `final_handoff_keep_follow_up_open` / `final_handoff_should_page_owner` / `final_handoff_target` + - Export `--github-output` fields for Linux CI terminal handoff/closure consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final terminal publish=`published` converges to `completed` + `handoff_release_closure`. + - `T2`: final terminal publish=`published_with_follow_up` converges to `completed_with_follow_up` + `handoff_release_with_follow_up`. + - `T3`: final terminal publish=`blocked` converges to `blocked` + `handoff_blocker`. + - `T4`: final terminal publish contract mismatch (decision/flags/channel/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/archive+follow-up+page flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-49` + +## P2-51 Linux CI Workflow Release Final Closure Gate +- Card ID: `P2-51` +- Priority: `P2` +- Problem: + - `P2-50` already converges final handoff semantics, but Linux CI still lacks one terminal closure contract that downstream owner-closure consumers can consume directly without remapping handoff statuses. + - Without this dedicated closure gate, downstream automation still repeats custom logic for `completed_with_follow_up` versus `blocked` paths, increasing drift risk on final close ownership semantics. +- Scope: + - Add release final closure gate that consumes `P2-50` release final handoff report and emits: + - `ci_workflow_release_final_closure.json` + - `ci_workflow_release_final_closure.md` + - Converge to normalized outputs: + - `release_final_closure_status` (`closed`/`closed_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_closure_decision` (`close_release`/`close_with_follow_up`/`close_blocker`/`abort_close`) + - `release_final_closure_exit_code` + - `final_closure_is_closed` / `final_closure_has_open_follow_up` / `final_closure_should_page_owner` / `final_closure_target` + - Export `--github-output` fields for Linux CI terminal closure consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final handoff=`completed` converges to `closed` + `close_release`. + - `T2`: final handoff=`completed_with_follow_up` converges to `closed_with_follow_up` + `close_with_follow_up`. + - `T3`: final handoff=`blocked` converges to `blocked` + `close_blocker`. + - `T4`: final handoff contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/closed+follow-up+page flags/target/queue-url/run-id/run-url/report paths. +- Depends on: `P2-50` + +## P2-52 Linux CI Workflow Release Final Closure Publish Gate +- Card ID: `P2-52` +- Priority: `P2` +- Problem: + - `P2-51` already converges final closure semantics, but Linux CI still lacks one terminal publish contract dedicated to final closure announcement semantics for downstream consumers. + - Without this dedicated closure-publish gate, downstream automation still repeats custom logic for `closed_with_follow_up` versus `blocked` paths, increasing drift risk on final announce/manual-action semantics. +- Scope: + - Add release final closure publish gate that consumes `P2-51` release final closure report and emits: + - `ci_workflow_release_final_closure_publish.json` + - `ci_workflow_release_final_closure_publish.md` + - Converge to normalized outputs: + - `release_final_closure_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_closure_publish_decision` (`announce_release_closed`/`announce_release_closed_with_follow_up`/`announce_release_blocker`/`abort_publish`) + - `release_final_closure_publish_exit_code` + - `final_closure_publish_should_notify` / `final_closure_publish_requires_manual_action` / `final_closure_publish_channel` + - Export `--github-output` fields for Linux CI final closure publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final closure=`closed` converges to `published` + `announce_release_closed`. + - `T2`: final closure=`closed_with_follow_up` converges to `published_with_follow_up` + `announce_release_closed_with_follow_up`. + - `T3`: final closure=`blocked` converges to `blocked` + `announce_release_blocker`. + - `T4`: final closure contract mismatch (decision/flags/target) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-51` + +## P2-53 Linux CI Workflow Release Final Archive Gate +- Card ID: `P2-53` +- Priority: `P2` +- Problem: + - `P2-52` already converges terminal closure publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. + - Without this dedicated final-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics. +- Scope: + - Add release final archive gate that consumes `P2-52` release final closure publish report and emits: + - `ci_workflow_release_final_archive.json` + - `ci_workflow_release_final_archive.md` + - Converge to normalized outputs: + - `release_final_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_archive_decision` (`archive_release_closed`/`archive_release_closed_with_follow_up`/`archive_release_blocker`/`abort_archive`) + - `release_final_archive_exit_code` + - `final_archive_should_publish` / `final_archive_requires_manual_action` / `final_archive_channel` + - Export `--github-output` fields for Linux CI final archive consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final closure publish=`published` converges to `archived` + `archive_release_closed`. + - `T2`: final closure publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_closed_with_follow_up`. + - `T3`: final closure publish=`blocked` converges to `blocked` + `archive_release_blocker`. + - `T4`: final closure publish contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/publish+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-52` + +## P2-54 Linux CI Workflow Release Final Verdict Gate +- Card ID: `P2-54` +- Priority: `P2` +- Problem: + - `P2-53` already converges final archive semantics, but Linux CI still lacks one terminal final-verdict contract that downstream release decision consumers can consume directly without remapping archive statuses. + - Without this dedicated final-verdict gate, downstream automation still repeats custom logic for `archived_with_follow_up` versus `blocked` paths, increasing drift risk on ship/manual-action semantics. +- Scope: + - Add release final verdict gate that consumes `P2-53` release final archive report and emits: + - `ci_workflow_release_final_verdict.json` + - `ci_workflow_release_final_verdict.md` + - Converge to normalized outputs: + - `release_final_verdict_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_verdict_decision` (`ship_release`/`ship_release_with_follow_up`/`escalate_release_blocker`/`abort_verdict`) + - `release_final_verdict_exit_code` + - `final_verdict_should_ship` / `final_verdict_requires_manual_action` / `final_verdict_channel` + - Export `--github-output` fields for Linux CI final verdict consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final archive=`archived` converges to `released` + `ship_release`. + - `T2`: final archive=`archived_with_follow_up` converges to `released_with_follow_up` + `ship_release_with_follow_up`. + - `T3`: final archive=`blocked` converges to `blocked` + `escalate_release_blocker`. + - `T4`: final archive contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/ship+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-53` + +## P2-55 Linux CI Workflow Release Final Verdict Publish Gate +- Card ID: `P2-55` +- Priority: `P2` +- Problem: + - `P2-54` already converges final verdict semantics, but Linux CI still lacks one terminal publish contract that downstream notification/audit consumers can consume directly without remapping verdict statuses. + - Without this dedicated final-verdict-publish gate, downstream automation still repeats custom logic for `released_with_follow_up` versus `blocked` paths, increasing drift risk on notify/manual-action semantics. +- Scope: + - Add release final verdict publish gate that consumes `P2-54` release final verdict report and emits: + - `ci_workflow_release_final_verdict_publish.json` + - `ci_workflow_release_final_verdict_publish.md` + - Converge to normalized outputs: + - `release_final_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_verdict_publish_decision` (`announce_release_shipped`/`announce_release_shipped_with_follow_up`/`announce_release_blocker`/`abort_publish`) + - `release_final_verdict_publish_exit_code` + - `final_verdict_publish_should_notify` / `final_verdict_publish_requires_manual_action` / `final_verdict_publish_channel` + - Export `--github-output` fields for Linux CI final verdict publish consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final verdict=`released` converges to `published` + `announce_release_shipped`. + - `T2`: final verdict=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_shipped_with_follow_up`. + - `T3`: final verdict=`blocked` converges to `blocked` + `announce_release_blocker`. + - `T4`: final verdict contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-54` + +## P2-56 Linux CI Workflow Release Final Publish Archive Gate +- Card ID: `P2-56` +- Priority: `P2` +- Problem: + - `P2-55` already converges final verdict publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. + - Without this dedicated final-publish-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics. +- Scope: + - Add release final publish archive gate that consumes `P2-55` release final verdict publish report and emits: + - `ci_workflow_release_final_publish_archive.json` + - `ci_workflow_release_final_publish_archive.md` + - Converge to normalized outputs: + - `release_final_publish_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) + - `release_final_publish_archive_decision` (`archive_release_shipped`/`archive_release_shipped_with_follow_up`/`archive_release_blocker`/`abort_archive`) + - `release_final_publish_archive_exit_code` + - `final_publish_archive_should_archive` / `final_publish_archive_requires_manual_action` / `final_publish_archive_channel` + - Export `--github-output` fields for Linux CI final publish archive consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` + - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: final verdict publish=`published` converges to `archived` + `archive_release_shipped`. + - `T2`: final verdict publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_shipped_with_follow_up`. + - `T3`: final verdict publish=`blocked` converges to `blocked` + `archive_release_blocker`. + - `T4`: final verdict publish contract mismatch (decision/flags/channel) converges to `contract_failed`. + - `T5`: GitHub output includes status/decision/exit_code/archive+manual-action/channel/queue-url/run-id/run-url/report paths. +- Depends on: `P2-55` + +## P2-57 Linux Gate Manifest Drift Closure Gate +- Card ID: `P2-57` +- Priority: `P2` +- Problem: + - As `P2-xx` gate cards keep growing, there is no single guard that continuously verifies closure across gate scripts, runtime contract tests, and Linux unified manifest wiring. + - Without this closure gate, future card additions can silently miss `test_*_gate_runtime.py` or omit Linux manifest entries, only failing late in Linux stage. +- Scope: + - Add a dedicated drift-closure gate that scans: + - `scripts/run_*_gate.py` (excluding `run_linux_unified_gate.py`) + - expected `tests/test_*_gate_runtime.py` + - `scripts/run_linux_unified_gate.py::LINUX_UNIFIED_TEST_FILES` + - Emit normalized drift report artifacts: + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.md` + - Export `--github-output` fields for CI observability: + - status + missing-runtime-tests count + missing-manifest count + orphan-manifest count. +- Implementation: + - `scripts/run_p2_linux_gate_manifest_drift_gate.py` + - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: gate script scan includes `run_p2_linux_gate_manifest_drift_gate.py` and excludes `run_linux_unified_gate.py`. + - `T2`: expected runtime test mapping follows `run_xxx_gate.py` -> `test_xxx_gate_runtime.py`. + - `T3`: repository baseline report converges to `status=passed` with zero missing runtime tests / manifest entries / orphan manifest entries. + - `T4`: GitHub output fields expose status and drift counters. +- Depends on: `P2-56` + +## P2-59 Linux CI Workflow Terminal Verdict Closure Gate +- Card ID: `P2-59` +- Priority: `P2` +- Problem: + - `P2-56` and `P2-57` are both terminal quality signals, but Linux CI still lacks one single contract that downstream automation can consume as a final go/no-go verdict. + - Without a unified terminal verdict, Linux validation entry and release control logic must duplicate merge rules across two artifacts, increasing drift risk. +- Scope: + - Add one terminal verdict gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json` (`P2-56`) + - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` (`P2-57`) + - Emit normalized outputs: + - `terminal_verdict_status` (`ready_for_linux_validation`/`ready_with_follow_up_for_linux_validation`/`blocked`/`contract_failed`) + - `terminal_verdict_decision` (`proceed_linux_validation`/`proceed_linux_validation_with_follow_up`/`halt_linux_validation_blocker`/`abort_linux_validation`) + - `terminal_verdict_exit_code` + - `terminal_verdict_should_proceed` / `terminal_verdict_requires_manual_action` / `terminal_verdict_channel` + - Export `--github-output` fields for Linux CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: publish-archive=`archived` + drift=`passed` converges to `ready_for_linux_validation` + `proceed_linux_validation`. + - `T2`: publish-archive=`archived_with_follow_up` + drift=`passed` converges to `ready_with_follow_up_for_linux_validation` + `proceed_linux_validation_with_follow_up`. + - `T3`: publish-archive=`blocked` + drift=`passed` converges to `blocked` + `halt_linux_validation_blocker`. + - `T4`: publish-archive mismatch or drift failure converges to `contract_failed` + `abort_linux_validation`. + - `T5`: GitHub output includes status/decision/exit_code/proceed+manual-action/channel/drift counters/report paths. +- Depends on: `P2-57` + +## P2-60 Linux CI Workflow Linux Validation Dispatch Gate +- Card ID: `P2-60` +- Priority: `P2` +- Problem: + - `P2-59` gives a terminal go/no-go contract, but Linux CI still lacks a single dispatch contract that transforms terminal verdict into an executable Linux validation action. + - Without a dedicated Linux-validation-dispatch gate, downstream jobs still duplicate mapping logic across `ready_for_linux_validation` / `ready_with_follow_up_for_linux_validation` / `blocked` / `contract_failed` paths. +- Scope: + - Add one Linux validation dispatch gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json` (`P2-59`) + - Emit normalized outputs: + - `linux_validation_dispatch_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatched`/`dispatch_failed`/`blocked`/`contract_failed`) + - `linux_validation_dispatch_decision` (`dispatch_linux_validation`/`dispatch_linux_validation_with_follow_up`/`hold_linux_validation_blocker`/`abort_linux_validation_dispatch`) + - `linux_validation_dispatch_exit_code` + - `linux_validation_should_dispatch` / `linux_validation_requires_manual_action` / `linux_validation_channel` + - Support optional command execution with timeout control: + - default command: `python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure` + - optional override: `--linux-validation-command` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_dispatch_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: terminal verdict=`ready_for_linux_validation` converges to `ready_dry_run` + `dispatch_linux_validation`. + - `T2`: terminal verdict=`ready_with_follow_up_for_linux_validation` converges to `ready_with_follow_up_dry_run` + `dispatch_linux_validation_with_follow_up`. + - `T3`: terminal verdict=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. + - `T4`: terminal verdict mismatch or drift/evidence issue converges to `contract_failed` + `abort_linux_validation_dispatch`. + - `T5`: GitHub output includes status/decision/exit_code/dispatch+manual-action/channel/run-id/run-url/follow-up/report paths. +- Depends on: `P2-59` + +## P2-61 Linux CI Workflow Linux Validation Verdict Gate +- Card ID: `P2-61` +- Priority: `P2` +- Problem: + - `P2-60` converges Linux validation dispatch behavior, but Linux CI still lacks one terminal verdict artifact that downstream release governance can consume without re-parsing dispatch internals. + - Without a dedicated Linux-validation-verdict gate, status handling for `dispatched/dispatch_failed/blocked/contract_failed` remains duplicated across consumers and can drift from terminal-verdict and manifest-drift contracts. +- Scope: + - Add one Linux validation verdict gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json` (`P2-60`) + - Emit normalized outputs: + - `linux_validation_verdict_status` (`validated`/`validated_with_follow_up`/`validation_failed`/`blocked`/`contract_failed`) + - `linux_validation_verdict_decision` (`accept_linux_validation`/`accept_linux_validation_with_follow_up`/`escalate_linux_validation_failure`/`hold_linux_validation_blocker`/`abort_linux_validation_verdict`) + - `linux_validation_verdict_exit_code` + - `linux_validation_passed` / `linux_validation_verdict_requires_manual_action` / `linux_validation_verdict_channel` + - Support optional verdict command execution with timeout control: + - optional override: `--linux-validation-verdict-command` + - timeout control: `--linux-validation-verdict-timeout-seconds` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: dispatch=`dispatched` + terminal=`ready_for_linux_validation` converges to `validated` + `accept_linux_validation`. + - `T2`: dispatch=`dispatched` + terminal=`ready_with_follow_up_for_linux_validation` converges to `validated_with_follow_up` + `accept_linux_validation_with_follow_up`. + - `T3`: dispatch=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. + - `T4`: dispatch=`dispatch_failed` converges to `validation_failed` + `escalate_linux_validation_failure`; any contract mismatch/drift/evidence issue converges to `contract_failed` + `abort_linux_validation_verdict`. + - `T5`: GitHub output includes verdict status/decision/exit_code/passed+manual-action/channel/dispatch status+decision/report paths. +- Depends on: `P2-60` + +## P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate +- Card ID: `P2-62` +- Priority: `P2` +- Problem: + - `P2-61` converges Linux validation execution into one verdict contract, but Linux CI still lacks a dedicated publish contract that downstream release governance can consume without re-parsing verdict internals. + - Without a publish gate, status handling for `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` can drift across dashboards, notifications, and final closure pipelines. +- Scope: + - Add one Linux validation verdict publish gate that consumes: + - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json` (`P2-61`) + - Emit normalized outputs: + - `linux_validation_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) + - `linux_validation_verdict_publish_decision` (`announce_linux_validation_passed`/`announce_linux_validation_passed_with_follow_up`/`announce_linux_validation_blocker`/`abort_publish`) + - `linux_validation_verdict_publish_exit_code` + - `linux_validation_verdict_publish_should_notify` / `linux_validation_verdict_publish_requires_manual_action` / `linux_validation_verdict_publish_channel` + - Export `--github-output` fields for CI consumers. +- Implementation: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py` + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_publish_gate_runtime.py` + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` + - `scripts/run_linux_unified_gate.py` + - `README.md` + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` +- Test Contract: + - `T1`: verdict=`validated` converges to `published` + `announce_linux_validation_passed`. + - `T2`: verdict=`validated_with_follow_up` converges to `published_with_follow_up` + `announce_linux_validation_passed_with_follow_up`. + - `T3`: verdict=`blocked` converges to `blocked` + `announce_linux_validation_blocker`. + - `T4`: verdict contract mismatch/evidence issue converges to `contract_failed` + `abort_publish`. + - `T5`: GitHub output includes publish status/decision/exit_code/notify+manual-action/channel/run-id/run-url/follow-up/report paths. +- Depends on: `P2-61` + +## 8. 鎺ㄨ崘杩唬椤哄簭 + +鑻ュ彧鍏佽涓€杞竴杞帹杩涳紝寤鸿椤哄簭濡備笅锛? +1. `P0-01` +2. `P0-02` +3. `P0-03` +4. `P0-04` +5. `P0-05` +6. `P0-06` +7. `P0-07` +8. `P0-09` +9. `P0-08` +10. `P0-10` + +璇存槑锛? +- `P0-08` 涓嶉樆濉炰富鍔熻兘淇锛屽洜姝ゆ斁鍦?`P0-09` 鍚庨潰浜﹀彲 +- `P0-10` 浣滀负鏀跺彛鍗★紝搴旀斁鍦?Phase 0 灏鹃儴 + +## 9. 姣忚疆杩唬鐨勬渶灏忎骇鍑鸿姹? +姣忓畬鎴愪竴寮犲崱锛岃嚦灏戝悓姝ヤ互涓嬩骇鐗╋細 + +1. 浠g爜鍙樻洿 +2. 娴嬭瘯鍙樻洿 +3. 鏂囨。鍙樻洿 +4. 鎵嬪伐楠屾敹鍛戒护 +5. 椋庨櫓涓庡墿浣欑洸鍖? +寤鸿姣忓紶鍗$殑浜や粯璇存槑缁熶竴閲囩敤锛? +```text +銆愪换鍔″崱銆慞0-XX +銆愯В鍐炽€戔€︹€?銆愬畬鎴愩€戔€︹€?銆愭祴璇曘€戔€︹€?銆愰闄┿€戔€︹€?銆愪笅涓€鍗°€戔€︹€?``` + +## 10. 褰撳墠鏈€閫傚悎绔嬪嵆寮€宸ョ殑鍗″崟 + +鑻ョ珛鍒昏 Codex 涓嬪垁锛屾帹鑽愮涓€鎵瑰紑宸ュ崱锛? +1. `P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦` +2. `P0-02 /resume 鐪熼棴鐜痐 +3. `P0-03 auth/config 涓婚摼淇涓庣粺涓€` +4. `P0-04 feature flag 杩愯鏃朵慨澶峘 +5. `P0-05 review 鍛戒护浜у搧鍖朻 + +鐞嗙敱锛? +- 杩?5 寮犲崱鐩存帴鍐冲畾椤圭洰鏄惁浠庘€滃彲璺戜唬鐮佲€濊繘鍏モ€滃彲鏂藉伐浜у搧鈥?- 瀹冧滑閮藉睘浜庨珮纭畾鎬с€侀珮鏀剁泭銆佸彲鏈湴楠岃瘉鐨勪换鍔?- 鍏舵祴璇曞洖鎶ョ巼鏈€楂橈紝涓旇兘涓哄悗缁钩鍙板寲鍗″崟鎵竻闅滅 + +## 11. 鏂囨。缁存姢瑙勫垯 + +- 鑻ユ煇寮犱换鍔″崱杩涘叆寮€鍙戯紝鏂板鎵ц璁板綍鏃朵紭鍏堟洿鏂版湰鏂囦欢锛岃€屼笉鏄钩琛屾柊寤衡€滃悓涓婚灏忔枃妗b€?- 鑻ヤ竴寮犱换鍔″崱琚户缁媶鍒嗭紝浣跨敤瀛愮紪鍙凤細 + - `P0-03A` + - `P0-03B` +- 鑻ョ瓥鐣ュ彉鍖栧奖鍝嶄换鍔¤竟鐣岋紝鍏堟洿鏂颁笂娓告垬鐣ユ枃妗o紝鍐嶅洖鍐欐湰鏂囦欢 + +## 12. 缁撹 + +鏈枃妗g殑鐩爣涓嶆槸鈥滃垪寰呭姙浜嬮」鈥濓紝鑰屾槸寤虹珛涓€涓爺鍙戝彲鎵ц銆佹祴璇曞彲楠屾敹銆佺鐞嗗彲璺熻釜鐨勬柦宸ラ潰銆? +浠庣幇鍦ㄥ紑濮嬶紝鍚庣画鎵€鏈夊紑宸ュ缓璁兘搴斿熀浜庝换鍔″崱锛岃€屼笉鏄熀浜庢ā绯婃剰鍥俱€? +鎹㈣█涔嬶細 + +> 鍏堟寜鍗″紑宸ワ紝鍐嶆寜鍗¢獙灏革紝鍐嶆寜鍗℃帹杩涳紝涓嶅啀鍑劅瑙夊仛閲嶆瀯銆? +## 13. Execution Log (2026-04-29) + +- Card: `P0-01 Runtime Backbone` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `claude_code/mcp/server.py` +- Test scripts landed: + - `tests/test_main_runtime.py` (runtime bootstrap + mcp serve wiring coverage) + - `tests/test_runtime_bootstrap.py` (entrypoint consistency + mcp registry injection) +- Not executed locally per release policy: + - Unit/component/integration tests will be executed in Linux unified validation stage. + +## 14. Execution Log (2026-04-29, P0-02) + +- Card: `P0-02 /resume Runtime Closure` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/compact/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` +- Test scripts landed: + - `tests/test_resume_runtime.py` +- Not executed locally per release policy: + - Resume-related unit/component/integration checks will run in Linux unified validation stage. + +## 15. Execution Log (2026-04-29, P0-03) + +- Card: `P0-03 Auth/Config Runtime Unification` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/auth/__init__.py` +- Test scripts landed: + - `tests/test_commands_auth_runtime.py` + - `tests/test_main_runtime.py` (auth key precedence coverage extension) +- Not executed locally per release policy: + - Auth/config unit/component/integration checks will run in Linux unified validation stage. + +## 16. Execution Log (2026-04-29, P0-04) + +- Card: `P0-04 Feature Flag Runtime Repair` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/features.py` +- Test scripts landed: + - `tests/test_features_runtime.py` +- Not executed locally per release policy: + - Feature flag unit/component checks will run in Linux unified validation stage. + +## 17. Execution Log (2026-04-29, P0-05) + +- Card: `P0-05 Review Command Productization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/commands/review/__init__.py` + - `claude_code/services/review_service.py` +- Test scripts landed: + - `tests/test_review_command_runtime.py` +- Not executed locally per release policy: + - Review command unit/component/integration checks will run in Linux unified validation stage. + +## 18. Execution Log (2026-04-29, P0-06) + +- Card: `P0-06 Session/History/Memory Boundary Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/engine/query.py` + - `claude_code/engine/session.py` + - `claude_code/services/history_manager.py` + - `claude_code/services/memory_service.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_session_history_memory_runtime.py` + - `tests/test_context_builder_runtime.py` (boundary assertions extension) + - `tests/test_main_runtime.py` (runtime service wiring extension) +- Not executed locally per release policy: + - Session/history/memory unit/component/integration checks will run in Linux unified validation stage. + +## 19. Execution Log (2026-04-29, P0-07) + +- Card: `P0-07 Hooks Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/hooks_manager.py` + - `claude_code/commands/hooks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/repl/__init__.py` + - `claude_code/main.py` +- Test scripts landed: + - `tests/test_hooks_runtime.py` + - `tests/test_main_runtime.py` (hooks manager runtime bootstrap wiring update) +- Not executed locally per release policy: + - Hooks runtime unit/component/integration checks will run in Linux unified validation stage. + +## 20. Execution Log (2026-04-29, P0-09) + +- Card: `P0-09 Temp Artifact Hygiene` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `tests/test_plugins_runtime.py` + - `.gitignore` +- Test scripts landed: + - `tests/test_plugins_runtime.py` (fixture teardown cleanup assertion + idempotent cleanup coverage) +- Not executed locally per release policy: + - Plugin runtime cleanup and residue-isolation checks will run in Linux unified validation stage. + +## 21. Execution Log (2026-04-29, P0-08) + +- Card: `P0-08 Windows Bootstrap & Interpreter Probe Stabilization` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` + - `README.md` +- Test scripts landed: + - `tests/test_doctor_runtime.py` +- Not executed locally per release policy: + - Doctor interpreter-detection unit/component checks and Windows stub regression cases will run in Linux unified validation stage. + +## 22. Execution Log (2026-04-29, P0-10) + +- Card: `P0-10 Phase 0 Regression Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `pytest.ini` + - `scripts/run_phase0_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_phase0_regression_contract.py` +- Not executed locally per release policy: + - Phase 0 gate contract and curated regression-entry checks will run in Linux unified validation stage. + +## 23. Execution Log (2026-04-29, P1-01) + +- Card: `P1-01 Daemon/API Control Plane` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` + - `claude_code/server/__init__.py` + - `claude_code/main.py` + - `scripts/run_p1_control_plane_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_server_runtime.py` (`T1` schema validation, `T2` service behavior, `T3` client->server->runtime chain, `T4` daemon down/timeout/session-not-found) + - `tests/test_main_runtime.py` (daemon CLI bootstrap wiring coverage) +- Not executed locally per release policy: + - Daemon/API unit/component/integration/regression checks will run in Linux unified validation stage. + +## 24. Execution Log (2026-04-29, P1-02) + +- Card: `P1-02 Event Journal` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/event_journal.py` + - `claude_code/services/__init__.py` + - `claude_code/tasks/manager.py` + - `claude_code/tasks/factory.py` + - `claude_code/tasks/__init__.py` + - `claude_code/engine/query.py` + - `claude_code/server/control_plane.py` + - `claude_code/main.py` + - `scripts/run_p1_event_journal_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_event_journal_runtime.py` (`T1` schema/versioning, `T2` writer/reader correctness, `T3` query->tool->task event chain, `T4` partial write/corruption recovery) + - `tests/test_server_runtime.py` (event list/replay daemon API coverage) + - `tests/test_main_runtime.py` (runtime bootstrap wiring includes event journal) +- Not executed locally per release policy: + - Event journal unit/component/integration/regression checks will run in Linux unified validation stage. + +## 25. Execution Log (2026-04-29, P1-03) + +- Card: `P1-03 SQLite Runtime State Backend` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tasks/repository.py` (SQLite runtime repository + schema version/migration guard) + - `claude_code/tasks/factory.py` (runtime backend default switched to `sqlite`) + - `claude_code/tasks/__init__.py` (SQLite repository exports) + - `claude_code/engine/session.py` (SQLite session store + manager constructor hook) + - `claude_code/services/event_journal.py` (SQLite event journal + schema version/migration guard) + - `claude_code/services/__init__.py` (SQLite event journal exports) + - `claude_code/engine/query.py` (event journal duck-typing for backend-agnostic write path) + - `claude_code/main.py` (runtime bootstrap switched to unified `.claude/runtime_state.db`) + - `scripts/run_p1_sqlite_state_backend_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_sqlite_runtime_state_backend.py` (`T1` schema init/version guard, `T2` repository/session/event correctness, `T3` runtime chain wiring) + - `tests/test_tasks_backend_contract.py` (SQLite repository contract + schema guard extension) + - `tests/test_tasks_factory_runtime.py` (default runtime backend contract update to `sqlite`) + - `tests/test_tasks_manager_runtime.py` (SQLite lifecycle persistence/recovery extension) + - `tests/test_event_journal_runtime.py` (SQLite event journal schema/query/replay guard extension) + - `tests/test_main_runtime.py` (runtime bootstrap mocks updated for SQLite state wiring) +- Not executed locally per release policy: + - SQLite runtime state backend unit/component/integration/regression checks will run in Linux unified validation stage. + +## 26. Execution Log (2026-04-30, P1-04) + +- Card: `P1-04 Active Memory Runtime Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/memory_service.py` (scoped memory namespaces + scope snapshot API) + - `claude_code/engine/query.py` (active memory hit audit event + runtime memory scope propagation) + - `claude_code/main.py` (system prompt active memory snapshot injection + runtime memory scope wiring) + - `claude_code/tools/base.py` (tool context memory scope) + - `claude_code/tools/agent/__init__.py` (agent-level memory scope propagation) + - `claude_code/agents/builtin.py` (builtin agent memory scope defaults) + - `scripts/run_p1_active_memory_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_memory_scope_runtime.py` (`T1` scope namespace isolation, scoped snapshot boundaries) + - `tests/test_active_memory_runtime.py` (`T2` system-prompt memory injection, `T3` query memory-hit event chain, runtime scope propagation) +- Not executed locally per release policy: + - Active memory unit/component/integration/regression checks will run in Linux unified validation stage. + +## 27. Execution Log (2026-04-30, P1-05) + +- Card: `P1-05 Hook/Permission/Audit Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/permissions.py` (structured permission evaluation reason output for audit chain) + - `claude_code/services/hooks_manager.py` (hook result serialization helper) + - `claude_code/engine/query.py` (permission request/decision events + hook execution audit events) + - `scripts/run_p1_hook_permission_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_hook_permission_audit_runtime.py` (`T2` permission+hook+audit linkage, `T3` tool execution audit chain, `T4` deny/error partial-failure path) +- Not executed locally per release policy: + - Hook/permission/audit convergence unit/component/integration/regression checks will run in Linux unified validation stage. + +## 28. Execution Log (2026-04-30, P1-06) + +- Card: `P1-06 CLI Thin Client Migration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/main.py` (daemon thin-client mode for query/pipe path, daemon-required hard-fail option, local fallback branch) + - `scripts/run_p1_cli_thin_client_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_cli_daemon_thin_client_runtime.py` (`T2` client adapter mode resolution contract) + - `tests/test_main_runtime.py` (daemon thin-client success/fallback/fail-fast and pipe-mode chain coverage) +- Not executed locally per release policy: + - CLI thin-client adapter and daemon-fallback regression checks will run in Linux unified validation stage. + +## 29. Execution Log (2026-04-30, P2-01) + +- Card: `P2-01 Multi-Agent Supervisor` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/multi_agent_supervisor.py` (agent DAG scheduler, budget/timeout/retry policy, dependency artifact passing, parallel fan-out and aggregate close) + - `claude_code/services/__init__.py` (supervisor exports) + - `claude_code/tools/agent/__init__.py` (orchestrate mode + workflow schema + supervisor integration with structured artifact summary) + - `scripts/run_p2_multi_agent_supervisor_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_multi_agent_supervisor_runtime.py` (`T1` graph cycle guard, `T2` budget/timeout state machine, `T3` parallel scheduling, `T4` retry-based failure recovery; plus agent tool orchestrate path and config error branch) +- Not executed locally per release policy: + - Multi-agent supervisor unit/component/integration/regression checks will run in Linux unified validation stage. + +## 30. Execution Log (2026-04-30, P2-02) + +- Card: `P2-02 Artifact Bus & Conflict Convergence` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/artifact_bus.py` (artifact schema contract for patch/note/diff/report/finding, producer payload parsing, merge strategies `fail|append|replace`, conflict error path) + - `claude_code/services/multi_agent_supervisor.py` (artifact bus integration for dependency artifact passing, workflow-level artifact snapshot, node artifact conflict/scheme failure handling) + - `claude_code/services/__init__.py` (artifact bus exports) + - `scripts/run_p2_artifact_bus_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_artifact_bus_runtime.py` (`T1` schema validation, `T2` producer/consumer dependency view, `T4` conflict-fail path, append merge behavior) + - `tests/test_multi_agent_supervisor_runtime.py` (artifact bus summary extension + conflict merge failure branch in DAG execution) +- Not executed locally per release policy: + - Artifact bus unit/component/integration/regression checks will run in Linux unified validation stage. + +## 31. Execution Log (2026-04-30, P2-03) + +- Card: `P2-03 IDE Integration (VS Code first)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/control_plane.py` (added `/api/v1/ide/workspace` endpoint for IDE aggregate snapshot: diff, changed files, tasks, sessions, findings) + - `claude_code/server/ide_adapter.py` (VS Code client adapter and normalized workspace snapshot model) + - `claude_code/server/__init__.py` (IDE adapter exports) + - `scripts/run_p2_ide_integration_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_ide_integration_runtime.py` (`T2` client protocol adapter normalization, `T3` IDE -> daemon core chain for diff/task/session/findings) +- Not executed locally per release policy: + - IDE integration unit/component/integration/manual acceptance checks will run in Linux unified validation stage. + +## 32. Execution Log (2026-04-30, P2-04) + +- Card: `P2-04 GitHub/CI Workflow Agent` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/github_ci_workflow.py` (issue->plan->code->test->review->PR state machine, headless CI mode, command failure classification, structured workflow report) + - `claude_code/server/control_plane.py` (added `POST /api/v1/workflows/github-ci` endpoint + client helper for workflow execution) + - `claude_code/services/__init__.py` (GitHub/CI workflow service exports) + - `scripts/run_p2_github_ci_workflow_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_github_ci_workflow_runtime.py` (`T2` workflow state machine, `T3` fake repo/fake PR main chain, `T4` permission-denied/repo-dirty/network-failure branches) + - `tests/test_server_runtime.py` (daemon API route coverage for workflow success/failure mapping) +- Not executed locally per release policy: + - GitHub/CI workflow unit/component/integration/regression checks will run in Linux unified validation stage. + +## 33. Execution Log (2026-04-30, P2-05) + +- Card: `P2-05 Org Policy & Audit` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/services/org_policy_audit.py` (org policy matcher, secret redaction, approval queue transitions, audit event query/report) + - `claude_code/server/control_plane.py` (added org policy endpoints and client helpers: evaluate/approve/list/report) + - `claude_code/services/__init__.py` (Org policy audit service exports) + - `scripts/run_p2_org_policy_audit_gate.py` + - `README.md` +- Test scripts landed: + - `tests/test_org_policy_audit_runtime.py` (`T1` policy matcher+redaction, `T2` approval transitions, `T3` audit report chain, `T4` policy conflict block) + - `tests/test_server_runtime.py` (daemon API coverage for org policy evaluate/approval/audit/report and failure mapping) +- Not executed locally per release policy: + - Org policy/audit unit/component/integration/regression checks will run in Linux unified validation stage. + +## 34. Execution Log (2026-04-30, P2-06) + +- Card: `P2-06 Linux Unified Verification Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_linux_unified_gate.py` (aggregated Linux-stage command manifest for Phase 0 -> Phase 2 runtime chain) + - `README.md` (added unified gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (added P2-06 card + execution log) +- Test scripts landed: + - `tests/test_linux_unified_gate_runtime.py` (`T1` manifest file existence contract, `T2` command generation order/format contract) +- Not executed locally per release policy: + - Unified gate contract checks and all listed runtime tests will run in Linux unified validation stage. + +## 35. Execution Log (2026-04-30, P2-07) + +- Card: `P2-07 Agent Runtime Parity (sync/background)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/tools/base.py` (ToolContext adds conversation/provider/task_manager/tool_registry fields for runtime propagation) + - `claude_code/engine/query.py` (ToolContext now carries conversation/provider/task_manager/registry into tool execution path) + - `claude_code/tasks/manager.py` (`create_agent_task` supports metadata injection for audit/replay) + - `claude_code/tools/agent/__init__.py` (sync/background sub-agent parity: runtime provider/model resolution, registry propagation, task-manager reuse, background metadata contract) + - `scripts/run_p2_agent_runtime_parity_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-07 regression test to unified manifest) + - `README.md` (adds P2-07 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-07 card and execution log) +- Test scripts landed: + - `tests/test_agent_background_context_runtime.py` (`T2` sync/background context parity, `T3` background task metadata audit fields, `T4` provider/model inheritance regression) +- Not executed locally per release policy: + - Agent runtime parity unit/component/integration checks will run in Linux unified validation stage. + +## 36. Execution Log (2026-04-30, P2-08) + +- Card: `P2-08 Custom Agents Directory Loader` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/agents/loader.py` (custom agent markdown loader with YAML frontmatter parsing and cwd->git-root directory merge semantics) + - `claude_code/tools/agent/__init__.py` (lazy custom agent load, custom>builtin resolution, and merged unknown-type availability hints) + - `scripts/run_p2_custom_agents_loader_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-08 regression test to unified manifest) + - `README.md` (adds P2-08 gate command entry) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-08 card and execution log) +- Test scripts landed: + - `tests/test_custom_agents_loader_runtime.py` (`T1` frontmatter parsing, `T2` nested override merge, `T3` custom override builtin, `T4` invalid schema tolerance and unknown-type availability message) +- Not executed locally per release policy: + - Custom-agent loader unit/component/integration checks will run in Linux unified validation stage. + + +## 37. Execution Log (2026-04-30, P2-09) + +- Card: `P2-09 Linux Unified Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (Linux-stage executor over unified manifest; supports dry-run/print/continue-on-failure/report-dir and writes junit + summary json) + - `scripts/run_linux_unified_gate.py` (adds P2-09 execution gate contract test to unified manifest) + - `README.md` (adds P2-09 execution gate command entries) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-09 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (`T1` unified manifest order contract, `T2` junit path and extra-args forwarding contract) +- Not executed locally per release policy: + - Linux unified execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 38. Execution Log (2026-04-30, P2-10) + +- Card: `P2-10 JetBrains IDE Integration` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `claude_code/server/ide_adapter.py` (added `JetBrainsClientAdapter` + `JetBrainsWorkspaceSnapshot` + findings->inspections mapping with severity highlight normalization) + - `claude_code/server/__init__.py` (exports JetBrains adapter symbols) + - `scripts/run_p2_jetbrains_ide_integration_gate.py` + - `scripts/run_linux_unified_gate.py` (adds P2-10 adapter contract test to unified manifest) + - `README.md` (adds P2-10 gate command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-10 card and execution log) +- Test scripts landed: + - `tests/test_p2_jetbrains_ide_integration_runtime.py` (`T1` adapter snapshot normalization, `T2` findings->inspection highlight mapping, `T4` line type coercion tolerance) +- Not executed locally per release policy: + - JetBrains IDE adapter unit/component checks will run in Linux unified validation stage. + +## 39. Execution Log (2026-04-30, P2-11) + +- Card: `P2-11 Linux Sharded Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_execution_gate.py` (adds deterministic shard slicing via `--shard-total` + `--shard-index`, shard-aware dry-run header, shard metadata in summary payload) + - `README.md` (adds Linux shard-parallel invocation example) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-11 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds shard slicing contract, invalid shard argument guards, summary shard metadata contract) +- Not executed locally per release policy: + - P2-11 shard execution gate contract checks and full-suite execution will run in Linux validation stage. + +## 40. Execution Log (2026-04-30, P2-12) + +- Card: `P2-12 Linux Shard Aggregation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_aggregation_gate.py` (discovers shard summaries via direct path/glob/artifacts dir, validates summary schema, merges shard totals, emits merged summary and global pass/fail) + - `scripts/run_linux_unified_gate.py` (adds P2-12 aggregation contract test to unified manifest) + - `README.md` (adds Linux shard aggregation gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-12 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` (`T1` summary discovery dedupe, `T2` summary field validation, `T3` successful shard aggregation, `T4` missing-shard and total drift failure contract) + - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds empty-shard summary payload contract used by aggregation stage) +- Not executed locally per release policy: + - P2-12 aggregation gate contract checks and Linux multi-shard recovery validation will run in Linux unified validation stage. + +## 41. Execution Log (2026-04-30, P2-13) + +- Card: `P2-13 Linux Final Report Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_report_publish_gate.py` (loads merged shard summary, validates publish contract, emits final_report.json + final_report.md, enforces computed-vs-reported status consistency) + - `scripts/run_linux_unified_gate.py` (adds P2-13 publish gate contract test to unified manifest) + - `README.md` (adds P2-13 final report publish gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-13 card and execution log) +- Test scripts landed: + - `tests/test_p2_linux_report_publish_gate_runtime.py` (`T1` merged summary required fields/types, `T2` happy-path payload contract, `T3` status mismatch failure contract, `T4` markdown report section contract) +- Not executed locally per release policy: + - P2-13 publish gate contract checks and Linux final-report recovery validation will run in Linux unified validation stage. + +## 42. Execution Log (2026-04-30, P2-14) + +- Card: `P2-14 Linux Unified Pipeline Orchestration Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_unified_pipeline_gate.py` (orchestrates execution->aggregation->publish chain, supports stage skip controls, dry-run/print-commands, fail-fast, and pytest args forwarding to execution stage) + - `scripts/run_linux_unified_gate.py` (adds P2-14 pipeline contract test to unified manifest) + - `README.md` (adds P2-14 one-command Linux pipeline examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-14 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` (`T1` default 3-stage command plan with execution summary feed, `T2` skip-execution external summary passthrough, `T3` all-stage-skip empty plan contract) +- Not executed locally per release policy: + - P2-14 pipeline gate contract checks and Linux orchestrated recovery validation will run in Linux unified validation stage. + +## 43. Execution Log (2026-04-30, P2-15) + +- Card: `P2-15 Linux Shard Plan Generation Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_shard_plan_gate.py` (builds deterministic shard execution plan from unified manifest; emits per-shard report/summary/command payload; supports dry-run/print-commands/pytest args forwarding) + - `scripts/run_linux_unified_gate.py` (adds P2-15 shard plan contract test to unified manifest) + - `README.md` (adds P2-15 Linux shard plan command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-15 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_shard_plan_gate_runtime.py` (`T1` deterministic shard slicing contract, `T2` command+args forwarding contract, `T3` invalid shard_total rejection contract) +- Not executed locally per release policy: + - P2-15 shard plan gate contract checks and Linux fan-out orchestration validation will run in Linux unified validation stage. + +## 44. Execution Log (2026-04-30, P2-16) + +- Card: `P2-16 Linux CI Matrix Export Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_matrix_gate.py` (loads P2-15 shard plan, validates shard/total contract, emits CI-consumable matrix payload and summary-path collection, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-16 matrix gate contract test to unified manifest) + - `README.md` (adds P2-16 CI matrix gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-16 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_matrix_gate_runtime.py` (`T1` matrix include order/field contract, `T2` empty-shard filter contract, `T3` GitHub output value contract, `T4` totals mismatch rejection contract) +- Not executed locally per release policy: + - P2-16 CI matrix export contract checks and Linux fan-out/fan-in orchestration validation will run in Linux unified validation stage. + +## 45. Execution Log (2026-04-30, P2-17) + +- Card: `P2-17 Linux CI Workflow Plan Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_plan_gate.py` (loads P2-16 ci_matrix, validates matrix/summary_paths contracts, emits CI-consumable workflow plan with fan-out matrix and fan-in aggregation/publish commands, supports optional GitHub output export) + - `scripts/run_linux_unified_gate.py` (adds P2-17 workflow plan gate contract test to unified manifest) + - `README.md` (adds P2-17 workflow plan gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-17 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` (`T1` fan-out/fan-in payload contract, `T2` GitHub output contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-17 CI workflow plan contract checks and Linux fan-out/fan-in/final-publish orchestration validation will run in Linux unified validation stage. + +## 46. Execution Log (2026-04-30, P2-18) + +- Card: `P2-18 Linux CI Workflow YAML Render Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` (loads P2-17 ci_workflow_plan, validates fan-out/fan-in contract, renders GitHub Actions workflow YAML, emits render metadata for audit traceability) + - `scripts/run_linux_unified_gate.py` (adds P2-18 workflow YAML render contract test to unified manifest) + - `README.md` (adds P2-18 workflow YAML gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-18 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` (`T1` workflow YAML fan-out/fan-in render contract, `T2` render metadata contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection) +- Not executed locally per release policy: + - P2-18 workflow YAML render contract checks and Linux CI-end orchestration validation will run in Linux unified validation stage. + +## 47. Execution Log (2026-04-30, P2-19) + +- Card: `P2-19 Linux CI Workflow Drift Sync Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_sync_gate.py` (rebuilds expected workflow/metadata from P2-17 plan, validates drift against rendered artifacts, supports diff output and optional in-place sync write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-19 workflow sync contract test to unified manifest) + - `README.md` (adds P2-19 drift check/sync command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-19 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` (`T1` missing artifact drift contract, `T2` workflow content drift+diff contract, `T3` generated_at relaxed-mode contract, `T4` strict generated_at mismatch rejection) +- Not executed locally per release policy: + - P2-19 workflow sync contract checks and Linux CI drift-governance validation will run in Linux unified validation stage. + +## 48. Execution Log (2026-04-30, P2-20) + +- Card: `P2-20 Linux CI Workflow Command Guard Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` (validates command string/parts integrity, script target + required flag contract, fan-out/fan-in path binding, optional canonical write-back) + - `scripts/run_linux_unified_gate.py` (adds P2-20 command guard contract test to unified manifest) + - `README.md` (adds P2-20 command guard gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-20 card, dependency chain extension, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` (`T1` valid payload pass contract, `T2` command string/parts mismatch rejection + normalization contract, `T3` aggregation summary order mismatch rejection, `T4` publish path mismatch rejection) +- Not executed locally per release policy: + - P2-20 command-integrity contract checks and Linux CI command-governance validation will run in Linux unified validation stage. + +## 49. Execution Log (2026-04-30, P2-21) + +- Card: `P2-21 Linux CI Workflow Governance Convergence Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_gate.py` (aggregates P2-19 drift sync + P2-20 command guard + metadata lineage checks into one governance verdict and categorized failed_checks) + - `scripts/run_linux_unified_gate.py` (adds P2-21 governance gate contract test to unified manifest) + - `README.md` (adds P2-21 governance gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-21 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` (`T1` full aligned pass contract, `T2` workflow drift failure category, `T3` command guard failure category, `T4` metadata lineage mismatch category) +- Not executed locally per release policy: + - P2-21 governance convergence contract checks and Linux CI final-governance validation will run in Linux unified validation stage. + +## 50. Execution Log (2026-04-30, P2-22) + +- Card: `P2-22 Linux CI Workflow Governance Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` (publishes P2-21 governance report into CI decision payload, markdown report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-22 governance publish contract test to unified manifest) + - `README.md` (adds P2-22 governance publish gate invocation examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-22 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` (`T1` aligned governance pass contract, `T2` failed-checks deny-execution contract, `T3` failed_checks mismatch structural-failure contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-22 governance publish contract checks and Linux CI publish-stage validation will run in Linux unified validation stage. + +## 51. Execution Log (2026-04-30, P2-23) + +- Card: `P2-23 Linux CI Workflow Execution Decision Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_decision_gate.py` (converts P2-22 publish payload into execute/blocked decision contract with on-block exit policy, artifact-presence guard, markdown/json report, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-23 execution decision gate contract test to unified manifest) + - `README.md` (adds P2-23 decision-gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-23 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` (`T1` aligned pass -> execute contract, `T2` failed-governance -> blocked contract, `T3` blocked+skip policy exit-code contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-23 execution-decision contract checks and Linux CI handoff validation will run in Linux unified validation stage. + +## 52. Execution Log (2026-04-30, P2-24) + +- Card: `P2-24 Linux CI Workflow Dispatch Readiness Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` (converts P2-23 execution decision payload into dispatch-ready contract, canonical gh workflow dispatch command, markdown/json reports, and optional GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-24 dispatch gate contract test to unified manifest) + - `README.md` (adds P2-24 dispatch gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-24 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` (`T1` execute -> ready dispatch contract, `T2` blocked -> no-dispatch contract, `T3` decision contract mismatch rejection, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-24 dispatch-readiness contract checks and Linux CI dispatch handoff validation will run in Linux unified validation stage. + +## 53. Execution Log (2026-04-30, P2-25) + +- Card: `P2-25 Linux CI Workflow Dispatch Execution Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` (validates P2-24 dispatch contract, executes ready dispatch command with timeout handling, and emits execution JSON/markdown/github-output contract) + - `scripts/run_linux_unified_gate.py` (adds P2-25 dispatch execution gate contract test to unified manifest) + - `README.md` (adds P2-25 dispatch execution gate usage examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-25 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` (`T1` ready dry-run contract, `T2` blocked pass-through contract, `T3` command contract mismatch rejection, `T4` dispatch failure reason code contract, `T5` github-output field contract) +- Not executed locally per release policy: + - P2-25 dispatch execution contract checks and Linux CI dispatch execution validation will run in Linux unified validation stage. + +## 54. Execution Log (2026-04-30, P2-26) + +- Card: `P2-26 Linux CI Workflow Full Pipeline Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (initially composed P2-17 -> P2-31 into single pipeline entry with stage skips, strategy passthrough, dry-run/print, fail-fast policy, terminal publish, release handoff, and release trigger stage) + - `scripts/run_linux_unified_gate.py` (adds P2-26 pipeline gate contract test to unified manifest) + - `README.md` (updates P2-26 one-command workflow pipeline usage examples to include terminal publish coverage) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-26 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (`T1` default stage-chain contract through release trigger, `T2` strategy passthrough contract, `T3` partial skip-stage chain contract, `T4` all-skip empty-plan contract, `T5` terminal-publish-only stage contract) +- Not executed locally per release policy: + - P2-26 pipeline orchestration contract checks (including release trigger stage) and Linux CI end-to-end workflow gate validation will run in Linux unified validation stage. + +## 55. Execution Log (2026-04-30, P2-27) + +- Card: `P2-27 Linux CI Workflow Dispatch Traceability Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` (builds dispatch trace contract from P2-25 report, extracts run reference, emits poll command, supports optional --poll-now run status query and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain with P2-27 dispatch trace stage and new trace control flags) + - `scripts/run_linux_unified_gate.py` (adds P2-27 dispatch trace contract test to unified manifest) + - `README.md` (adds P2-27 gate usage and updated pipeline examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-27 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` (`T1` dispatched run reference extraction + poll command contract, `T2` missing run reference failure contract, `T3` blocked pass-through no-poll contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-27 stage presence and trace flag passthrough contract coverage) +- Not executed locally per release policy: + - P2-27 dispatch trace contract checks and Linux CI run-tracking validation will run in Linux unified validation stage. + +## 56. Execution Log (2026-04-30, P2-28) + +- Card: `P2-28 Linux CI Workflow Dispatch Completion Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` (converges P2-27 trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain with P2-28 completion stage and new completion control flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-28 completion contract test to unified manifest) + - `README.md` (adds P2-28 gate usage and updates P2-26 full-chain examples to P2-28) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-28 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` trace poll command mismatch rejection, `T4` github-output contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-28 stage presence and completion flag passthrough contract coverage) +- Not executed locally per release policy: + - P2-28 dispatch completion contract checks and Linux CI terminal convergence validation will run in Linux unified validation stage. + +## 57. Execution Log (2026-04-30, P2-29) + +- Card: `P2-29 Linux CI Workflow Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` (converts P2-28 completion artifact into terminal publish verdict with promote/hold decision, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_linux_unified_gate.py` (adds P2-29 terminal publish contract test to unified manifest) + - `README.md` (adds P2-29 terminal publish gate command examples) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-29 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` (`T1` success->publish/pass+promote contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract) +- Not executed locally per release policy: + - P2-29 terminal publish contract checks and Linux CI terminal-release handoff validation will run in Linux unified validation stage. + +## 58. Execution Log (2026-04-30, P2-30) + +- Card: `P2-30 Linux CI Workflow Release Handoff Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` (converts P2-29 terminal publish artifact into release handoff contract with promote/hold/reject action, target environment + channel semantics, contract mismatch guards, markdown/json/github-output outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain to compose P2-17 -> P2-30 and adds release handoff output flags + skip switch + environment/channel passthrough) + - `scripts/run_linux_unified_gate.py` (adds P2-30 release handoff contract test to unified manifest) + - `README.md` (adds P2-30 release handoff gate invocation examples and updates P2-26 full-chain examples to P2-30) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-30 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` (`T1` passed->ready_for_release contract, `T2` in_progress->awaiting_completion contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-30 stage presence, environment/channel passthrough, and release-handoff-only stage contract coverage) +- Not executed locally per release policy: + - P2-30 release handoff contract checks and Linux CI release-gate handoff validation will run in Linux unified validation stage. + +## 59. Execution Log (2026-04-30, P2-31) + +- Card: `P2-31 Linux CI Workflow Release Trigger Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` (loads P2-30 handoff contract, validates trigger readiness, executes/dry-runs release command, emits trigger JSON/markdown/github-output report) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose P2-17 -> P2-31 and adds release-trigger flags/outputs/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-31 release trigger contract test to unified manifest) + - `README.md` (adds P2-31 gate usage and updates P2-26 full-chain examples to P2-31) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-31 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` (`T1` ready_for_release dry-run contract, `T2` awaiting_completion pass-through contract, `T3` handoff contract mismatch rejection, `T4` trigger command failure convergence, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-31 stage presence, release-trigger flag passthrough, and release-trigger-only stage contract coverage) +- Not executed locally per release policy: + - P2-31 release trigger contract checks and Linux CI release dispatch execution validation will run in Linux unified validation stage. + +## 60. Execution Log (2026-04-30, P2-32) + +- Card: `P2-32 Linux CI Workflow Release Traceability Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` (loads P2-31 release trigger artifact, validates trigger contract, extracts release run reference, emits optional poll command, supports optional --poll-now status query, and outputs JSON/markdown/github-output report) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-32`, adds release-trace outputs/flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-32 release trace contract test to unified manifest) + - `README.md` (adds P2-32 gate usage and updates P2-26 full-chain examples to P2-32) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-32 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` (`T1` triggered->run reference extraction contract, `T2` missing run reference failure contract, `T3` trigger_failed passthrough contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-32 stage presence, release-trace flag passthrough, and release-trace-only stage contract coverage) +- Not executed locally per release policy: + - P2-32 release trace contract checks and Linux CI release run-tracking validation will run in Linux unified validation stage. + +## 61. Execution Log (2026-04-30, P2-33) + +- Card: `P2-33 Linux CI Workflow Release Completion Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` (converges P2-32 release trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-33`, adds release-completion outputs/flags/skip switch) + - `scripts/run_linux_unified_gate.py` (adds P2-33 release completion contract test to unified manifest) + - `README.md` (adds P2-33 gate usage and updates P2-26 full-chain examples to P2-33) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-33 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` release poll command mismatch rejection, `T4` github-output contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-33 stage presence, release-completion flag passthrough, and release-completion-only stage contract coverage) +- Not executed locally per release policy: + - P2-33 release completion contract checks and Linux CI release terminal convergence validation will run in Linux unified validation stage. + +## 62. Execution Log (2026-04-30, P2-34) + +- Card: `P2-34 Linux CI Workflow Release Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` (converts P2-33 release completion artifact into release terminal publish verdict with finalize/hold decision, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-34`, adds release-terminal-publish outputs and `--skip-release-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-34 release terminal publish contract test to unified manifest) + - `README.md` (adds P2-34 gate usage and updates P2-26 full-chain examples to P2-34) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-34 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` (`T1` success->passed+finalize contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-34 stage presence, skip flag coverage, and release-terminal-publish-only stage contract) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-34 runtime contract checks and Linux CI release terminal publish validation will run in Linux unified validation stage. + +## 63. Execution Log (2026-04-30, P2-35) + +- Card: `P2-35 Linux CI Workflow Release Finalization Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` (converts P2-34 release terminal publish artifact into final release closure contract with `finalize/hold/abort` decision, hold policy support, contract mismatch guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-35`, adds release-finalization outputs and `--skip-release-finalization` switch, plus `--on-release-hold` policy passthrough) + - `scripts/run_linux_unified_gate.py` (adds P2-35 release finalization contract test to unified manifest) + - `README.md` (adds P2-35 gate usage and updates P2-26 full-chain examples to P2-35) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-35 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` (`T1` passed->finalized contract, `T2` hold-policy-fail contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-35 stage presence, hold-policy passthrough, and release-finalization-only stage contract) +- Not executed locally per release policy: + - P2-35 runtime contract checks and Linux CI release finalization validation will run in Linux unified validation stage. + +## 64. Execution Log (2026-04-30, P2-36) + +- Card: `P2-36 Linux CI Workflow Release Closure Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` (converts P2-35 release finalization artifact into terminal release closure contract with `closed/pending/failed/contract_failed` status, `ship/hold/rollback` decision, consistency guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-36`, adds release-closure outputs and `--skip-release-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-36 release closure contract test to unified manifest) + - `README.md` (adds P2-36 gate usage and updates P2-26 full-chain examples to P2-36) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-36 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` (`T1` finalized->closed contract, `T2` awaiting_release->pending contract, `T3` finalization mismatch->contract_failed contract, `T4` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-36 stage presence, skip flag coverage, and release-closure-only stage contract) +- Not executed locally per release policy: + - P2-36 runtime contract checks and Linux CI release closure publish validation will run in Linux unified validation stage. + +## 65. Execution Log (2026-04-30, P2-37) + +- Card: `P2-37 Linux CI Workflow Release Archive Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` (converts P2-36 release closure artifact into terminal release archive contract with `ready/pending/failed/contract_failed` status, `publish/hold/block` decision, evidence-manifest existence checks, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-37`, adds release-archive outputs and `--skip-release-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-37 release archive contract test to unified manifest) + - `README.md` (adds P2-37 gate usage and updates P2-26 full-chain examples to P2-37) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-37 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` (`T1` closed+evidence->ready contract, `T2` pending->hold contract, `T3` closure mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-37 stage presence and release-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-37 runtime contract checks and Linux CI release evidence archive validation will run in Linux unified validation stage. + +## 66. Execution Log (2026-04-30, P2-38) + +- Card: `P2-38 Linux CI Workflow Release Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` (converts P2-37 release archive artifact into terminal release verdict contract with `published/awaiting_archive/blocked/contract_failed` status, `ship/hold/block` decision, evidence consistency guard, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-38`, adds release-verdict outputs and `--skip-release-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-38 release verdict contract test to unified manifest) + - `README.md` (adds P2-38 gate usage and updates P2-26 full-chain examples to P2-38) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-38 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` (`T1` ready->published contract, `T2` pending->awaiting_archive contract, `T3` archive mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-38 stage presence, skip flag coverage, and release-verdict-only stage contract) +- Not executed locally per release policy: + - P2-38 runtime contract checks and Linux CI final release verdict convergence validation will run in Linux unified validation stage. + +## 67. Execution Log (2026-04-30, Gate Contract Coverage Closure) + +- Card: `Phase 1 Gate Runtime Contract Closure (P1-01 ~ P1-06)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_linux_unified_gate.py` (extends Linux unified manifest with P1 gate runtime contract tests) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds closure execution log) +- Test scripts landed: + - `tests/test_p1_control_plane_gate_runtime.py` (P1-01 manifest file existence + pytest command contract) + - `tests/test_p1_event_journal_gate_runtime.py` (P1-02 manifest file existence + pytest command contract) + - `tests/test_p1_sqlite_state_backend_gate_runtime.py` (P1-03 gate command chain + file existence contract) + - `tests/test_p1_active_memory_gate_runtime.py` (P1-04 gate command chain + file existence contract) + - `tests/test_p1_hook_permission_audit_gate_runtime.py` (P1-05 gate command chain + file existence contract) + - `tests/test_p1_cli_thin_client_gate_runtime.py` (P1-06 gate command chain + file existence contract) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root tests --json` passed. + - `py -3 scripts/check_syntax.py --root scripts --json` passed. +- Not executed locally per release policy: + - All new P1 gate runtime contract tests will be executed in Linux unified validation stage. + +## 68. Execution Log (2026-05-01, P2-39) + +- Card: `P2-39 Linux CI Workflow Release Incident Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` (converts P2-38 release verdict artifact into terminal incident dispatch contract with `not_required/ready_dry_run/dispatched/dispatch_failed/contract_failed` status, verdict consistency guard, incident command execution fallback, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-39`, adds release-incident outputs, incident command flags, and `--skip-release-incident` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-39 release incident contract test to unified manifest) + - `README.md` (adds P2-39 gate usage and updates P2-26 full-chain examples to P2-39) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-39 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` (`T1` published->not_required contract, `T2` blocked->ready_dry_run contract, `T3` verdict mismatch->contract_failed contract, `T4` incident command failure convergence, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-39 stage presence, incident flag passthrough, and release-incident-only stage contract coverage) +- Not executed locally per release policy: + - P2-39 runtime contract checks and Linux CI release incident dispatch validation will run in Linux unified validation stage. + +## 69. Execution Log (2026-05-01, Gate Contract Coverage Closure II) + +- Card: `Gate Runtime Contract Closure II (Phase 0 + P2 gate manifest coverage)` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `tests/test_phase0_gate_runtime.py` (adds contract coverage for `scripts/run_phase0_gate.py` manifest + pytest command builder) + - `scripts/run_linux_unified_gate.py` (extends unified manifest to include: + - `tests/test_phase0_gate_runtime.py` + - `tests/test_linux_unified_gate_runtime.py` + - `tests/test_p2_multi_agent_supervisor_gate_runtime.py` + - `tests/test_p2_artifact_bus_gate_runtime.py` + - `tests/test_p2_ide_integration_gate_runtime.py` + - `tests/test_p2_github_ci_workflow_gate_runtime.py` + - `tests/test_p2_org_policy_audit_gate_runtime.py` + - `tests/test_p2_agent_runtime_parity_gate_runtime.py` + - `tests/test_p2_custom_agents_loader_gate_runtime.py` + - `tests/test_p2_jetbrains_ide_integration_gate_runtime.py`) +- Verification performed without running tests: + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. + - Gate contract scan passed: `NO_MISSING_GATE_TESTS`. + - Unified manifest scan passed: `NO_MISSING_UNIFIED_MANIFEST_TESTS`. +- Not executed locally per release policy: + - All newly added gate runtime contract tests remain pending for Linux unified validation stage. + +## 70. Execution Log (2026-05-01, P2-40) + +- Card: `P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` (converts P2-39 release incident artifact into terminal release verdict contract with `released/awaiting_archive/blocked_incident_ready_dry_run/blocked_incident_dispatched/blocked_incident_failed/blocked/contract_failed` status and `ship/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-40`, adds release-terminal-verdict outputs, stage wiring, and `--skip-release-terminal-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-40 release terminal verdict contract test to unified manifest) + - `README.md` (adds P2-40 gate usage and updates P2-26 full-chain examples to P2-40) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-40 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` (`T1` published->released contract, `T2` blocked+ready_dry_run->blocked_incident_ready_dry_run contract, `T3` verdict/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-40 stage presence, command path assertions, skip flag propagation, and release-terminal-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-40 runtime contract checks and Linux CI final terminal verdict convergence validation will run in Linux unified validation stage. + +## 71. Execution Log (2026-05-01, P2-41) + +- Card: `P2-41 Linux CI Workflow Release Delivery Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` (converts P2-40 release terminal verdict artifact into final delivery contract with `shipped/pending_follow_up/blocked_incident/blocked_incident_failed/blocked/contract_failed` status and `deliver/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-41`, adds release-delivery outputs, stage wiring, and `--skip-release-delivery` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-41 release delivery contract test to unified manifest) + - `README.md` (adds P2-41 gate usage and updates P2-26 full-chain examples to P2-41) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-41 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` (`T1` released->shipped contract, `T2` blocked_incident_ready_dry_run->blocked_incident contract, `T3` terminal/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-41 stage presence, command path assertions, skip flag propagation, and release-delivery-only stage contract coverage) +- Not executed locally per release policy: + - P2-41 runtime contract checks and Linux CI final delivery closure convergence validation will run in Linux unified validation stage. + +## 72. Execution Log (2026-05-01, P2-42) + +- Card: `P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` (converts P2-41 release delivery artifact into terminal publish contract with `published/pending_follow_up/blocked/contract_failed` status and `announce_release/announce_hold/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-42`, adds release-delivery-terminal-publish outputs, stage wiring, and `--skip-release-delivery-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-42 release delivery terminal publish contract test to unified manifest) + - `README.md` (adds P2-42 gate usage and updates P2-26 full-chain examples to P2-42) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-42 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` (`T1` shipped->published contract, `T2` blocked_incident->blocked contract, `T3` delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-42 stage presence, command path assertions, skip flag propagation, and release-delivery-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-42 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage. + +## 73. Execution Log (2026-05-01, P2-43) + +- Card: `P2-43 Linux CI Workflow Release Delivery Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` (converts P2-42 release delivery terminal publish artifact into final verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_release/open_follow_up/escalate_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-43`, adds release-delivery-final-verdict outputs, stage wiring, and `--skip-release-delivery-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-43 release delivery final verdict contract test to unified manifest) + - `README.md` (adds P2-43 gate usage and updates P2-26 full-chain examples to P2-43) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-43 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` (`T1` published->completed contract, `T2` pending_follow_up->requires_follow_up contract, `T3` terminal/delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-43 stage presence, command path assertions, skip flag propagation, and release-delivery-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-43 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage. + +## 74. Execution Log (2026-05-01, P2-44) + +- Card: `P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` (converts P2-43 release delivery final verdict artifact into follow-up dispatch contract with `closed/follow_up_required/escalated/contract_failed` status and `no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-44`, adds release-follow-up-dispatch outputs, stage wiring, and `--skip-release-follow-up-dispatch` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-44 release follow-up dispatch contract test to unified manifest) + - `README.md` (adds P2-44 gate usage and updates P2-26 full-chain examples to P2-44) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-44 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` (`T1` completed->closed contract, `T2` requires_follow_up->follow_up_required contract, `T3` final-verdict mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-44 stage presence, command path assertions, skip flag propagation, and release-follow-up-dispatch-only stage contract coverage) +- Not executed locally per release policy: + - P2-44 runtime contract checks and Linux CI follow-up dispatch convergence validation will run in Linux unified validation stage. + +## 75. Execution Log (2026-05-01, P2-45) + +- Card: `P2-45 Linux CI Workflow Release Follow-Up Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` (converts P2-44 release follow-up dispatch artifact into follow-up closure contract with `closed/queued_dry_run/queued/queue_failed/contract_failed` status and `no_action/queue_follow_up/queue_escalation/abort_queue` decision, plus consistency guards, optional follow-up queue command execution, markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-45`, adds release-follow-up-closure outputs, stage wiring, follow-up command flags, and `--skip-release-follow-up-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-45 release follow-up closure contract test to unified manifest) + - `README.md` (adds P2-45 gate usage and updates P2-26 full-chain examples to P2-45) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-45 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` (`T1` closed->no_action contract, `T2` follow_up_required->queued_dry_run contract, `T3` dispatch mismatch->contract_failed contract, `T4` follow-up queue command failure->queue_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-45 stage presence, command path assertions, follow-up flag propagation, and release-follow-up-closure-only stage contract coverage) +- Not executed locally per release policy: + - P2-45 runtime contract checks and Linux CI follow-up closure convergence validation will run in Linux unified validation stage. + +## 76. Execution Log (2026-05-01, P2-46) + +- Card: `P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` (converts P2-45 release follow-up closure artifact into terminal publish contract with `published/pending_queue/queue_failed/contract_failed` status and `announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-46`, adds release-follow-up-terminal-publish outputs, stage wiring, and `--skip-release-follow-up-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-46 release follow-up terminal publish contract test to unified manifest) + - `README.md` (adds P2-46 gate usage and updates P2-26 full-chain examples to P2-46) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-46 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` (`T1` closed->published contract, `T2` queued_dry_run->pending_queue contract, `T3` closure mismatch->contract_failed contract, `T4` queue_failed->queue_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-46 stage presence, command path assertions, skip flag propagation, and release-follow-up-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-46 runtime contract checks and Linux CI follow-up terminal publish convergence validation will run in Linux unified validation stage. + +## 77. Execution Log (2026-05-01, P2-47) + +- Card: `P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` (converts P2-46 release follow-up terminal publish artifact into final follow-up verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-47`, adds release-follow-up-final-verdict outputs, stage wiring, and `--skip-release-follow-up-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-47 release follow-up final verdict contract test to unified manifest) + - `README.md` (adds P2-47 gate usage and updates P2-26 full-chain examples to P2-47) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-47 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` (`T1` published+announce_closed->completed contract, `T2` pending_queue->requires_follow_up contract, `T3` queue_failed->blocked contract, `T4` contract mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-47 stage presence, command path assertions, skip flag propagation, and release-follow-up-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-47 runtime contract checks and Linux CI follow-up final verdict convergence validation will run in Linux unified validation stage. + +## 78. Execution Log (2026-05-01, P2-48) + +- Card: `P2-48 Linux CI Workflow Release Final Outcome Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` (converges P2-43 + P2-47 final verdict artifacts into one terminal release outcome contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-48`, adds release-final-outcome outputs, stage wiring, and `--skip-release-final-outcome` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-48 release final outcome contract test to unified manifest) + - `README.md` (adds P2-48 gate usage and updates P2-26 full-chain examples to P2-48) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-48 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` (`T1` released contract, `T2` released_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-48 stage presence, command path assertions, skip flag propagation, and release-final-outcome-only stage contract coverage) +- Not executed locally per release policy: + - P2-48 runtime contract checks and Linux CI final release outcome convergence validation will run in Linux unified validation stage. + +## 79. Execution Log (2026-05-01, P2-49) + +- Card: `P2-49 Linux CI Workflow Release Final Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` (converges P2-48 final outcome artifact into terminal publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-49`, adds release-final-terminal-publish outputs, stage wiring, and `--skip-release-final-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-49 release final terminal publish contract test to unified manifest) + - `README.md` (adds P2-49 gate usage and updates P2-26 full-chain examples to P2-49) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-49 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-49 stage presence, command path assertions, skip flag propagation, and release-final-terminal-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-49 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage. + +## 80. Execution Log (2026-05-01, P2-50) + +- Card: `P2-50 Linux CI Workflow Release Final Handoff Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` (converges P2-49 final terminal publish artifact into release handoff contract with `completed/completed_with_follow_up/blocked/contract_failed` status and `handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-50`, adds release-final-handoff outputs, stage wiring, and `--skip-release-final-handoff` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-50 release final handoff contract test to unified manifest) + - `README.md` (adds P2-50 gate usage and updates P2-26 full-chain examples to P2-50) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-50 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` (`T1` published->completed contract, `T2` published_with_follow_up->completed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-50 stage presence, command path assertions, skip flag propagation, and release-final-handoff-only stage contract coverage) +- Not executed locally per release policy: + - P2-50 runtime contract checks and Linux CI final handoff convergence validation will run in Linux unified validation stage. + +## 81. Execution Log (2026-05-01, P2-51) + +- Card: `P2-51 Linux CI Workflow Release Final Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` (converges P2-50 final handoff artifact into release closure contract with `closed/closed_with_follow_up/blocked/contract_failed` status and `close_release/close_with_follow_up/close_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-51`, adds release-final-closure outputs, stage wiring, and `--skip-release-final-closure` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-51 release final closure contract test to unified manifest) + - `README.md` (adds P2-51 gate usage and updates P2-26 full-chain examples to P2-51) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-51 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` (`T1` completed->closed contract, `T2` completed_with_follow_up->closed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-51 stage presence, command path assertions, skip flag propagation, and release-final-closure-only stage contract coverage) +- Not executed locally per release policy: + - P2-51 runtime contract checks and Linux CI final closure convergence validation will run in Linux unified validation stage. + +## 82. Execution Log (2026-05-01, P2-52) + +- Card: `P2-52 Linux CI Workflow Release Final Closure Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` (converges P2-51 release final closure artifact into final closure publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-52`, adds release-final-closure-publish outputs, stage wiring, and `--skip-release-final-closure-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-52 release final closure publish contract test to unified manifest) + - `README.md` (adds P2-52 gate usage and updates P2-26 full-chain examples to P2-52) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-52 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` (`T1` closed->published contract, `T2` closed_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-52 stage presence, command path assertions, skip flag propagation, and release-final-closure-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-52 runtime contract checks and Linux CI final closure publish convergence validation will run in Linux unified validation stage. + +## 83. Execution Log (2026-05-01, P2-53) + +- Card: `P2-53 Linux CI Workflow Release Final Archive Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` (converges P2-52 release final closure publish artifact into final archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-53`, adds release-final-archive outputs, stage wiring, and `--skip-release-final-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-53 release final archive contract test to unified manifest) + - `README.md` (adds P2-53 gate usage and updates P2-26 full-chain examples to P2-53) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-53 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-53 stage presence, command path assertions, skip flag propagation, and release-final-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-53 runtime contract checks and Linux CI final archive convergence validation will run in Linux unified validation stage. + +## 84. Execution Log (2026-05-01, P2-54) + +- Card: `P2-54 Linux CI Workflow Release Final Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` (converges P2-53 final archive artifact into final verdict contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-54`, adds release-final-verdict outputs, stage wiring, and `--skip-release-final-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-54 release final verdict contract test to unified manifest) + - `README.md` (adds P2-54 gate usage and updates P2-26 full-chain examples to P2-54) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-54 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` (`T1` archived->released contract, `T2` archived_with_follow_up->released_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-54 stage presence, command path assertions, skip flag propagation, and release-final-verdict-only stage contract coverage) +- Not executed locally per release policy: + - P2-54 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage. + +## 85. Execution Log (2026-05-01, P2-55) + +- Card: `P2-55 Linux CI Workflow Release Final Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` (converges P2-54 final verdict artifact into final verdict publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-55`, adds release-final-verdict-publish outputs, stage wiring, and `--skip-release-final-verdict-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-55 release final verdict publish contract test to unified manifest) + - `README.md` (adds P2-55 gate usage and updates P2-26 full-chain examples to P2-55) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-55 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-55 stage presence, command path assertions, skip flag propagation, and release-final-verdict-publish-only stage contract coverage) +- Not executed locally per release policy: + - P2-55 runtime contract checks and Linux CI final verdict publish convergence validation will run in Linux unified validation stage. + +## 86. Execution Log (2026-05-01, P2-56) + +- Card: `P2-56 Linux CI Workflow Release Final Publish Archive Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` (converges P2-55 final verdict publish artifact into final publish archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-56`, adds release-final-publish-archive outputs, stage wiring, and `--skip-release-final-publish-archive` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-56 release final publish archive contract test to unified manifest) + - `README.md` (adds P2-56 gate usage and updates P2-26 full-chain examples to P2-56) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-56 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-56 stage presence, command path assertions, skip flag propagation, and release-final-publish-archive-only stage contract coverage) +- Not executed locally per release policy: + - P2-56 runtime contract checks and Linux CI final publish archive convergence validation will run in Linux unified validation stage. + +## 87. Execution Log (2026-05-01, P2-57) + +- Card: `P2-57 Linux Gate Manifest Drift Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_gate_manifest_drift_gate.py` (adds closure guard that scans `run_*_gate.py` -> `test_*_gate_runtime.py` mapping and validates Linux unified manifest coverage + orphan manifest entries, emits JSON/Markdown + GitHub outputs) + - `scripts/run_linux_unified_gate.py` (adds P2-57 runtime contract test to Linux unified manifest) + - `README.md` (adds P2-57 standalone usage while keeping P2-26 full-chain wording as `P2-17 -> P2-56`) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-57 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` (`T1` gate scan include/exclude contract, `T2` runtime-test mapping contract, `T3` baseline drift report passed contract, `T4` github-output field contract) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-57 runtime contract checks and Linux gate drift closure convergence validation will run in Linux unified validation stage. + +## 88. Execution Log (2026-05-01, P2-58) + +- Card: `P2-58 Linux CI Workflow Pipeline Closure to P2-57` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain description to `P2-17 -> P2-57`, wires `workflow_gate_manifest_drift` stage to call `scripts/run_p2_linux_gate_manifest_drift_gate.py`, adds `--gate-manifest-drift-json-output` / `--gate-manifest-drift-markdown-output`, and adds `--skip-gate-manifest-drift`) + - `README.md` (updates P2-26 full-chain wording to `P2-17 -> P2-57` and adds P2-57-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-58 execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-57 stage presence in default/partial chain, command assertions for drift-stage output paths, skip-flag propagation, and P2-57-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-58 runtime contract checks and Linux CI workflow pipeline closure convergence validation will run in Linux unified validation stage. + +## 89. Execution Log (2026-05-01, P2-59) + +- Card: `P2-59 Linux CI Workflow Terminal Verdict Closure Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` (converges P2-56 release final publish archive report and P2-57 gate manifest drift report into one terminal verdict contract with `ready_for_linux_validation/ready_with_follow_up_for_linux_validation/blocked/contract_failed` status and `proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-59`, adds terminal-verdict outputs, stage wiring, and `--skip-terminal-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-59 terminal verdict contract test to unified manifest) + - `README.md` (adds P2-59 standalone usage, updates P2-26 full-chain examples to `P2-17 -> P2-59`, and adds P2-59-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-59 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` (`T1` archived+drift-passed->ready contract, `T2` archived-with-follow-up+drift-passed->ready-with-follow-up contract, `T3` blocked+drift-passed->blocked contract, `T4` archive mismatch or drift-failed->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-59 stage presence in default/partial chain, command assertions for terminal-verdict report paths, compatibility propagation for `skip_terminal_verdict`, and P2-59-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - P2-59 runtime contract checks and Linux CI terminal verdict closure convergence validation will run in Linux unified validation stage. + +## 90. Execution Log (2026-05-01, P2-60) + +- Card: `P2-60 Linux CI Workflow Linux Validation Dispatch Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_dispatch_gate.py` (converges P2-59 terminal verdict into Linux validation dispatch contract with `ready_dry_run/ready_with_follow_up_dry_run/dispatched/dispatch_failed/blocked/contract_failed` status and `dispatch_linux_validation/dispatch_linux_validation_with_follow_up/hold_linux_validation_blocker/abort_linux_validation_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-60`, adds Linux validation dispatch outputs, stage wiring, timeout flag, and `--skip-linux-validation-dispatch` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-60 Linux validation dispatch contract test to unified manifest) + - `README.md` (adds P2-60 standalone usage, updates full-chain examples to `P2-17 -> P2-60`, and adds P2-60-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-60 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_dispatch_gate_runtime.py` (`T1` ready->ready_dry_run contract, `T2` ready_with_follow_up->ready_with_follow_up_dry_run contract, `T3` blocked->blocked contract, `T4` mismatch/drift/evidence issue->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-60 stage presence in default/partial chain, command assertions for Linux validation dispatch paths/timeout, and P2-60-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 91. Execution Log (2026-05-01, P2-61) + +- Card: `P2-61 Linux CI Workflow Linux Validation Verdict Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_gate.py` (converges P2-60 Linux validation dispatch artifact into one final Linux validation verdict contract with `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` status and `accept_linux_validation/accept_linux_validation_with_follow_up/escalate_linux_validation_failure/hold_linux_validation_blocker/abort_linux_validation_verdict` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-61`, adds Linux validation verdict outputs, stage wiring, and `--skip-linux-validation-verdict` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-61 Linux validation verdict contract test to unified manifest) + - `README.md` (adds P2-61 standalone usage, updates full-chain examples to `P2-17 -> P2-61`, and adds P2-61-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-61 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_gate_runtime.py` (`T1` ready_dry_run->validated contract, `T2` ready_with_follow_up_dry_run->validated_with_follow_up contract, `T3` dispatch_failed->validation_failed contract, `T4` dispatch/evidence mismatch->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-61 stage presence in default/partial chain, command assertions for Linux validation verdict paths, skip propagation for `skip_linux_validation_verdict`, and P2-61-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 92. Execution Log (2026-05-02, P2-62) + +- Card: `P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_verdict_publish_gate.py` (converges P2-61 Linux validation verdict artifact into one publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-62`, adds Linux validation verdict publish outputs, stage wiring, and `--skip-linux-validation-verdict-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-62 Linux validation verdict publish contract test to unified manifest) + - `README.md` (adds P2-62 standalone usage, updates full-chain examples to `P2-17 -> P2-62`, and adds P2-62-only pipeline command) + - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-62 card, and execution log) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_verdict_publish_gate_runtime.py` (`T1` validated->published contract, `T2` validated_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-62 stage presence in default/partial chain, command assertions for Linux validation verdict publish paths, skip propagation for `skip_linux_validation_verdict_publish`, and P2-62-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 93. Execution Log (2026-05-02, P2-63) + +- Card: `P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate` +- Status: `implemented` (Linux full-suite verification pending) +- Code landed: + - `scripts/run_p2_linux_ci_workflow_linux_validation_terminal_publish_gate.py` (converges P2-62 Linux validation verdict publish artifact into one terminal publish contract with `terminal_published/terminal_published_with_follow_up/terminal_blocked/terminal_contract_failed` status and `announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish` decision, plus consistency guards and markdown/json/github-output contracts) + - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-63`, adds Linux validation terminal publish outputs, stage wiring, and `--skip-linux-validation-terminal-publish` switch) + - `scripts/run_linux_unified_gate.py` (adds P2-63 Linux validation terminal publish contract test to unified manifest) + - `README.md` (adds P2-63 standalone usage, updates full-chain examples to `P2-17 -> P2-63`, and adds P2-63-only pipeline command) +- Test scripts landed: + - `tests/test_p2_linux_ci_workflow_linux_validation_terminal_publish_gate_runtime.py` (`T1` published->terminal_published contract, `T2` published_with_follow_up->terminal_published_with_follow_up contract, `T3` blocked->terminal_blocked contract, `T4` publish contract mismatch/evidence issue->terminal_contract_failed contract, `T5` github-output field contract) + - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-63 stage presence in default/partial chain, command assertions for Linux validation terminal publish paths, skip propagation for `skip_linux_validation_terminal_publish`, and P2-63-only stage contract coverage) +- Verification performed (without running tests): + - `py -3 scripts/check_syntax.py --root scripts --json` passed. + - `py -3 scripts/check_syntax.py --root tests --json` passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. diff --git a/.claude/tmp_exec_from_head_refresh.txt b/.claude/tmp_exec_from_head_refresh.txt new file mode 100644 index 0000000..4794aa8 Binary files /dev/null and b/.claude/tmp_exec_from_head_refresh.txt differ diff --git a/.claude/tmp_obj_EXEC_from_head.txt b/.claude/tmp_obj_EXEC_from_head.txt new file mode 100644 index 0000000..4794aa8 Binary files /dev/null and b/.claude/tmp_obj_EXEC_from_head.txt differ diff --git a/.claude/tmp_obj_README_from_head.txt b/.claude/tmp_obj_README_from_head.txt new file mode 100644 index 0000000..4a408be Binary files /dev/null and b/.claude/tmp_obj_README_from_head.txt differ diff --git a/.claude/tmp_p2_96_source_snapshot.py b/.claude/tmp_p2_96_source_snapshot.py new file mode 100644 index 0000000..a68e783 --- /dev/null +++ b/.claude/tmp_p2_96_source_snapshot.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-97 gate for Linux CI workflow pipeline closure to P2-96. + +This script validates that the P2-96 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_96_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_96_missing": '"pipeline_closure_card_id": "P2-96"', + "pipeline_closure_target_card_id_p2_95_missing": '"pipeline_closure_target_card_id": "P2-95"', + "pipeline_closure_status_key_p2_95_missing": "pipeline_closure_to_p2_95_status", + "pipeline_closure_decision_verified_p2_95_missing": "pipeline_closure_to_p2_95_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_95_missing": "ci_workflow_pipeline_closure_p2_95.json", + "pipeline_closure_report_markdown_p2_95_missing": "ci_workflow_pipeline_closure_p2_95.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_96_payload( + *, + project_root: Path, + pipeline_closure_p2_95_gate_script: Path, + pipeline_closure_p2_95_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_95_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_95_gate_script)) + p2_96_closure_text = "" + else: + p2_96_closure_text = _read_text(pipeline_closure_p2_95_gate_script) + + if not pipeline_closure_p2_95_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_95_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_96_CLOSURE_PATTERNS.items(): + if pattern not in p2_96_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_96_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_96_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_96_verified", + "pipeline_closure_p2_96_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-97 " + f"pipeline_closure_to_p2_96_status={status} " + f"pipeline_closure_to_p2_96_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_95_gate_script": str( + pipeline_closure_p2_95_gate_script.resolve() + ), + "source_pipeline_closure_p2_95_runtime_test": str( + pipeline_closure_p2_95_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-97", + "pipeline_closure_target_card_id": "P2-96", + "pipeline_closure_to_p2_96_status": status, + "pipeline_closure_to_p2_96_decision": decision, + "pipeline_closure_to_p2_96_exit_code": exit_code, + "pipeline_closure_to_p2_96_should_proceed": should_proceed, + "pipeline_closure_to_p2_96_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_96_channel": channel, + "pipeline_closure_to_p2_96_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_96_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-96 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_96_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_96_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_96_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_96_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_96_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_96_channel']}`" + ), + ( + "- Source P2-96 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_95_gate_script']}`" + ), + ( + "- Source P2-96 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_95_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_96_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_96_status": str( + payload["pipeline_closure_to_p2_96_status"] + ), + "workflow_pipeline_closure_to_p2_96_decision": str( + payload["pipeline_closure_to_p2_96_decision"] + ), + "workflow_pipeline_closure_to_p2_96_exit_code": str( + payload["pipeline_closure_to_p2_96_exit_code"] + ), + "workflow_pipeline_closure_to_p2_96_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_96_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_96_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_96_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_96_channel": str( + payload["pipeline_closure_to_p2_96_channel"] + ), + "workflow_pipeline_closure_to_p2_96_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_96_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_96_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_96_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_96_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-96 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-95-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py", + help="P2-96 closure verifier script path (P2-95 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-95-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py", + help="P2-96 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_95_gate_script = project_root / Path( + args.pipeline_closure_p2_95_gate_script + ) + pipeline_closure_p2_95_runtime_test = project_root / Path( + args.pipeline_closure_p2_95_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_96_payload( + project_root=project_root, + pipeline_closure_p2_95_gate_script=pipeline_closure_p2_95_gate_script, + pipeline_closure_p2_95_runtime_test=pipeline_closure_p2_95_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-p2-96-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_96_status={payload['pipeline_closure_to_p2_96_status']} " + f"pipeline_closure_to_p2_96_decision={payload['pipeline_closure_to_p2_96_decision']} " + f"pipeline_closure_to_p2_96_exit_code={payload['pipeline_closure_to_p2_96_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_96_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/.claude/tmp_pipeline_excerpt.txt b/.claude/tmp_pipeline_excerpt.txt new file mode 100644 index 0000000..657daee --- /dev/null +++ b/.claude/tmp_pipeline_excerpt.txt @@ -0,0 +1,360 @@ + 200: linux_validation_terminal_dispatch_terminal_verdict_publish_json_output: Path, + 201: linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output: Path, + 202: linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output: Path, + 203: linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output: Path, + 204: linux_validation_terminal_dispatch_terminal_verdict_closure_json_output: Path, + 205: linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output: Path, + 206: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output: Path, + 207: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output: Path, + 208: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output: Path, + 209: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output: Path, + 210: workflow_name: str, + 211: python_version: str, + 212: artifact_prefix: str, + 213: target_environment: str, + 214: release_channel: str, + 215: workflow_ref: str, + 216: release_workflow_path: str, + 217: release_workflow_ref: str, + 218: release_timeout_seconds: int, +----- + 201: linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output: Path, + 202: linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output: Path, + 203: linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output: Path, + 204: linux_validation_terminal_dispatch_terminal_verdict_closure_json_output: Path, + 205: linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output: Path, + 206: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output: Path, + 207: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output: Path, + 208: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output: Path, + 209: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output: Path, + 210: workflow_name: str, + 211: python_version: str, + 212: artifact_prefix: str, + 213: target_environment: str, + 214: release_channel: str, + 215: workflow_ref: str, + 216: release_workflow_path: str, + 217: release_workflow_ref: str, + 218: release_timeout_seconds: int, + 219: release_command: str, +----- + 304: skip_linux_validation_terminal_dispatch_final_verdict: bool, + 305: skip_linux_validation_terminal_dispatch_final_verdict_publish: bool, + 306: skip_linux_validation_terminal_dispatch_final_publish_archive: bool, + 307: skip_linux_validation_terminal_dispatch_terminal_verdict: bool, + 308: skip_linux_validation_terminal_dispatch_terminal_verdict_publish: bool, + 309: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive: bool, + 310: skip_linux_validation_terminal_dispatch_terminal_verdict_closure: bool, + 311: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: bool, + 312: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: bool, + 313: ) -> list[tuple[str, list[str]]]: + 314: pipeline: list[tuple[str, list[str]]] = [] + 315: + 316: if not skip_plan: + 317: pipeline.append( + 318: ( + 319: "workflow_plan", + 320: [ + 321: python_executable, + 322: "scripts/run_p2_linux_ci_workflow_plan_gate.py", +----- + 1471: "--output-json", + 1472: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1473: "--output-markdown", + 1474: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output), + 1475: ], + 1476: ) + 1477: ) + 1478: + 1479: if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: + 1480: pipeline.append( + 1481: ( + 1482: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + 1483: [ + 1484: python_executable, + 1485: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1486: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + 1487: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1488: "--output-json", + 1489: str( +----- + 1474: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output), + 1475: ], + 1476: ) + 1477: ) + 1478: + 1479: if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: + 1480: pipeline.append( + 1481: ( + 1482: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + 1483: [ + 1484: python_executable, + 1485: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1486: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + 1487: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1488: "--output-json", + 1489: str( + 1490: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 1491: ), + 1492: "--output-markdown", +----- + 1477: ) + 1478: + 1479: if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: + 1480: pipeline.append( + 1481: ( + 1482: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + 1483: [ + 1484: python_executable, + 1485: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1486: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + 1487: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1488: "--output-json", + 1489: str( + 1490: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 1491: ), + 1492: "--output-markdown", + 1493: str( + 1494: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 1495: ), +----- + 1482: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + 1483: [ + 1484: python_executable, + 1485: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1486: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + 1487: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1488: "--output-json", + 1489: str( + 1490: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 1491: ), + 1492: "--output-markdown", + 1493: str( + 1494: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 1495: ), + 1496: ], + 1497: ) + 1498: ) + 1499: + 1500: return pipeline +----- + 1486: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + 1487: str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + 1488: "--output-json", + 1489: str( + 1490: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 1491: ), + 1492: "--output-markdown", + 1493: str( + 1494: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 1495: ), + 1496: ], + 1497: ) + 1498: ) + 1499: + 1500: return pipeline + 1501: + 1502: + 1503: def main() -> int: + 1504: parser = argparse.ArgumentParser( +----- + 2116: ) + 2117: parser.add_argument( + 2118: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-markdown-output", + 2119: default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md", + 2120: help="P2-81 Linux validation terminal dispatch terminal verdict closure publish markdown path", + 2121: ) + 2122: parser.add_argument( + 2123: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-json-output", + 2124: default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json", + 2125: help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive JSON path", + 2126: ) + 2127: parser.add_argument( + 2128: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-markdown-output", + 2129: default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md", + 2130: help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive markdown path", + 2131: ) + 2132: parser.add_argument( + 2133: "--workflow-name", + 2134: default="Linux Unified Gate", +----- + 2121: ) + 2122: parser.add_argument( + 2123: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-json-output", + 2124: default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json", + 2125: help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive JSON path", + 2126: ) + 2127: parser.add_argument( + 2128: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-markdown-output", + 2129: default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md", + 2130: help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive markdown path", + 2131: ) + 2132: parser.add_argument( + 2133: "--workflow-name", + 2134: default="Linux Unified Gate", + 2135: help="Workflow name forwarded to P2-18/P2-19/P2-21", + 2136: ) + 2137: parser.add_argument( + 2138: "--python-version", + 2139: default="3.11", +----- + 2650: help="Skip P2-80 Linux validation terminal dispatch terminal verdict closure stage", + 2651: ) + 2652: parser.add_argument( + 2653: "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure-publish", + 2654: action="store_true", + 2655: help="Skip P2-81 Linux validation terminal dispatch terminal verdict closure publish stage", + 2656: ) + 2657: parser.add_argument( + 2658: "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive", + 2659: action="store_true", + 2660: help="Skip P2-82 Linux validation terminal dispatch terminal verdict closure publish archive stage", + 2661: ) + 2662: parser.add_argument( + 2663: "--print-commands", + 2664: action="store_true", + 2665: help="Print planned commands", + 2666: ) + 2667: parser.add_argument( + 2668: "--dry-run", +----- + 2732: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure = bool( + 2733: args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive + 2734: ) + 2735: if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: + 2736: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish = bool( + 2737: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure + 2738: ) + 2739: if ( + 2740: not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive + 2741: ): + 2742: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive = bool( + 2743: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish + 2744: ) + 2745: + 2746: if args.dispatch_timeout_seconds < 1: + 2747: print( + 2748: "[p2-linux-ci-workflow-pipeline-gate] --dispatch-timeout-seconds must be >= 1", + 2749: file=sys.stderr, + 2750: ) +----- + 2734: ) + 2735: if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: + 2736: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish = bool( + 2737: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure + 2738: ) + 2739: if ( + 2740: not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive + 2741: ): + 2742: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive = bool( + 2743: args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish + 2744: ) + 2745: + 2746: if args.dispatch_timeout_seconds < 1: + 2747: print( + 2748: "[p2-linux-ci-workflow-pipeline-gate] --dispatch-timeout-seconds must be >= 1", + 2749: file=sys.stderr, + 2750: ) + 2751: return 2 + 2752: if args.release_timeout_seconds < 1: +----- + 3081: args.linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output + 3082: ), + 3083: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output=Path( + 3084: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output + 3085: ), + 3086: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output=Path( + 3087: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output + 3088: ), + 3089: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output=Path( + 3090: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 3091: ), + 3092: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output=Path( + 3093: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 3094: ), + 3095: python_version=args.python_version, + 3096: artifact_prefix=args.artifact_prefix, + 3097: target_environment=args.target_environment, + 3098: release_channel=args.release_channel, + 3099: workflow_ref=args.workflow_ref, +----- + 3082: ), + 3083: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output=Path( + 3084: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output + 3085: ), + 3086: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output=Path( + 3087: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output + 3088: ), + 3089: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output=Path( + 3090: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 3091: ), + 3092: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output=Path( + 3093: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 3094: ), + 3095: python_version=args.python_version, + 3096: artifact_prefix=args.artifact_prefix, + 3097: target_environment=args.target_environment, + 3098: release_channel=args.release_channel, + 3099: workflow_ref=args.workflow_ref, + 3100: release_workflow_path=args.release_workflow_path, +----- + 3084: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output + 3085: ), + 3086: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output=Path( + 3087: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output + 3088: ), + 3089: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output=Path( + 3090: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 3091: ), + 3092: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output=Path( + 3093: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 3094: ), + 3095: python_version=args.python_version, + 3096: artifact_prefix=args.artifact_prefix, + 3097: target_environment=args.target_environment, + 3098: release_channel=args.release_channel, + 3099: workflow_ref=args.workflow_ref, + 3100: release_workflow_path=args.release_workflow_path, + 3101: release_workflow_ref=args.release_workflow_ref, + 3102: release_timeout_seconds=args.release_timeout_seconds, +----- + 3085: ), + 3086: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output=Path( + 3087: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output + 3088: ), + 3089: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output=Path( + 3090: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + 3091: ), + 3092: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output=Path( + 3093: args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + 3094: ), + 3095: python_version=args.python_version, + 3096: artifact_prefix=args.artifact_prefix, + 3097: target_environment=args.target_environment, + 3098: release_channel=args.release_channel, + 3099: workflow_ref=args.workflow_ref, + 3100: release_workflow_path=args.release_workflow_path, + 3101: release_workflow_ref=args.release_workflow_ref, + 3102: release_timeout_seconds=args.release_timeout_seconds, + 3103: release_command=args.release_command, +----- + 3188: skip_linux_validation_terminal_dispatch_final_verdict=args.skip_linux_validation_terminal_dispatch_final_verdict, + 3189: skip_linux_validation_terminal_dispatch_final_verdict_publish=args.skip_linux_validation_terminal_dispatch_final_verdict_publish, + 3190: skip_linux_validation_terminal_dispatch_final_publish_archive=args.skip_linux_validation_terminal_dispatch_final_publish_archive, + 3191: skip_linux_validation_terminal_dispatch_terminal_verdict=args.skip_linux_validation_terminal_dispatch_terminal_verdict, + 3192: skip_linux_validation_terminal_dispatch_terminal_verdict_publish=args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish, + 3193: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive, + 3194: skip_linux_validation_terminal_dispatch_terminal_verdict_closure=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure, + 3195: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish, + 3196: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive, + 3197: ) + 3198: + 3199: if not pipeline: + 3200: print("[p2-linux-ci-workflow-pipeline-gate] no stages selected") + 3201: return 0 + 3202: + 3203: if args.print_commands or args.dry_run: + 3204: for idx, (stage, command) in enumerate(pipeline, start=1): + 3205: print(f"{idx}. [{stage}] {_format_shell_command(command)}") + 3206: if args.dry_run: +----- diff --git a/.claude/tmp_pipeline_test_excerpt.txt b/.claude/tmp_pipeline_test_excerpt.txt new file mode 100644 index 0000000..037e230 --- /dev/null +++ b/.claude/tmp_pipeline_test_excerpt.txt @@ -0,0 +1,621 @@ + 246: "linux_validation_terminal_dispatch_terminal_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json"), + 247: "linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md"), + 248: "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json"), + 249: "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md"), + 250: "linux_validation_terminal_dispatch_terminal_verdict_closure_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json"), + 251: "linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md"), + 252: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json"), + 253: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md"), + 254: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json"), + 255: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md"), + 256: "workflow_name": "Linux Unified Gate", + 257: "python_version": "3.11", + 258: "artifact_prefix": "linux-unified-summary", + 259: "target_environment": "production", + 260: "release_channel": "stable", + 261: "workflow_ref": "main", + 262: "release_workflow_path": ".github/workflows/release.yml", + 263: "release_workflow_ref": "main", + 264: "release_timeout_seconds": 900, + 265: "release_command": "", + 266: "release_trace_poll_timeout_seconds": 600, +----- + 247: "linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md"), + 248: "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json"), + 249: "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md"), + 250: "linux_validation_terminal_dispatch_terminal_verdict_closure_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json"), + 251: "linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md"), + 252: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json"), + 253: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md"), + 254: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json"), + 255: "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md"), + 256: "workflow_name": "Linux Unified Gate", + 257: "python_version": "3.11", + 258: "artifact_prefix": "linux-unified-summary", + 259: "target_environment": "production", + 260: "release_channel": "stable", + 261: "workflow_ref": "main", + 262: "release_workflow_path": ".github/workflows/release.yml", + 263: "release_workflow_ref": "main", + 264: "release_timeout_seconds": 900, + 265: "release_command": "", + 266: "release_trace_poll_timeout_seconds": 600, + 267: "release_completion_poll_interval_seconds": 30, +----- + 350: "skip_linux_validation_terminal_dispatch_final_verdict": False, + 351: "skip_linux_validation_terminal_dispatch_final_verdict_publish": False, + 352: "skip_linux_validation_terminal_dispatch_final_publish_archive": False, + 353: "skip_linux_validation_terminal_dispatch_terminal_verdict": False, + 354: "skip_linux_validation_terminal_dispatch_terminal_verdict_publish": False, + 355: "skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive": False, + 356: "skip_linux_validation_terminal_dispatch_terminal_verdict_closure": False, + 357: "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish": False, + 358: "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive": False, + 359: } + 360: kwargs.update(overrides) + 361: if "skip_terminal_verdict" not in overrides: + 362: kwargs["skip_terminal_verdict"] = bool( + 363: kwargs["skip_gate_manifest_drift"] + 364: or kwargs["skip_release_final_publish_archive"] + 365: ) + 366: if "skip_linux_validation_dispatch" not in overrides: + 367: kwargs["skip_linux_validation_dispatch"] = bool(kwargs["skip_terminal_verdict"]) + 368: if "skip_linux_validation_verdict" not in overrides: + 369: kwargs["skip_linux_validation_verdict"] = bool( + 370: kwargs["skip_linux_validation_dispatch"] +----- + 444: if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure" not in overrides: + 445: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] = bool( + 446: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive"] + 447: ) + 448: if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" not in overrides: + 449: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] = bool( + 450: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] + 451: ) + 452: if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" not in overrides: + 453: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive"] = bool( + 454: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] + 455: ) + 456: return gate.build_ci_workflow_pipeline_commands(**kwargs) + 457: + 458: + 459: def test_build_ci_workflow_pipeline_commands_default_stage_chain(): + 460: commands = _build_commands() + 461: assert [stage for stage, _ in commands] == [ + 462: "workflow_plan", + 463: "workflow_yaml", + 464: "workflow_sync", +----- + 445: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] = bool( + 446: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive"] + 447: ) + 448: if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" not in overrides: + 449: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] = bool( + 450: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] + 451: ) + 452: if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" not in overrides: + 453: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive"] = bool( + 454: kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] + 455: ) + 456: return gate.build_ci_workflow_pipeline_commands(**kwargs) + 457: + 458: + 459: def test_build_ci_workflow_pipeline_commands_default_stage_chain(): + 460: commands = _build_commands() + 461: assert [stage for stage, _ in commands] == [ + 462: "workflow_plan", + 463: "workflow_yaml", + 464: "workflow_sync", + 465: "workflow_command_guard", +----- + 517: "workflow_linux_validation_terminal_dispatch_final_verdict", + 518: "workflow_linux_validation_terminal_dispatch_final_verdict_publish", + 519: "workflow_linux_validation_terminal_dispatch_final_publish_archive", + 520: "workflow_linux_validation_terminal_dispatch_terminal_verdict", + 521: "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish", + 522: "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", + 523: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure", + 524: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", + 525: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + 526: ] + 527: + 528: assert commands[0][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_plan_gate.py"] + 529: assert commands[1][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_yaml_gate.py"] + 530: assert commands[2][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_sync_gate.py"] + 531: assert commands[3][1][:2] == [ + 532: "python", + 533: "scripts/run_p2_linux_ci_workflow_command_guard_gate.py", + 534: ] + 535: assert commands[4][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_governance_gate.py"] + 536: assert commands[5][1][:2] == [ + 537: "python", +----- + 1869: ) + 1870: + 1 + 1871: ) + 1872: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[ + 1873: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_index + 1874: ].endswith( + 1875: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + 1876: ) + 1877: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command = command_map[ + 1878: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 1879: ] + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" +----- + 1870: + 1 + 1871: ) + 1872: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[ + 1873: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_index + 1874: ].endswith( + 1875: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + 1876: ) + 1877: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command = command_map[ + 1878: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 1879: ] + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) +----- + 1873: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_index + 1874: ].endswith( + 1875: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + 1876: ) + 1877: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command = command_map[ + 1878: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 1879: ] + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ +----- + 1876: ) + 1877: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command = command_map[ + 1878: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 1879: ] + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" +----- + 1879: ] + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( +----- + 1880: assert ( + 1881: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + 1882: == [ + 1883: "python", + 1884: "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" +----- + 1885: ] + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index +----- + 1886: ) + 1887: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + 1888: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1889: "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( +----- + 1890: ) + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( +----- + 1891: + 1 + 1892: ) + 1893: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1894: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + 1895: ].endswith( + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" +----- + 1896: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index +----- + 1897: ) + 1898: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( +----- + 1899: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1900: "--output-json" + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) +----- + 1901: ) + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) + 1920: + 1921: +----- + 1902: + 1 + 1903: ) + 1904: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1905: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + 1906: ].endswith( + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) + 1920: + 1921: + 1922: def test_build_ci_workflow_pipeline_commands_supports_partial_skip(): +----- + 1907: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) + 1920: + 1921: + 1922: def test_build_ci_workflow_pipeline_commands_supports_partial_skip(): + 1923: commands = _build_commands( + 1924: skip_plan=True, + 1925: skip_yaml=True, + 1926: skip_sync=True, + 1927: skip_command_guard=True, +----- + 1908: ) + 1909: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) + 1920: + 1921: + 1922: def test_build_ci_workflow_pipeline_commands_supports_partial_skip(): + 1923: commands = _build_commands( + 1924: skip_plan=True, + 1925: skip_yaml=True, + 1926: skip_sync=True, + 1927: skip_command_guard=True, + 1928: skip_governance=True, +----- + 1910: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + 1911: "--output-markdown" + 1912: ) + 1913: + 1 + 1914: ) + 1915: assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + 1916: linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + 1917: ].endswith( + 1918: "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + 1919: ) + 1920: + 1921: + 1922: def test_build_ci_workflow_pipeline_commands_supports_partial_skip(): + 1923: commands = _build_commands( + 1924: skip_plan=True, + 1925: skip_yaml=True, + 1926: skip_sync=True, + 1927: skip_command_guard=True, + 1928: skip_governance=True, + 1929: ) + 1930: assert [stage for stage, _ in commands] == [ +----- + 4832: skip_linux_validation_terminal_dispatch_final_verdict=True, + 4833: skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + 4834: skip_linux_validation_terminal_dispatch_final_publish_archive=True, + 4835: skip_linux_validation_terminal_dispatch_terminal_verdict=True, + 4836: skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + 4837: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + 4838: skip_linux_validation_terminal_dispatch_terminal_verdict_closure=False, + 4839: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=True, + 4840: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=True, + 4841: ) + 4842: assert [stage for stage, _ in commands] == [ + 4843: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure" + 4844: ] + 4845: + 4846: + 4847: def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_only_stage(): + 4848: commands = _build_commands( + 4849: skip_plan=True, + 4850: skip_yaml=True, + 4851: skip_sync=True, + 4852: skip_command_guard=True, +----- + 4904: skip_linux_validation_terminal_dispatch_final_verdict=True, + 4905: skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + 4906: skip_linux_validation_terminal_dispatch_final_publish_archive=True, + 4907: skip_linux_validation_terminal_dispatch_terminal_verdict=True, + 4908: skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + 4909: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + 4910: skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + 4911: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=False, + 4912: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=True, + 4913: ) + 4914: assert [stage for stage, _ in commands] == [ + 4915: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + 4916: ] + 4917: + 4918: + 4919: def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_only_stage(): + 4920: commands = _build_commands( + 4921: skip_plan=True, + 4922: skip_yaml=True, + 4923: skip_sync=True, + 4924: skip_command_guard=True, +----- + 4911: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=False, + 4912: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=True, + 4913: ) + 4914: assert [stage for stage, _ in commands] == [ + 4915: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + 4916: ] + 4917: + 4918: + 4919: def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_only_stage(): + 4920: commands = _build_commands( + 4921: skip_plan=True, + 4922: skip_yaml=True, + 4923: skip_sync=True, + 4924: skip_command_guard=True, + 4925: skip_governance=True, + 4926: skip_governance_publish=True, + 4927: skip_decision=True, + 4928: skip_dispatch=True, + 4929: skip_dispatch_execution=True, + 4930: skip_dispatch_trace=True, + 4931: skip_dispatch_completion=True, +----- + 4976: skip_linux_validation_terminal_dispatch_final_verdict=True, + 4977: skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + 4978: skip_linux_validation_terminal_dispatch_final_publish_archive=True, + 4979: skip_linux_validation_terminal_dispatch_terminal_verdict=True, + 4980: skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + 4981: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + 4982: skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + 4983: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=True, + 4984: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=False, + 4985: ) + 4986: assert [stage for stage, _ in commands] == [ + 4987: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 4988: ] + 4989: +----- + 4979: skip_linux_validation_terminal_dispatch_terminal_verdict=True, + 4980: skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + 4981: skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + 4982: skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + 4983: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=True, + 4984: skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=False, + 4985: ) + 4986: assert [stage for stage, _ in commands] == [ + 4987: "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + 4988: ] + 4989: +----- diff --git a/.claude/tmp_readme_cp936_bytes.bin b/.claude/tmp_readme_cp936_bytes.bin new file mode 100644 index 0000000..9499663 --- /dev/null +++ b/.claude/tmp_readme_cp936_bytes.bin @@ -0,0 +1 @@ +# Claude Code Python一个面向代码工作的 AI CLI / REPL / MCP 项目,提?Claude Code 风格的交互、工具调用、任务运行时、Agent、多 Provider 接入,以及一套持续演进中的工程化基础设施??README 是当前仓库的统一入口。大体量专题文档继续保留?`docs/`,这里只保留必要摘要、明确导航和后续维护约定,避免文档再次失控?## 项目定位- 语言与运行时:Python 3.10+、`asyncio`- 主入口:`python -m claude_code.main` ?`python run.py`- 交互模式:交互式 REPL、单?query、管道模式、MCP 服务器模?- 模型接入:`anthropic`、`openai`、`ollama`、`vllm`、`deepseek`、`bedrock`、`vertex`- 关键子系统:`QueryEngine`、`ToolRegistry`、权限系统、任务运行时、Agent、MCP、技能系统、服务层当前代码?`azure` 路径有显?fail-fast 提示,因此这里不把它作为当前推荐运行路径?## 当前整理结论截至 `2026-04-17`,本仓库?Markdown 文档可以收敛为以下结构:1. `README.md` 只负责统一入口,不再堆砌超长教程和实现枚举?2. “当前状态”优先看带日期的评估/执行文档,而不是更早的评审草稿?3. `docs/current/reference/AGENT.MD` ?`docs/current/reference/MCP.md` 体量较大,保留独立文档,不再重复搬进 README?4. 架构、性能、中间件三条线分别保留“当前结论”与“历史细节”两层,不再并列堆叠多个同级入口?本次额外核对结果?- 测试复核:`pytest -q -c pytest.ini` 通过,结果为 `90 passed`- 架构最新快照:`2026-04-16` 的评测结论为“工程可用基线可推进”,评分 `8.4 / 10`- 中间件演进快照:`Phase 0` 已完成,`Phase 1` 持续推进- 性能快照:`Baseline V1` 记录启动耗时?`1109.38 ms`## 快速开?### 1. 安装依赖```bashpython -m pip install -r requirements.txt```如果你只是从源码运行,本仓库当前建议?`requirements.txt` ?`pytest.ini` 为准?### 2. 选择 Provider#### Anthropic```bashexport CLAUDE_API_PROVIDER=anthropicexport ANTHROPIC_API_KEY=your-keyexport CLAUDE_MODEL=claude-sonnet-4-20250514```#### Ollama / 本地 OpenAI 兼容模型```bashexport CLAUDE_API_PROVIDER=ollamaexport OPENAI_BASE_URL=http://localhost:11434/v1export CLAUDE_MODEL=qwen2.5:14b```说明?- `openai` / `deepseek` 需?`OPENAI_API_KEY`- `ollama` / `vllm` 允许本地模型路径,不强制真实 API Key- PowerShell 请将 `export` 改为 `$env:变量?"?`### Windows 推荐启动(避?WindowsApps stub?Windows 上不要依?`C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe` 这类 Store alias stub?建议优先使用?```powershellpy -3 -m claude_code.main --doctorpy -3 -m claude_code.main```或显式使用虚拟环境解释器?```powershell.\.venv\Scripts\python.exe -m claude_code.main --doctor.\.venv\Scripts\python.exe -m claude_code.main````--doctor` 会输出解释器来源与风险提示;若命?WindowsApps stub,会给出明确警告与替代启动命令?### 3. 常用启动方式```bash# 查看帮助python -m claude_code.main --help# 版本python -m claude_code.main --version# 健康检?python -m claude_code.main --doctor# 交互?REPLpython -m claude_code.main# 单次提问python -m claude_code.main "解释当前目录下的项目结构"# 管道模式echo "帮我总结这段代码" | python -m claude_code.main --pipe# 作为 MCP 服务器启?python -m claude_code.main --mcp-serve# 启动本地 daemon / API 控制面(P1-01?python -m claude_code.main --daemon-serve --daemon-host 127.0.0.1 --daemon-port 8787# CLI ?thin-client ?daemon(P1-06?python -m claude_code.main --daemon-client "explain current repo status"echo "summarize changed files" | python -m claude_code.main --pipe --daemon-client```## 核心能力概览- 交互层:REPL、单?query、管道模式、命令系?- 工具层:文件读写、搜索、Shell/PowerShell、工作流、任务控制、技能、浏览器、LSP、MCP、Worktree、Cron、Team- 运行时:`QueryEngine` 编排模型对话、工具调用、权限判定和上下文构?- 任务体系:`TaskManager`、`TaskRepository`、`TaskQueue` 支撑后台任务和任务存储演?- 服务层:缓存、速率限制、遥测、历史管理、记忆、加密、hooks、shutdown、token estimation- 扩展能力:MCP server/client、Agent、技能目录、项目级 `.claude/` 运行数据如果你要看完整工具清单,不再?README 查找,直接看 `docs/current/reference/MCP.md`?## 仓库结构```textclaude-code-python/├── claude_code/ # 主代?? ├── main.py # CLI 入口? ├── engine/ # QueryEngine / context / session / permissions? ├── tools/ # 工具系统? ├── commands/ # 命令系统? ├── tasks/ # 任务运行时、仓储、队?? ├── services/ # 缓存、遥测、历史、hooks、shutdown ?? ├── mcp/ # MCP 服务端实?? ├── agents/ # 内置 Agent 定义? ├── skills/ # 技能系?? └── state/ # 状态模?├── docs/ # 专题文档├── examples/ # 示例脚本├── scripts/ # 辅助脚本与性能脚本├── tests/ # 测试├── run.py # 源码运行入口├── requirements.txt # 依赖清单└── pytest.ini # pytest 配置```## 文档地图下面是整理后的推荐阅读顺序?| 文档 | 角色 | 什么时候看 | 当前定位 ||---|---|---|---|| `README.md` | 总入?| 第一次进入仓?| 唯一导航入口 || `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 架构现状快照 | 想知道项目现在整体状?| 架构主入?|| `docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` | 产品战略与超车路线图 | 想知道项目如何从“可用底盘”走到“超?Claude Code?| 战略主入?|| `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` | 执行计划与任务卡?| 想直接开工并明确每张卡的研发范围、完成标准、测试深?| 执行主入?|| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 任务/中间件落地记?| 想知道任务系统已改到哪一?| 中间件现状主入口 || `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 性能一页纸 | 想快速看性能状?| 性能主入?|| `docs/current/reference/MCP.md` | MCP 使用手册 | 想把仓库作为 MCP server 接入 | MCP 主入?|| `docs/current/reference/AGENT.MD` | Agent 全量手册 | 想理?Agent、异步子任务、多智能?| Agent 主入?|| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 首轮问题盘点 | 想追溯最初发现了哪些问题 | 架构历史文档 || `docs/history/architecture/FIX_PROPOSALS.md` | 修复方案?| 想看历史修复设计与备选方?| 架构历史文档 || `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 中间件演进评?| 想评?Redis/MySQL/平台化是否有必要 | 中间件决策文?|| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 性能路线?| 想看阶段目标和执行计?| 性能路线?|| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 性能优化明细 | 想追踪具体改了哪些热路径 | 性能细节文档 || `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 性能进度记录 | 想看阶段性进?| 性能历史文档 || `docs/current/performance/BASELINE_V1.md` | 性能基线摘要 | 想看基线结论 | 性能数据摘要 || `docs/current/performance/baseline_v1.json` | 原始基线数据 | 想做对比或自动化处理 | 性能原始数据 |## 各主题的权威入口为避免“同一主题多个入口同时有效”,后续统一按下面的主入口理解:- 架构现状:`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md`- 产品战略 / 超越 Claude Code 路线图:`docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md`- 执行计划 / 任务卡单:`docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 架构历史问题与修复备选:`docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md`- 任务/中间件当前落地状态:`docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md`- 任务/中间件演进决策依据:`docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md`- 性能当前状态:`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 性能基线数据:`docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json`- MCP 使用:`docs/current/reference/MCP.md`- Agent 使用:`docs/current/reference/AGENT.MD`## 架构、性能、中间件的摘?### 架构综合 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 与历史评审文档,目前可以将架构结论收敛为?- 方向上是合理的模块化单体- 关键主链路曾出现过工具暴露、权限硬闸、Agent 异步、Provider 适配等断?- 截至 `2026-04-16` 的评测文档,这些关键断点已完成一轮闭环修复,测试记录?`68 passed`- 更早?`docs/history/architecture/ARCHITECTURE_REVIEW.md` ?`docs/history/architecture/FIX_PROPOSALS.md` 主要用于追溯“为什么会这么改”,不再代表最新状?### 中间?/ 任务系统综合两份中间件文档,当前结论是:- 仓库原始模型是“进程内队列 + 文件持久化”,不是数据库队列架?- 对单?CLI 合理,但对平台化、多实例、可恢复场景风险较高- 当前落地重点已从“是否立刻上 Redis/MySQL”转为“先把任务模型、仓储、队列抽象收敛?- 已完成的工作包括:`tasks.json` schema 统一、文件锁、安全写入、durable 语义修复、`TaskRepository` / `TaskQueue` 抽象接入### 性能综合性能文档,当前结论是?- 已建立语法门禁、性能 smoke、基线脚本三件基础设施- 当前基线主数据见 `docs/current/performance/baseline_v1.json`- 单页摘要优先?`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 详细优化项优先看 `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md`## 开发与验证```bash# 运行测试python -m pytest -q -c pytest.ini# 运行 Phase 0 主链回归门禁(提交前/合并前)python scripts/run_phase0_gate.py# 运行 Phase 1 控制面回归门禁(P1-01?python scripts/run_p1_control_plane_gate.py# 运行 Phase 1 事件日志回归门禁(P1-02?python scripts/run_p1_event_journal_gate.py# 运行 Phase 1 SQLite 状态后端回归门禁(P1-03?python scripts/run_p1_sqlite_state_backend_gate.py# 运行 Phase 1 Active Memory 回归门禁(P1-04?python scripts/run_p1_active_memory_gate.py# 运行 Phase 1 Hook/Permission/Audit 收敛门禁(P1-05?python scripts/run_p1_hook_permission_audit_gate.py# 运行 Phase 1 CLI thin-client 迁移门禁(P1-06?python scripts/run_p1_cli_thin_client_gate.py# 运行 Phase 2 ?Agent Supervisor 门禁(P2-01?python scripts/run_p2_multi_agent_supervisor_gate.py# 运行 Phase 2 Artifact Bus 门禁(P2-02?python scripts/run_p2_artifact_bus_gate.py# 运行 Phase 2 IDE 集成门禁(P2-03?python scripts/run_p2_ide_integration_gate.py# 运行 Phase 2 GitHub/CI workflow 门禁(P2-04?python scripts/run_p2_github_ci_workflow_gate.py# 运行 Phase 2 组织策略与审计门禁(P2-05?python scripts/run_p2_org_policy_audit_gate.py# 运行 Phase 2 Agent 运行时一致性门禁(P2-07?python scripts/run_p2_agent_runtime_parity_gate.py# 运行 Phase 2 自定?Agent 目录加载门禁(P2-08?python scripts/run_p2_custom_agents_loader_gate.py# 运行 Phase 2 JetBrains IDE 集成门禁(P2-10?python scripts/run_p2_jetbrains_ide_integration_gate.py# 运行 Phase 0-2 Linux 统一验尸门禁(P2-06?python scripts/run_linux_unified_gate.py# 运行 Phase 2 Linux 统一验尸执行门禁(P2-09,Linux 执行?python scripts/run_p2_linux_unified_execution_gate.py --dry-runpython scripts/run_p2_linux_unified_execution_gate.py --continue-on-failure# 分片并行示例? ?job 中的?2 片)python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2 --continue-on-failure# 聚合多分?summary 并给出全局通过/失败判定(P2-12,Linux 回收阶段执行?python scripts/run_p2_linux_shard_aggregation_gate.py --artifacts-dir .claude/reportspython scripts/run_p2_linux_shard_aggregation_gate.py --summary-glob ".claude/reports/**/summary.json"# ?merged summary 发布最终报告(P2-13,Linux 回收阶段执行?python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.jsonpython scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md# 一键串联执?>汇?>发布全链门禁(P2-14,Linux 编排入口?python scripts/run_p2_linux_unified_pipeline_gate.py --dry-runpython scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure --fail-fast# 仅做汇?发布(跳过执行阶段)python scripts/run_p2_linux_unified_pipeline_gate.py --skip-execution --summary-glob ".claude/reports/**/summary.json"# 生成 Linux 分片执行计划(P2-15,供 CI fan-out job 直接消费?python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --dry-run --print-commandspython scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --output .claude/reports/linux_unified_gate/shard_plan.json --continue-on-failure -- -k runtime# 生成 Linux CI matrix 产物(P2-16,供 CI matrix 直接消费?python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --dry-run --print-matrixpython scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --output .claude/reports/linux_unified_gate/ci_matrix.json --skip-empty-shards# 生成 Linux CI workflow 计划产物(P2-17,供 fan-out/fan-in job 直接消费?python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --dry-run --print-planpython scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --output .claude/reports/linux_unified_gate/ci_workflow_plan.json# ?workflow 计划渲染?GitHub Actions YAML(P2-18,供 CI 直接执行?python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-yamlpython scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --output-workflow .github/workflows/linux_unified_gate.yml --output-metadata .claude/reports/linux_unified_gate/ci_workflow_render.json# 校验/同步 workflow 渲染产物漂移(P2-19,防?plan->yaml 漂移?python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --print-diffpython scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --write# 校验/规范?workflow 计划中的命令契约(P2-20,防?command/parts/路径 漂移?python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --write --output .claude/reports/linux_unified_gate/ci_workflow_command_guard.json# 汇?workflow 漂移 + 命令 + 元数据血缘治理(P2-21,生成单一治理结论?python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --output .claude/reports/linux_unified_gate/ci_workflow_governance.json# 发布治理结论?CI 可消费决策(P2-22,生?should_execute_workflow 与发布报告)python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --output-json .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_governance_publish.md# 将治理发布结果收敛为执行决策闸门(P2-23,统一 execute/blocked 判定与退出策略)python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --on-block skip --output-json .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_execution_decision.md# 将执行决策收敛为可调度命令契约(P2-24,统一 dispatch-ready/blocked ?gh workflow run 命令?python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch.md# 执行调度契约并输出最终执行回执(P2-25,统一 dispatched/blocked/fail 的执行结果)python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md# ?dispatch 执行结果收敛?run trace 契约(P2-27,统一 run_id/url/poll 命令与状态)python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md# 等待 dispatch run 收敛为最终完成判定(P2-28,统一 completed/in_progress/timeout/failure 语义?python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md# 发布 dispatch completion 的终局结论(P2-29,统一 promote/hold 发布语义?python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md# ?terminal publish 收敛?release handoff 契约(P2-30,统一 release job 触发语义?python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --target-environment production --release-channel stable --output-json .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_handoff.md# 执行 release 最终触发门禁(P2-31,统一 release dispatch 执行语义?python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trigger.md# 追踪 release trigger 执行后的 run 语义(P2-32,统一 release run trace/poll 状态)python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trace.md# 等待 release run 收敛为最终完成判定(P2-33,统一 release 完成态语义)python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_completion.md# 发布 release completion 的终局结论(P2-34,统一 release finalize/hold 语义?python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md# ?release terminal publish 收敛为最?finalization 决策(P2-35,统一 finalize/hold/abort 语义?python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --on-hold pass --output-json .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_finalization.md# ?release finalization 收敛为最?closure 发布契约(P2-36,统一 close/notify/rollback 语义?python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_closure.md# ?release closure 收敛为最终证据归档契约(P2-37,统一 archive/publish/block 语义?python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_archive.md# ?release archive 收敛为最终发布判词契约(P2-38,统一 ship/hold/block 语义?python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_verdict.md# ?release verdict 收敛?incident dispatch 执行契约(P2-39,统一告警触发语义?python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --incident-repo acme/demo --incident-label release-incident --output-json .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_incident.md# ?release incident 收敛为终局 terminal verdict 契约(P2-40,统一 ship/hold/escalate/block 语义?python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md# ?release terminal verdict 收敛为最?delivery 契约(P2-41,统一 deliver/hold/escalate/block 语义?python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery.md# P2-42: converge release delivery to terminal publish contract (announce_release/announce_hold/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md# P2-43: converge release delivery terminal publish to final verdict contract (close_release/open_follow_up/escalate_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md# P2-44: converge release delivery final verdict to follow-up dispatch contract (no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch).python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md# P2-45: converge release follow-up dispatch to follow-up closure contract (no_action/queue_follow_up/queue_escalation/abort_queue).python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --follow-up-repo acme/demo --follow-up-label release-follow-up --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md# P2-46: converge release follow-up closure to terminal publish contract (announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish).python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md# P2-47: converge release follow-up terminal publish to final verdict contract (close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close).python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md# P2-48: converge delivery/follow-up final verdicts to one release final outcome contract (ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome).python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md# P2-49: converge release final outcome to terminal publish contract (announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md# P2-50: converge release final terminal publish to final handoff contract (handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff).python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md# P2-51: converge release final handoff to final closure contract (close_release/close_with_follow_up/close_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md# P2-52: publish release final closure contract (announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md# P2-53: archive release final closure publish contract (archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md# P2-54: converge release final archive to final verdict contract (ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict).python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md# P2-55: publish release final verdict contract (announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md# P2-56: archive release final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md# P2-57: close gate-script/runtime-test/manifest drift contract.python scripts/run_p2_linux_gate_manifest_drift_gate.py --dry-run --print-reportpython scripts/run_p2_linux_gate_manifest_drift_gate.py --output-json .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-markdown .claude/reports/linux_unified_gate/linux_gate_manifest_drift.md# P2-59: converge P2-56+P2-57 to one Linux terminal verdict contract.python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md# P2-60: dispatch Linux validation command from P2-59 terminal verdict contract.python scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --linux-validation-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md# P2-61: converge Linux validation dispatch into one final Linux validation verdict contract.python scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md# P2-62: publish Linux validation verdict contract (announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish).python scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md# P2-63: terminal publish Linux validation verdict publish contract (announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish).python scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md# P2-64: converge Linux validation terminal publish contract to final verdict (accept_linux_validation_terminal/accept_linux_validation_terminal_with_follow_up/escalate_linux_validation_terminal_blocker/abort_linux_validation_terminal_verdict).python scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md# P2-65: publish Linux validation final verdict contract (announce_linux_validation_final_validated/announce_linux_validation_final_validated_with_follow_up/announce_linux_validation_final_blocker/abort_publish).python scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md# P2-66: archive Linux validation final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md# P2-67: converge Linux validation final publish archive contract to terminal verdict (proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation).python scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md# P2-68: publish Linux validation terminal verdict contract (announce_linux_validation_terminal_ready/announce_linux_validation_terminal_ready_with_follow_up/announce_linux_validation_terminal_blocker/abort_publish).python scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md# P2-69: converge Linux validation terminal verdict publish to terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md# P2-70: execute Linux validation terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --linux-validation-terminal-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md# P2-71: trace Linux validation terminal dispatch execution run reference (run_tracking_ready/run_tracking_missing/run_poll_failed/run_in_progress/run_completed_success/run_completed_failure).python scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --poll-timeout-seconds 600 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md# P2-72: await Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md# P2-73: terminal publish Linux validation terminal dispatch completion contract (announce_linux_validation_terminal_dispatch_completed/announce_linux_validation_terminal_dispatch_completed_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failed/abort_linux_validation_terminal_dispatch_terminal_publish).python scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md# P2-74: final verdict Linux validation terminal dispatch terminal publish contract (accept_linux_validation_terminal_dispatch/accept_linux_validation_terminal_dispatch_with_follow_up/keep_linux_validation_terminal_dispatch_in_progress/escalate_linux_validation_terminal_dispatch_blocker/escalate_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict).python scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md# P2-75: publish Linux validation terminal dispatch final verdict contract (announce_linux_validation_terminal_dispatch_validated/announce_linux_validation_terminal_dispatch_validated_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict_publish).python scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md# P2-76: archive Linux validation terminal dispatch final verdict publish contract (archive_linux_validation_terminal_dispatch_validated/archive_linux_validation_terminal_dispatch_validated_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_publish_archive).python scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md# P2-77: terminal verdict Linux validation terminal dispatch final publish archive contract (proceed_linux_validation_terminal_dispatch/proceed_linux_validation_terminal_dispatch_with_follow_up/hold_linux_validation_terminal_dispatch_in_progress/halt_linux_validation_terminal_dispatch_blocker/halt_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78: publish Linux validation terminal dispatch terminal verdict contract (announce_linux_validation_terminal_dispatch_ready/announce_linux_validation_terminal_dispatch_ready_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79: archive Linux validation terminal dispatch terminal verdict publish contract (archive_linux_validation_terminal_dispatch_ready/archive_linux_validation_terminal_dispatch_ready_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 一键串?P2-17 -> P2-79 CI workflow 全链门禁(P2-26,Linux CI 总入口)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --dry-runpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --fail-fast --sync-write --command-guard-write --dispatch-trace-poll-now --completion-poll-interval-seconds 20 --completion-max-polls 15 --target-environment production --release-channel stable --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --release-trace-poll-now --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --on-release-hold pass --follow-up-repo acme/demo --follow-up-label release-follow-up# 仅执行后半链(从治理发布开始)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance# 仅执?P2-29 终局发布(复用已产出?completion 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执?P2-30 release handoff(复用已产出?terminal publish 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --target-environment staging --release-channel canary --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执?P2-31 release trigger(复用已产出?release handoff 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --skip-release-trace --skip-release-completion# 仅执?P2-32 release trace(复用已产出?release trigger 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --release-trace-poll-now --skip-release-completion# 仅执?P2-33 release completion(复用已产出?release trace 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --skip-release-terminal-publish# 仅执?P2-34 release terminal publish(复用已产出?release completion 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-finalization --skip-release-closure --skip-release-archive# 仅执?P2-35 release finalization(复用已产出?release terminal publish 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --on-release-hold fail --skip-release-closure --skip-release-archive# 仅执?P2-36 release closure(复用已产出?release finalization 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-archive# 仅执?P2-37 release archive(复用已产出?release closure 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-verdict --skip-release-incident# 仅执?P2-38 release verdict(复用已产出?release archive 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-incident# 仅执?P2-39 release incident(复用已产出?release verdict 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-terminal-verdict --incident-repo acme/demo --incident-label release-incident# 仅执?P2-40 release terminal verdict(复用已产出?release incident 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-42 only: run release delivery terminal publish (reuse release delivery report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-43 only: run release delivery final verdict (reuse release delivery terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-follow-up-dispatch# P2-44 only: run release follow-up dispatch (reuse release delivery final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict# P2-45 only: run release follow-up closure (reuse release follow-up dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-46 only: run release follow-up terminal publish (reuse release follow-up closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-47 only: run release follow-up final verdict (reuse release follow-up terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-48 only: run release final outcome (reuse P2-43 + P2-47 final verdict reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict# P2-49 only: run release final terminal publish (reuse P2-48 final outcome report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-50 only: run release final handoff (reuse P2-49 final terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-closure --skip-release-final-closure-publish# P2-51 only: run release final closure (reuse P2-50 final handoff report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure-publish# P2-52 only: run release final closure publish (reuse P2-51 final closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-archive# P2-53 only: run release final archive (reuse P2-52 final closure publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-54 only: run release final verdict (reuse P2-53 final archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive# P2-55 only: run release final verdict publish (reuse P2-54 final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict# P2-56 only: run release final publish archive (reuse P2-55 final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish# P2-57 only: run gate manifest drift closure (independent closure check over scripts/tests/manifest).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive# P2-59 only: run terminal verdict closure (reuse P2-56 + P2-57 reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift# P2-60 only: run Linux validation dispatch (reuse P2-59 terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --linux-validation-timeout-seconds 7200# P2-61 only: run Linux validation verdict (reuse P2-60 Linux validation dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch# P2-62 only: run Linux validation verdict publish (reuse P2-61 Linux validation verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict# P2-63 only: run Linux validation terminal publish (reuse P2-62 Linux validation verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish# P2-64 only: run Linux validation final verdict (reuse P2-63 Linux validation terminal publish report).# P2-65 only: run Linux validation final verdict publish (reuse P2-64 Linux validation final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publishpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict# P2-66 only: run Linux validation final publish archive (reuse P2-65 Linux validation final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish# P2-67 only: run Linux validation terminal verdict (reuse P2-66 Linux validation final publish archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive# P2-68 only: run Linux validation terminal verdict publish (reuse P2-67 Linux validation terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict# P2-69 only: run Linux validation terminal dispatch (reuse P2-68 Linux validation terminal verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish# P2-70 only: run Linux validation terminal dispatch execution (reuse P2-69 Linux validation terminal dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch# P2-71 only: run Linux validation terminal dispatch trace (reuse P2-70 Linux validation terminal dispatch execution report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion# P2-72 only: run Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-terminal-publish# P2-73 only: run Linux validation terminal dispatch terminal publish (reuse P2-72 Linux validation terminal dispatch completion report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-final-verdict# P2-74 only: run Linux validation terminal dispatch final verdict (reuse P2-73 Linux validation terminal dispatch terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish# P2-75 only: run Linux validation terminal dispatch final verdict publish (reuse P2-74 Linux validation terminal dispatch final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-final-publish-archive# P2-76 only: run Linux validation terminal dispatch final publish archive (reuse P2-75 Linux validation terminal dispatch final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-terminal-verdict# P2-77 only: run Linux validation terminal dispatch terminal verdict (reuse P2-76 Linux validation terminal dispatch final publish archive report).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78 only: run Linux validation terminal dispatch terminal verdict publish (reuse P2-77 Linux validation terminal dispatch terminal verdict report).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79 only: run Linux validation terminal dispatch terminal verdict publish archive (reuse P2-78 Linux validation terminal dispatch terminal verdict publish report).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 语法检?python scripts/check_syntax.py --root claude_code# 运行性能 smokepython -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py# 生成性能基线python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json```本次整理时实际复核过?```bashpython -m pytest -q -c pytest.ini```结果?`90 passed`?## 配置与运行时文件当前代码里常见的配置与运行时文件分布如下?| 位置 | 用?||---|---|| `~/.claude-code-python/base.json` | 全局基础配置 || `~/.claude-code-python/{env}.json` | 按环境分层配?|| `~/.claude-code-python/config.json` | 用户手工覆盖配置 || `.claude-code-python.json` | 项目本地设置 || `.claude/` | 项目运行数据目录,常见内容包括任务、计划、技能、hooks、todo、team、scheduled tasks || `.mcp.json` | MCP 相关配置 || `CLAUDE.md` / `CLAUDE.local.md` | 项目指导信息,由 `/init` 初始?|如果你要排查任务系统,优先关注:- `.claude/tasks.json`- `.claude/runtime_tasks.json`- `.claude/scheduled_tasks.json`## 示例与入口文?- `examples/basic_usage.py`:基础调用示例- `examples/configuration.py`:配置示?- `examples/custom_tools.py`:自定义工具示例- `examples/mcp_integration.py`:MCP 集成示例- `examples/session_management.py`:会话管理示?## 后续文档维护规则为了避免文档再次膨胀,后续建议遵守以下规则:1. README 只做入口、摘要和导航,不再追加大型专题说明?2. 同一主题最多保留一个“当前主文档”和一个“历史补充文档”?3. 新的评估/总结文档必须带日期,并明确自己是否取代旧文档?4. 超过一页的专题内容直接放到 `docs/`,README 只写摘要和链接?5. 如果某个专题已经有主文档,后续补充优先更新原文档,而不是新建平行文档?## 结论这次整理后,`README.md` 不再承担“所有知识都写在一处”的职责,而是成为仓库的稳定入口:- 想运行项目,先看“快速开始?- 想看当前架构/性能/中间件状态,直接看对应主文档- 想深?Agent ?MCP,再进入大文?- 想追溯历史问题和修复方案,再看历史文?这就是当前仓库文档的推荐阅读顺序和维护边界? \ No newline at end of file diff --git a/.claude/tmp_readme_head.md b/.claude/tmp_readme_head.md new file mode 100644 index 0000000..4a408be Binary files /dev/null and b/.claude/tmp_readme_head.md differ diff --git a/.claude/tmp_readme_u8decode.txt b/.claude/tmp_readme_u8decode.txt new file mode 100644 index 0000000..5b655a0 --- /dev/null +++ b/.claude/tmp_readme_u8decode.txt @@ -0,0 +1 @@ +# Claude Code Python一个面向代码工作的 AI CLI / REPL / MCP 项目,提�?Claude Code 风格的交互、工具调用、任务运行时、Agent、多 Provider 接入,以及一套持续演进中的工程化基础设施�?�?README 是当前仓库的统一入口。大体量专题文档继续保留�?`docs/`,这里只保留必要摘要、明确导航和后续维护约定,避免文档再次失控�?## 项目定位- 语言与运行时:Python 3.10+、`asyncio`- 主入口:`python -m claude_code.main` �?`python run.py`- 交互模式:交互式 REPL、单�?query、管道模式、MCP 服务器模�?- 模型接入:`anthropic`、`openai`、`ollama`、`vllm`、`deepseek`、`bedrock`、`vertex`- 关键子系统:`QueryEngine`、`ToolRegistry`、权限系统、任务运行时、Agent、MCP、技能系统、服务层当前代码�?`azure` 路径有显�?fail-fast 提示,因此这里不把它作为当前推荐运行路径�?## 当前整理结论截至 `2026-04-17`,本仓库�?Markdown 文档可以收敛为以下结构:1. `README.md` 只负责统一入口,不再堆砌超长教程和实现枚举�?2. “当前状态”优先看带日期的评估/执行文档,而不是更早的评审草稿�?3. `docs/current/reference/AGENT.MD` �?`docs/current/reference/MCP.md` 体量较大,保留独立文档,不再重复搬进 README�?4. 架构、性能、中间件三条线分别保留“当前结论”与“历史细节”两层,不再并列堆叠多个同级入口�?本次额外核对结果�?- 测试复核:`pytest -q -c pytest.ini` 通过,结果为 `90 passed`- 架构最新快照:`2026-04-16` 的评测结论为“工程可用基线可推进”,评分 `8.4 / 10`- 中间件演进快照:`Phase 0` 已完成,`Phase 1` 持续推进- 性能快照:`Baseline V1` 记录启动耗时�?`1109.38 ms`## 快速开�?### 1. 安装依赖```bashpython -m pip install -r requirements.txt```如果你只是从源码运行,本仓库当前建议�?`requirements.txt` �?`pytest.ini` 为准�?### 2. 选择 Provider#### Anthropic```bashexport CLAUDE_API_PROVIDER=anthropicexport ANTHROPIC_API_KEY=your-keyexport CLAUDE_MODEL=claude-sonnet-4-20250514```#### Ollama / 本地 OpenAI 兼容模型```bashexport CLAUDE_API_PROVIDER=ollamaexport OPENAI_BASE_URL=http://localhost:11434/v1export CLAUDE_MODEL=qwen2.5:14b```说明�?- `openai` / `deepseek` 需�?`OPENAI_API_KEY`- `ollama` / `vllm` 允许本地模型路径,不强制真实 API Key- PowerShell 请将 `export` 改为 `$env:变量�?"�?`### Windows 推荐启动(避�?WindowsApps stub�?Windows 上不要依�?`C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe` 这类 Store alias stub�?建议优先使用�?```powershellpy -3 -m claude_code.main --doctorpy -3 -m claude_code.main```或显式使用虚拟环境解释器�?```powershell.\.venv\Scripts\python.exe -m claude_code.main --doctor.\.venv\Scripts\python.exe -m claude_code.main````--doctor` 会输出解释器来源与风险提示;若命�?WindowsApps stub,会给出明确警告与替代启动命令�?### 3. 常用启动方式```bash# 查看帮助python -m claude_code.main --help# 版本python -m claude_code.main --version# 健康检�?python -m claude_code.main --doctor# 交互�?REPLpython -m claude_code.main# 单次提问python -m claude_code.main "解释当前目录下的项目结构"# 管道模式echo "帮我总结这段代码" | python -m claude_code.main --pipe# 作为 MCP 服务器启�?python -m claude_code.main --mcp-serve# 启动本地 daemon / API 控制面(P1-01�?python -m claude_code.main --daemon-serve --daemon-host 127.0.0.1 --daemon-port 8787# CLI �?thin-client �?daemon(P1-06�?python -m claude_code.main --daemon-client "explain current repo status"echo "summarize changed files" | python -m claude_code.main --pipe --daemon-client```## 核心能力概览- 交互层:REPL、单�?query、管道模式、命令系�?- 工具层:文件读写、搜索、Shell/PowerShell、工作流、任务控制、技能、浏览器、LSP、MCP、Worktree、Cron、Team- 运行时:`QueryEngine` 编排模型对话、工具调用、权限判定和上下文构�?- 任务体系:`TaskManager`、`TaskRepository`、`TaskQueue` 支撑后台任务和任务存储演�?- 服务层:缓存、速率限制、遥测、历史管理、记忆、加密、hooks、shutdown、token estimation- 扩展能力:MCP server/client、Agent、技能目录、项目级 `.claude/` 运行数据如果你要看完整工具清单,不再�?README 查找,直接看 `docs/current/reference/MCP.md`�?## 仓库结构```textclaude-code-python/├── claude_code/ # 主代�?�? ├── main.py # CLI 入口�? ├── engine/ # QueryEngine / context / session / permissions�? ├── tools/ # 工具系统�? ├── commands/ # 命令系统�? ├── tasks/ # 任务运行时、仓储、队�?�? ├── services/ # 缓存、遥测、历史、hooks、shutdown �?�? ├── mcp/ # MCP 服务端实�?�? ├── agents/ # 内置 Agent 定义�? ├── skills/ # 技能系�?�? └── state/ # 状态模�?├── docs/ # 专题文档├── examples/ # 示例脚本├── scripts/ # 辅助脚本与性能脚本├── tests/ # 测试├── run.py # 源码运行入口├── requirements.txt # 依赖清单└── pytest.ini # pytest 配置```## 文档地图下面是整理后的推荐阅读顺序�?| 文档 | 角色 | 什么时候看 | 当前定位 ||---|---|---|---|| `README.md` | 总入�?| 第一次进入仓�?| 唯一导航入口 || `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 架构现状快照 | 想知道项目现在整体状�?| 架构主入�?|| `docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` | 产品战略与超车路线图 | 想知道项目如何从“可用底盘”走到“超�?Claude Code�?| 战略主入�?|| `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` | 执行计划与任务卡�?| 想直接开工并明确每张卡的研发范围、完成标准、测试深�?| 执行主入�?|| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 任务/中间件落地记�?| 想知道任务系统已改到哪一�?| 中间件现状主入口 || `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 性能一页纸 | 想快速看性能状�?| 性能主入�?|| `docs/current/reference/MCP.md` | MCP 使用手册 | 想把仓库作为 MCP server 接入 | MCP 主入�?|| `docs/current/reference/AGENT.MD` | Agent 全量手册 | 想理�?Agent、异步子任务、多智能�?| Agent 主入�?|| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 首轮问题盘点 | 想追溯最初发现了哪些问题 | 架构历史文档 || `docs/history/architecture/FIX_PROPOSALS.md` | 修复方案�?| 想看历史修复设计与备选方�?| 架构历史文档 || `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 中间件演进评�?| 想评�?Redis/MySQL/平台化是否有必要 | 中间件决策文�?|| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 性能路线�?| 想看阶段目标和执行计�?| 性能路线�?|| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 性能优化明细 | 想追踪具体改了哪些热路径 | 性能细节文档 || `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 性能进度记录 | 想看阶段性进�?| 性能历史文档 || `docs/current/performance/BASELINE_V1.md` | 性能基线摘要 | 想看基线结论 | 性能数据摘要 || `docs/current/performance/baseline_v1.json` | 原始基线数据 | 想做对比或自动化处理 | 性能原始数据 |## 各主题的权威入口为避免“同一主题多个入口同时有效”,后续统一按下面的主入口理解:- 架构现状:`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md`- 产品战略 / 超越 Claude Code 路线图:`docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md`- 执行计划 / 任务卡单:`docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 架构历史问题与修复备选:`docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md`- 任务/中间件当前落地状态:`docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md`- 任务/中间件演进决策依据:`docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md`- 性能当前状态:`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 性能基线数据:`docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json`- MCP 使用:`docs/current/reference/MCP.md`- Agent 使用:`docs/current/reference/AGENT.MD`## 架构、性能、中间件的摘�?### 架构综合 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 与历史评审文档,目前可以将架构结论收敛为�?- 方向上是合理的模块化单体- 关键主链路曾出现过工具暴露、权限硬闸、Agent 异步、Provider 适配等断�?- 截至 `2026-04-16` 的评测文档,这些关键断点已完成一轮闭环修复,测试记录�?`68 passed`- 更早�?`docs/history/architecture/ARCHITECTURE_REVIEW.md` �?`docs/history/architecture/FIX_PROPOSALS.md` 主要用于追溯“为什么会这么改”,不再代表最新状�?### 中间�?/ 任务系统综合两份中间件文档,当前结论是:- 仓库原始模型是“进程内队列 + 文件持久化”,不是数据库队列架�?- 对单�?CLI 合理,但对平台化、多实例、可恢复场景风险较高- 当前落地重点已从“是否立刻上 Redis/MySQL”转为“先把任务模型、仓储、队列抽象收敛�?- 已完成的工作包括:`tasks.json` schema 统一、文件锁、安全写入、durable 语义修复、`TaskRepository` / `TaskQueue` 抽象接入### 性能综合性能文档,当前结论是�?- 已建立语法门禁、性能 smoke、基线脚本三件基础设施- 当前基线主数据见 `docs/current/performance/baseline_v1.json`- 单页摘要优先�?`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 详细优化项优先看 `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md`## 开发与验证```bash# 运行测试python -m pytest -q -c pytest.ini# 运行 Phase 0 主链回归门禁(提交前/合并前)python scripts/run_phase0_gate.py# 运行 Phase 1 控制面回归门禁(P1-01�?python scripts/run_p1_control_plane_gate.py# 运行 Phase 1 事件日志回归门禁(P1-02�?python scripts/run_p1_event_journal_gate.py# 运行 Phase 1 SQLite 状态后端回归门禁(P1-03�?python scripts/run_p1_sqlite_state_backend_gate.py# 运行 Phase 1 Active Memory 回归门禁(P1-04�?python scripts/run_p1_active_memory_gate.py# 运行 Phase 1 Hook/Permission/Audit 收敛门禁(P1-05�?python scripts/run_p1_hook_permission_audit_gate.py# 运行 Phase 1 CLI thin-client 迁移门禁(P1-06�?python scripts/run_p1_cli_thin_client_gate.py# 运行 Phase 2 �?Agent Supervisor 门禁(P2-01�?python scripts/run_p2_multi_agent_supervisor_gate.py# 运行 Phase 2 Artifact Bus 门禁(P2-02�?python scripts/run_p2_artifact_bus_gate.py# 运行 Phase 2 IDE 集成门禁(P2-03�?python scripts/run_p2_ide_integration_gate.py# 运行 Phase 2 GitHub/CI workflow 门禁(P2-04�?python scripts/run_p2_github_ci_workflow_gate.py# 运行 Phase 2 组织策略与审计门禁(P2-05�?python scripts/run_p2_org_policy_audit_gate.py# 运行 Phase 2 Agent 运行时一致性门禁(P2-07�?python scripts/run_p2_agent_runtime_parity_gate.py# 运行 Phase 2 自定�?Agent 目录加载门禁(P2-08�?python scripts/run_p2_custom_agents_loader_gate.py# 运行 Phase 2 JetBrains IDE 集成门禁(P2-10�?python scripts/run_p2_jetbrains_ide_integration_gate.py# 运行 Phase 0-2 Linux 统一验尸门禁(P2-06�?python scripts/run_linux_unified_gate.py# 运行 Phase 2 Linux 统一验尸执行门禁(P2-09,Linux 执行�?python scripts/run_p2_linux_unified_execution_gate.py --dry-runpython scripts/run_p2_linux_unified_execution_gate.py --continue-on-failure# 分片并行示例�? �?job 中的�?2 片)python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2 --continue-on-failure# 聚合多分�?summary 并给出全局通过/失败判定(P2-12,Linux 回收阶段执行�?python scripts/run_p2_linux_shard_aggregation_gate.py --artifacts-dir .claude/reportspython scripts/run_p2_linux_shard_aggregation_gate.py --summary-glob ".claude/reports/**/summary.json"# �?merged summary 发布最终报告(P2-13,Linux 回收阶段执行�?python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.jsonpython scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md# 一键串联执�?>汇�?>发布全链门禁(P2-14,Linux 编排入口�?python scripts/run_p2_linux_unified_pipeline_gate.py --dry-runpython scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure --fail-fast# 仅做汇�?发布(跳过执行阶段)python scripts/run_p2_linux_unified_pipeline_gate.py --skip-execution --summary-glob ".claude/reports/**/summary.json"# 生成 Linux 分片执行计划(P2-15,供 CI fan-out job 直接消费�?python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --dry-run --print-commandspython scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --output .claude/reports/linux_unified_gate/shard_plan.json --continue-on-failure -- -k runtime# 生成 Linux CI matrix 产物(P2-16,供 CI matrix 直接消费�?python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --dry-run --print-matrixpython scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --output .claude/reports/linux_unified_gate/ci_matrix.json --skip-empty-shards# 生成 Linux CI workflow 计划产物(P2-17,供 fan-out/fan-in job 直接消费�?python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --dry-run --print-planpython scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --output .claude/reports/linux_unified_gate/ci_workflow_plan.json# �?workflow 计划渲染�?GitHub Actions YAML(P2-18,供 CI 直接执行�?python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-yamlpython scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --output-workflow .github/workflows/linux_unified_gate.yml --output-metadata .claude/reports/linux_unified_gate/ci_workflow_render.json# 校验/同步 workflow 渲染产物漂移(P2-19,防�?plan->yaml 漂移�?python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --print-diffpython scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --write# 校验/规范�?workflow 计划中的命令契约(P2-20,防�?command/parts/路径 漂移�?python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --write --output .claude/reports/linux_unified_gate/ci_workflow_command_guard.json# 汇�?workflow 漂移 + 命令 + 元数据血缘治理(P2-21,生成单一治理结论�?python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --output .claude/reports/linux_unified_gate/ci_workflow_governance.json# 发布治理结论�?CI 可消费决策(P2-22,生�?should_execute_workflow 与发布报告)python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --output-json .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_governance_publish.md# 将治理发布结果收敛为执行决策闸门(P2-23,统一 execute/blocked 判定与退出策略)python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --on-block skip --output-json .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_execution_decision.md# 将执行决策收敛为可调度命令契约(P2-24,统一 dispatch-ready/blocked �?gh workflow run 命令�?python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch.md# 执行调度契约并输出最终执行回执(P2-25,统一 dispatched/blocked/fail 的执行结果)python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md# �?dispatch 执行结果收敛�?run trace 契约(P2-27,统一 run_id/url/poll 命令与状态)python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md# 等待 dispatch run 收敛为最终完成判定(P2-28,统一 completed/in_progress/timeout/failure 语义�?python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md# 发布 dispatch completion 的终局结论(P2-29,统一 promote/hold 发布语义�?python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md# �?terminal publish 收敛�?release handoff 契约(P2-30,统一 release job 触发语义�?python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --target-environment production --release-channel stable --output-json .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_handoff.md# 执行 release 最终触发门禁(P2-31,统一 release dispatch 执行语义�?python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trigger.md# 追踪 release trigger 执行后的 run 语义(P2-32,统一 release run trace/poll 状态)python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trace.md# 等待 release run 收敛为最终完成判定(P2-33,统一 release 完成态语义)python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_completion.md# 发布 release completion 的终局结论(P2-34,统一 release finalize/hold 语义�?python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md# �?release terminal publish 收敛为最�?finalization 决策(P2-35,统一 finalize/hold/abort 语义�?python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --on-hold pass --output-json .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_finalization.md# �?release finalization 收敛为最�?closure 发布契约(P2-36,统一 close/notify/rollback 语义�?python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_closure.md# �?release closure 收敛为最终证据归档契约(P2-37,统一 archive/publish/block 语义�?python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_archive.md# �?release archive 收敛为最终发布判词契约(P2-38,统一 ship/hold/block 语义�?python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_verdict.md# �?release verdict 收敛�?incident dispatch 执行契约(P2-39,统一告警触发语义�?python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --incident-repo acme/demo --incident-label release-incident --output-json .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_incident.md# �?release incident 收敛为终局 terminal verdict 契约(P2-40,统一 ship/hold/escalate/block 语义�?python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md# �?release terminal verdict 收敛为最�?delivery 契约(P2-41,统一 deliver/hold/escalate/block 语义�?python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery.md# P2-42: converge release delivery to terminal publish contract (announce_release/announce_hold/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md# P2-43: converge release delivery terminal publish to final verdict contract (close_release/open_follow_up/escalate_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md# P2-44: converge release delivery final verdict to follow-up dispatch contract (no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch).python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md# P2-45: converge release follow-up dispatch to follow-up closure contract (no_action/queue_follow_up/queue_escalation/abort_queue).python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --follow-up-repo acme/demo --follow-up-label release-follow-up --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md# P2-46: converge release follow-up closure to terminal publish contract (announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish).python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md# P2-47: converge release follow-up terminal publish to final verdict contract (close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close).python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md# P2-48: converge delivery/follow-up final verdicts to one release final outcome contract (ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome).python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md# P2-49: converge release final outcome to terminal publish contract (announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md# P2-50: converge release final terminal publish to final handoff contract (handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff).python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md# P2-51: converge release final handoff to final closure contract (close_release/close_with_follow_up/close_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md# P2-52: publish release final closure contract (announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md# P2-53: archive release final closure publish contract (archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md# P2-54: converge release final archive to final verdict contract (ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict).python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md# P2-55: publish release final verdict contract (announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md# P2-56: archive release final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md# P2-57: close gate-script/runtime-test/manifest drift contract.python scripts/run_p2_linux_gate_manifest_drift_gate.py --dry-run --print-reportpython scripts/run_p2_linux_gate_manifest_drift_gate.py --output-json .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-markdown .claude/reports/linux_unified_gate/linux_gate_manifest_drift.md# P2-59: converge P2-56+P2-57 to one Linux terminal verdict contract.python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md# P2-60: dispatch Linux validation command from P2-59 terminal verdict contract.python scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --linux-validation-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md# P2-61: converge Linux validation dispatch into one final Linux validation verdict contract.python scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md# P2-62: publish Linux validation verdict contract (announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish).python scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md# P2-63: terminal publish Linux validation verdict publish contract (announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish).python scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md# P2-64: converge Linux validation terminal publish contract to final verdict (accept_linux_validation_terminal/accept_linux_validation_terminal_with_follow_up/escalate_linux_validation_terminal_blocker/abort_linux_validation_terminal_verdict).python scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md# P2-65: publish Linux validation final verdict contract (announce_linux_validation_final_validated/announce_linux_validation_final_validated_with_follow_up/announce_linux_validation_final_blocker/abort_publish).python scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md# P2-66: archive Linux validation final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md# P2-67: converge Linux validation final publish archive contract to terminal verdict (proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation).python scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md# P2-68: publish Linux validation terminal verdict contract (announce_linux_validation_terminal_ready/announce_linux_validation_terminal_ready_with_follow_up/announce_linux_validation_terminal_blocker/abort_publish).python scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md# P2-69: converge Linux validation terminal verdict publish to terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md# P2-70: execute Linux validation terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --linux-validation-terminal-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md# P2-71: trace Linux validation terminal dispatch execution run reference (run_tracking_ready/run_tracking_missing/run_poll_failed/run_in_progress/run_completed_success/run_completed_failure).python scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --poll-timeout-seconds 600 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md# P2-72: await Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md# P2-73: terminal publish Linux validation terminal dispatch completion contract (announce_linux_validation_terminal_dispatch_completed/announce_linux_validation_terminal_dispatch_completed_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failed/abort_linux_validation_terminal_dispatch_terminal_publish).python scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md# P2-74: final verdict Linux validation terminal dispatch terminal publish contract (accept_linux_validation_terminal_dispatch/accept_linux_validation_terminal_dispatch_with_follow_up/keep_linux_validation_terminal_dispatch_in_progress/escalate_linux_validation_terminal_dispatch_blocker/escalate_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict).python scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md# P2-75: publish Linux validation terminal dispatch final verdict contract (announce_linux_validation_terminal_dispatch_validated/announce_linux_validation_terminal_dispatch_validated_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict_publish).python scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md# P2-76: archive Linux validation terminal dispatch final verdict publish contract (archive_linux_validation_terminal_dispatch_validated/archive_linux_validation_terminal_dispatch_validated_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_publish_archive).python scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md# P2-77: terminal verdict Linux validation terminal dispatch final publish archive contract (proceed_linux_validation_terminal_dispatch/proceed_linux_validation_terminal_dispatch_with_follow_up/hold_linux_validation_terminal_dispatch_in_progress/halt_linux_validation_terminal_dispatch_blocker/halt_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78: publish Linux validation terminal dispatch terminal verdict contract (announce_linux_validation_terminal_dispatch_ready/announce_linux_validation_terminal_dispatch_ready_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79: archive Linux validation terminal dispatch terminal verdict publish contract (archive_linux_validation_terminal_dispatch_ready/archive_linux_validation_terminal_dispatch_ready_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 一键串�?P2-17 -> P2-79 CI workflow 全链门禁(P2-26,Linux CI 总入口)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --dry-runpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --fail-fast --sync-write --command-guard-write --dispatch-trace-poll-now --completion-poll-interval-seconds 20 --completion-max-polls 15 --target-environment production --release-channel stable --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --release-trace-poll-now --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --on-release-hold pass --follow-up-repo acme/demo --follow-up-label release-follow-up# 仅执行后半链(从治理发布开始)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance# 仅执�?P2-29 终局发布(复用已产出�?completion 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执�?P2-30 release handoff(复用已产出�?terminal publish 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --target-environment staging --release-channel canary --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执�?P2-31 release trigger(复用已产出�?release handoff 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --skip-release-trace --skip-release-completion# 仅执�?P2-32 release trace(复用已产出�?release trigger 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --release-trace-poll-now --skip-release-completion# 仅执�?P2-33 release completion(复用已产出�?release trace 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --skip-release-terminal-publish# 仅执�?P2-34 release terminal publish(复用已产出�?release completion 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-finalization --skip-release-closure --skip-release-archive# 仅执�?P2-35 release finalization(复用已产出�?release terminal publish 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --on-release-hold fail --skip-release-closure --skip-release-archive# 仅执�?P2-36 release closure(复用已产出�?release finalization 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-archive# 仅执�?P2-37 release archive(复用已产出�?release closure 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-verdict --skip-release-incident# 仅执�?P2-38 release verdict(复用已产出�?release archive 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-incident# 仅执�?P2-39 release incident(复用已产出�?release verdict 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-terminal-verdict --incident-repo acme/demo --incident-label release-incident# 仅执�?P2-40 release terminal verdict(复用已产出�?release incident 报告�?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-42 only: run release delivery terminal publish (reuse release delivery report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-43 only: run release delivery final verdict (reuse release delivery terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-follow-up-dispatch# P2-44 only: run release follow-up dispatch (reuse release delivery final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict# P2-45 only: run release follow-up closure (reuse release follow-up dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-46 only: run release follow-up terminal publish (reuse release follow-up closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-47 only: run release follow-up final verdict (reuse release follow-up terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-48 only: run release final outcome (reuse P2-43 + P2-47 final verdict reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict# P2-49 only: run release final terminal publish (reuse P2-48 final outcome report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-50 only: run release final handoff (reuse P2-49 final terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-closure --skip-release-final-closure-publish# P2-51 only: run release final closure (reuse P2-50 final handoff report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure-publish# P2-52 only: run release final closure publish (reuse P2-51 final closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-archive# P2-53 only: run release final archive (reuse P2-52 final closure publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-54 only: run release final verdict (reuse P2-53 final archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive# P2-55 only: run release final verdict publish (reuse P2-54 final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict# P2-56 only: run release final publish archive (reuse P2-55 final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish# P2-57 only: run gate manifest drift closure (independent closure check over scripts/tests/manifest).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive# P2-59 only: run terminal verdict closure (reuse P2-56 + P2-57 reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift# P2-60 only: run Linux validation dispatch (reuse P2-59 terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --linux-validation-timeout-seconds 7200# P2-61 only: run Linux validation verdict (reuse P2-60 Linux validation dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch# P2-62 only: run Linux validation verdict publish (reuse P2-61 Linux validation verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict# P2-63 only: run Linux validation terminal publish (reuse P2-62 Linux validation verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish# P2-64 only: run Linux validation final verdict (reuse P2-63 Linux validation terminal publish report).# P2-65 only: run Linux validation final verdict publish (reuse P2-64 Linux validation final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publishpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict# P2-66 only: run Linux validation final publish archive (reuse P2-65 Linux validation final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish# P2-67 only: run Linux validation terminal verdict (reuse P2-66 Linux validation final publish archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive# P2-68 only: run Linux validation terminal verdict publish (reuse P2-67 Linux validation terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict# P2-69 only: run Linux validation terminal dispatch (reuse P2-68 Linux validation terminal verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish# P2-70 only: run Linux validation terminal dispatch execution (reuse P2-69 Linux validation terminal dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch# P2-71 only: run Linux validation terminal dispatch trace (reuse P2-70 Linux validation terminal dispatch execution report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion# P2-72 only: run Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-terminal-publish# P2-73 only: run Linux validation terminal dispatch terminal publish (reuse P2-72 Linux validation terminal dispatch completion report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-final-verdict# P2-74 only: run Linux validation terminal dispatch final verdict (reuse P2-73 Linux validation terminal dispatch terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish# P2-75 only: run Linux validation terminal dispatch final verdict publish (reuse P2-74 Linux validation terminal dispatch final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-final-publish-archive# P2-76 only: run Linux validation terminal dispatch final publish archive (reuse P2-75 Linux validation terminal dispatch final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-terminal-verdict# P2-77 only: run Linux validation terminal dispatch terminal verdict (reuse P2-76 Linux validation terminal dispatch final publish archive report).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78 only: run Linux validation terminal dispatch terminal verdict publish (reuse P2-77 Linux validation terminal dispatch terminal verdict report).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79 only: run Linux validation terminal dispatch terminal verdict publish archive (reuse P2-78 Linux validation terminal dispatch terminal verdict publish report).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 语法检�?python scripts/check_syntax.py --root claude_code# 运行性能 smokepython -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py# 生成性能基线python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json```本次整理时实际复核过�?```bashpython -m pytest -q -c pytest.ini```结果�?`90 passed`�?## 配置与运行时文件当前代码里常见的配置与运行时文件分布如下�?| 位置 | 用�?||---|---|| `~/.claude-code-python/base.json` | 全局基础配置 || `~/.claude-code-python/{env}.json` | 按环境分层配�?|| `~/.claude-code-python/config.json` | 用户手工覆盖配置 || `.claude-code-python.json` | 项目本地设置 || `.claude/` | 项目运行数据目录,常见内容包括任务、计划、技能、hooks、todo、team、scheduled tasks || `.mcp.json` | MCP 相关配置 || `CLAUDE.md` / `CLAUDE.local.md` | 项目指导信息,由 `/init` 初始�?|如果你要排查任务系统,优先关注:- `.claude/tasks.json`- `.claude/runtime_tasks.json`- `.claude/scheduled_tasks.json`## 示例与入口文�?- `examples/basic_usage.py`:基础调用示例- `examples/configuration.py`:配置示�?- `examples/custom_tools.py`:自定义工具示例- `examples/mcp_integration.py`:MCP 集成示例- `examples/session_management.py`:会话管理示�?## 后续文档维护规则为了避免文档再次膨胀,后续建议遵守以下规则:1. README 只做入口、摘要和导航,不再追加大型专题说明�?2. 同一主题最多保留一个“当前主文档”和一个“历史补充文档”�?3. 新的评估/总结文档必须带日期,并明确自己是否取代旧文档�?4. 超过一页的专题内容直接放到 `docs/`,README 只写摘要和链接�?5. 如果某个专题已经有主文档,后续补充优先更新原文档,而不是新建平行文档�?## 结论这次整理后,`README.md` 不再承担“所有知识都写在一处”的职责,而是成为仓库的稳定入口:- 想运行项目,先看“快速开始�?- 想看当前架构/性能/中间件状态,直接看对应主文档- 想深�?Agent �?MCP,再进入大文�?- 想追溯历史问题和修复方案,再看历史文�?这就是当前仓库文档的推荐阅读顺序和维护边界�? \ No newline at end of file diff --git a/.claude/tmp_utf8_test.txt b/.claude/tmp_utf8_test.txt new file mode 100644 index 0000000..f2ba8f8 --- /dev/null +++ b/.claude/tmp_utf8_test.txt @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/.gitignore b/.gitignore index c4ed96a..75ebee4 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,6 @@ Thumbs.db sessions/ cache/ .claude/perf_tmp/ +tests/.tmp_context/ +tests/.tmp_plugins/ +tests/.tmp_*/ diff --git a/README.md b/README.md index 439e665..9499663 100644 --- a/README.md +++ b/README.md @@ -1,262 +1 @@ -# Claude Code Python - -一个面向代码工作的 AI CLI / REPL / MCP 项目,提供 Claude Code 风格的交互、工具调用、任务运行时、Agent、多 Provider 接入,以及一套持续演进中的工程化基础设施。 - -本 README 是当前仓库的统一入口。大体量专题文档继续保留在 `docs/`,这里只保留必要摘要、明确导航和后续维护约定,避免文档再次失控。 - -## 项目定位 - -- 语言与运行时:Python 3.10+、`asyncio` -- 主入口:`python -m claude_code.main` 或 `python run.py` -- 交互模式:交互式 REPL、单次 query、管道模式、MCP 服务器模式 -- 模型接入:`anthropic`、`openai`、`ollama`、`vllm`、`deepseek`、`bedrock`、`vertex` -- 关键子系统:`QueryEngine`、`ToolRegistry`、权限系统、任务运行时、Agent、MCP、技能系统、服务层 - -当前代码中 `azure` 路径有显式 fail-fast 提示,因此这里不把它作为当前推荐运行路径。 - -## 当前整理结论 - -截至 `2026-04-17`,本仓库的 Markdown 文档可以收敛为以下结构: - -1. `README.md` 只负责统一入口,不再堆砌超长教程和实现枚举。 -2. “当前状态”优先看带日期的评估/执行文档,而不是更早的评审草稿。 -3. `docs/current/reference/AGENT.MD` 与 `docs/current/reference/MCP.md` 体量较大,保留独立文档,不再重复搬进 README。 -4. 架构、性能、中间件三条线分别保留“当前结论”与“历史细节”两层,不再并列堆叠多个同级入口。 - -本次额外核对结果: - -- 测试复核:`pytest -q -c pytest.ini` 通过,结果为 `90 passed` -- 架构最新快照:`2026-04-16` 的评测结论为“工程可用基线可推进”,评分 `8.4 / 10` -- 中间件演进快照:`Phase 0` 已完成,`Phase 1` 持续推进 -- 性能快照:`Baseline V1` 记录启动耗时约 `1109.38 ms` - -## 快速开始 - -### 1. 安装依赖 - -```bash -python -m pip install -r requirements.txt -``` - -如果你只是从源码运行,本仓库当前建议以 `requirements.txt` 和 `pytest.ini` 为准。 - -### 2. 选择 Provider - -#### Anthropic - -```bash -export CLAUDE_API_PROVIDER=anthropic -export ANTHROPIC_API_KEY=your-key -export CLAUDE_MODEL=claude-sonnet-4-20250514 -``` - -#### Ollama / 本地 OpenAI 兼容模型 - -```bash -export CLAUDE_API_PROVIDER=ollama -export OPENAI_BASE_URL=http://localhost:11434/v1 -export CLAUDE_MODEL=qwen2.5:14b -``` - -说明: - -- `openai` / `deepseek` 需要 `OPENAI_API_KEY` -- `ollama` / `vllm` 允许本地模型路径,不强制真实 API Key -- PowerShell 请将 `export` 改为 `$env:变量名="值"` - -### 3. 常用启动方式 - -```bash -# 查看帮助 -python -m claude_code.main --help - -# 版本 -python -m claude_code.main --version - -# 健康检查 -python -m claude_code.main --doctor - -# 交互式 REPL -python -m claude_code.main - -# 单次提问 -python -m claude_code.main "解释当前目录下的项目结构" - -# 管道模式 -echo "帮我总结这段代码" | python -m claude_code.main --pipe - -# 作为 MCP 服务器启动 -python -m claude_code.main --mcp-serve -``` - -## 核心能力概览 - -- 交互层:REPL、单轮 query、管道模式、命令系统 -- 工具层:文件读写、搜索、Shell/PowerShell、工作流、任务控制、技能、浏览器、LSP、MCP、Worktree、Cron、Team -- 运行时:`QueryEngine` 编排模型对话、工具调用、权限判定和上下文构建 -- 任务体系:`TaskManager`、`TaskRepository`、`TaskQueue` 支撑后台任务和任务存储演进 -- 服务层:缓存、速率限制、遥测、历史管理、记忆、加密、hooks、shutdown、token estimation -- 扩展能力:MCP server/client、Agent、技能目录、项目级 `.claude/` 运行数据 - -如果你要看完整工具清单,不再从 README 查找,直接看 `docs/current/reference/MCP.md`。 - -## 仓库结构 - -```text -claude-code-python/ -├── claude_code/ # 主代码 -│ ├── main.py # CLI 入口 -│ ├── engine/ # QueryEngine / context / session / permissions -│ ├── tools/ # 工具系统 -│ ├── commands/ # 命令系统 -│ ├── tasks/ # 任务运行时、仓储、队列 -│ ├── services/ # 缓存、遥测、历史、hooks、shutdown 等 -│ ├── mcp/ # MCP 服务端实现 -│ ├── agents/ # 内置 Agent 定义 -│ ├── skills/ # 技能系统 -│ └── state/ # 状态模型 -├── docs/ # 专题文档 -├── examples/ # 示例脚本 -├── scripts/ # 辅助脚本与性能脚本 -├── tests/ # 测试 -├── run.py # 源码运行入口 -├── requirements.txt # 依赖清单 -└── pytest.ini # pytest 配置 -``` - -## 文档地图 - -下面是整理后的推荐阅读顺序。 - -| 文档 | 角色 | 什么时候看 | 当前定位 | -|---|---|---|---| -| `README.md` | 总入口 | 第一次进入仓库 | 唯一导航入口 | -| `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 架构现状快照 | 想知道项目现在整体状态 | 架构主入口 | -| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 任务/中间件落地记录 | 想知道任务系统已改到哪一步 | 中间件现状主入口 | -| `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 性能一页纸 | 想快速看性能状态 | 性能主入口 | -| `docs/current/reference/MCP.md` | MCP 使用手册 | 想把仓库作为 MCP server 接入 | MCP 主入口 | -| `docs/current/reference/AGENT.MD` | Agent 全量手册 | 想理解 Agent、异步子任务、多智能体 | Agent 主入口 | -| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 首轮问题盘点 | 想追溯最初发现了哪些问题 | 架构历史文档 | -| `docs/history/architecture/FIX_PROPOSALS.md` | 修复方案库 | 想看历史修复设计与备选方案 | 架构历史文档 | -| `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 中间件演进评估 | 想评估 Redis/MySQL/平台化是否有必要 | 中间件决策文档 | -| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 性能路线图 | 想看阶段目标和执行计划 | 性能路线图 | -| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 性能优化明细 | 想追踪具体改了哪些热路径 | 性能细节文档 | -| `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 性能进度记录 | 想看阶段性进展 | 性能历史文档 | -| `docs/current/performance/BASELINE_V1.md` | 性能基线摘要 | 想看基线结论 | 性能数据摘要 | -| `docs/current/performance/baseline_v1.json` | 原始基线数据 | 想做对比或自动化处理 | 性能原始数据 | - -## 各主题的权威入口 - -为避免“同一主题多个入口同时有效”,后续统一按下面的主入口理解: - -- 架构现状:`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` -- 架构历史问题与修复备选:`docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md` -- 任务/中间件当前落地状态:`docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` -- 任务/中间件演进决策依据:`docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` -- 性能当前状态:`docs/current/performance/PERFORMANCE_ONE_PAGER.md` -- 性能基线数据:`docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json` -- MCP 使用:`docs/current/reference/MCP.md` -- Agent 使用:`docs/current/reference/AGENT.MD` - -## 架构、性能、中间件的摘要 - -### 架构 - -综合 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 与历史评审文档,目前可以将架构结论收敛为: - -- 方向上是合理的模块化单体 -- 关键主链路曾出现过工具暴露、权限硬闸、Agent 异步、Provider 适配等断点 -- 截至 `2026-04-16` 的评测文档,这些关键断点已完成一轮闭环修复,测试记录为 `68 passed` -- 更早的 `docs/history/architecture/ARCHITECTURE_REVIEW.md` 和 `docs/history/architecture/FIX_PROPOSALS.md` 主要用于追溯“为什么会这么改”,不再代表最新状态 - -### 中间件 / 任务系统 - -综合两份中间件文档,当前结论是: - -- 仓库原始模型是“进程内队列 + 文件持久化”,不是数据库队列架构 -- 对单机 CLI 合理,但对平台化、多实例、可恢复场景风险较高 -- 当前落地重点已从“是否立刻上 Redis/MySQL”转为“先把任务模型、仓储、队列抽象收敛” -- 已完成的工作包括:`tasks.json` schema 统一、文件锁、安全写入、durable 语义修复、`TaskRepository` / `TaskQueue` 抽象接入 - -### 性能 - -综合性能文档,当前结论是: - -- 已建立语法门禁、性能 smoke、基线脚本三件基础设施 -- 当前基线主数据见 `docs/current/performance/baseline_v1.json` -- 单页摘要优先看 `docs/current/performance/PERFORMANCE_ONE_PAGER.md` -- 详细优化项优先看 `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` - -## 开发与验证 - -```bash -# 运行测试 -python -m pytest -q -c pytest.ini - -# 语法检查 -python scripts/check_syntax.py --root claude_code - -# 运行性能 smoke -python -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py - -# 生成性能基线 -python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json -``` - -本次整理时实际复核过: - -```bash -python -m pytest -q -c pytest.ini -``` - -结果为 `90 passed`。 - -## 配置与运行时文件 - -当前代码里常见的配置与运行时文件分布如下: - -| 位置 | 用途 | -|---|---| -| `~/.claude-code-python/base.json` | 全局基础配置 | -| `~/.claude-code-python/{env}.json` | 按环境分层配置 | -| `~/.claude-code-python/config.json` | 用户手工覆盖配置 | -| `.claude-code-python.json` | 项目本地设置 | -| `.claude/` | 项目运行数据目录,常见内容包括任务、计划、技能、hooks、todo、team、scheduled tasks | -| `.mcp.json` | MCP 相关配置 | -| `CLAUDE.md` / `CLAUDE.local.md` | 项目指导信息,由 `/init` 初始化 | - -如果你要排查任务系统,优先关注: - -- `.claude/tasks.json` -- `.claude/runtime_tasks.json` -- `.claude/scheduled_tasks.json` - -## 示例与入口文件 - -- `examples/basic_usage.py`:基础调用示例 -- `examples/configuration.py`:配置示例 -- `examples/custom_tools.py`:自定义工具示例 -- `examples/mcp_integration.py`:MCP 集成示例 -- `examples/session_management.py`:会话管理示例 - -## 后续文档维护规则 - -为了避免文档再次膨胀,后续建议遵守以下规则: - -1. README 只做入口、摘要和导航,不再追加大型专题说明。 -2. 同一主题最多保留一个“当前主文档”和一个“历史补充文档”。 -3. 新的评估/总结文档必须带日期,并明确自己是否取代旧文档。 -4. 超过一页的专题内容直接放到 `docs/`,README 只写摘要和链接。 -5. 如果某个专题已经有主文档,后续补充优先更新原文档,而不是新建平行文档。 - -## 结论 - -这次整理后,`README.md` 不再承担“所有知识都写在一处”的职责,而是成为仓库的稳定入口: - -- 想运行项目,先看“快速开始” -- 想看当前架构/性能/中间件状态,直接看对应主文档 -- 想深入 Agent 或 MCP,再进入大文档 -- 想追溯历史问题和修复方案,再看历史文档 - -这就是当前仓库文档的推荐阅读顺序和维护边界。 - +# Claude Code Python一个面向代码工作的 AI CLI / REPL / MCP 项目,提?Claude Code 风格的交互、工具调用、任务运行时、Agent、多 Provider 接入,以及一套持续演进中的工程化基础设施??README 是当前仓库的统一入口。大体量专题文档继续保留?`docs/`,这里只保留必要摘要、明确导航和后续维护约定,避免文档再次失控?## 项目定位- 语言与运行时:Python 3.10+、`asyncio`- 主入口:`python -m claude_code.main` ?`python run.py`- 交互模式:交互式 REPL、单?query、管道模式、MCP 服务器模?- 模型接入:`anthropic`、`openai`、`ollama`、`vllm`、`deepseek`、`bedrock`、`vertex`- 关键子系统:`QueryEngine`、`ToolRegistry`、权限系统、任务运行时、Agent、MCP、技能系统、服务层当前代码?`azure` 路径有显?fail-fast 提示,因此这里不把它作为当前推荐运行路径?## 当前整理结论截至 `2026-04-17`,本仓库?Markdown 文档可以收敛为以下结构:1. `README.md` 只负责统一入口,不再堆砌超长教程和实现枚举?2. “当前状态”优先看带日期的评估/执行文档,而不是更早的评审草稿?3. `docs/current/reference/AGENT.MD` ?`docs/current/reference/MCP.md` 体量较大,保留独立文档,不再重复搬进 README?4. 架构、性能、中间件三条线分别保留“当前结论”与“历史细节”两层,不再并列堆叠多个同级入口?本次额外核对结果?- 测试复核:`pytest -q -c pytest.ini` 通过,结果为 `90 passed`- 架构最新快照:`2026-04-16` 的评测结论为“工程可用基线可推进”,评分 `8.4 / 10`- 中间件演进快照:`Phase 0` 已完成,`Phase 1` 持续推进- 性能快照:`Baseline V1` 记录启动耗时?`1109.38 ms`## 快速开?### 1. 安装依赖```bashpython -m pip install -r requirements.txt```如果你只是从源码运行,本仓库当前建议?`requirements.txt` ?`pytest.ini` 为准?### 2. 选择 Provider#### Anthropic```bashexport CLAUDE_API_PROVIDER=anthropicexport ANTHROPIC_API_KEY=your-keyexport CLAUDE_MODEL=claude-sonnet-4-20250514```#### Ollama / 本地 OpenAI 兼容模型```bashexport CLAUDE_API_PROVIDER=ollamaexport OPENAI_BASE_URL=http://localhost:11434/v1export CLAUDE_MODEL=qwen2.5:14b```说明?- `openai` / `deepseek` 需?`OPENAI_API_KEY`- `ollama` / `vllm` 允许本地模型路径,不强制真实 API Key- PowerShell 请将 `export` 改为 `$env:变量?"?`### Windows 推荐启动(避?WindowsApps stub?Windows 上不要依?`C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe` 这类 Store alias stub?建议优先使用?```powershellpy -3 -m claude_code.main --doctorpy -3 -m claude_code.main```或显式使用虚拟环境解释器?```powershell.\.venv\Scripts\python.exe -m claude_code.main --doctor.\.venv\Scripts\python.exe -m claude_code.main````--doctor` 会输出解释器来源与风险提示;若命?WindowsApps stub,会给出明确警告与替代启动命令?### 3. 常用启动方式```bash# 查看帮助python -m claude_code.main --help# 版本python -m claude_code.main --version# 健康检?python -m claude_code.main --doctor# 交互?REPLpython -m claude_code.main# 单次提问python -m claude_code.main "解释当前目录下的项目结构"# 管道模式echo "帮我总结这段代码" | python -m claude_code.main --pipe# 作为 MCP 服务器启?python -m claude_code.main --mcp-serve# 启动本地 daemon / API 控制面(P1-01?python -m claude_code.main --daemon-serve --daemon-host 127.0.0.1 --daemon-port 8787# CLI ?thin-client ?daemon(P1-06?python -m claude_code.main --daemon-client "explain current repo status"echo "summarize changed files" | python -m claude_code.main --pipe --daemon-client```## 核心能力概览- 交互层:REPL、单?query、管道模式、命令系?- 工具层:文件读写、搜索、Shell/PowerShell、工作流、任务控制、技能、浏览器、LSP、MCP、Worktree、Cron、Team- 运行时:`QueryEngine` 编排模型对话、工具调用、权限判定和上下文构?- 任务体系:`TaskManager`、`TaskRepository`、`TaskQueue` 支撑后台任务和任务存储演?- 服务层:缓存、速率限制、遥测、历史管理、记忆、加密、hooks、shutdown、token estimation- 扩展能力:MCP server/client、Agent、技能目录、项目级 `.claude/` 运行数据如果你要看完整工具清单,不再?README 查找,直接看 `docs/current/reference/MCP.md`?## 仓库结构```textclaude-code-python/├── claude_code/ # 主代?? ├── main.py # CLI 入口? ├── engine/ # QueryEngine / context / session / permissions? ├── tools/ # 工具系统? ├── commands/ # 命令系统? ├── tasks/ # 任务运行时、仓储、队?? ├── services/ # 缓存、遥测、历史、hooks、shutdown ?? ├── mcp/ # MCP 服务端实?? ├── agents/ # 内置 Agent 定义? ├── skills/ # 技能系?? └── state/ # 状态模?├── docs/ # 专题文档├── examples/ # 示例脚本├── scripts/ # 辅助脚本与性能脚本├── tests/ # 测试├── run.py # 源码运行入口├── requirements.txt # 依赖清单└── pytest.ini # pytest 配置```## 文档地图下面是整理后的推荐阅读顺序?| 文档 | 角色 | 什么时候看 | 当前定位 ||---|---|---|---|| `README.md` | 总入?| 第一次进入仓?| 唯一导航入口 || `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` | 架构现状快照 | 想知道项目现在整体状?| 架构主入?|| `docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md` | 产品战略与超车路线图 | 想知道项目如何从“可用底盘”走到“超?Claude Code?| 战略主入?|| `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` | 执行计划与任务卡?| 想直接开工并明确每张卡的研发范围、完成标准、测试深?| 执行主入?|| `docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md` | 任务/中间件落地记?| 想知道任务系统已改到哪一?| 中间件现状主入口 || `docs/current/performance/PERFORMANCE_ONE_PAGER.md` | 性能一页纸 | 想快速看性能状?| 性能主入?|| `docs/current/reference/MCP.md` | MCP 使用手册 | 想把仓库作为 MCP server 接入 | MCP 主入?|| `docs/current/reference/AGENT.MD` | Agent 全量手册 | 想理?Agent、异步子任务、多智能?| Agent 主入?|| `docs/history/architecture/ARCHITECTURE_REVIEW.md` | 首轮问题盘点 | 想追溯最初发现了哪些问题 | 架构历史文档 || `docs/history/architecture/FIX_PROPOSALS.md` | 修复方案?| 想看历史修复设计与备选方?| 架构历史文档 || `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` | 中间件演进评?| 想评?Redis/MySQL/平台化是否有必要 | 中间件决策文?|| `docs/history/performance/PERFORMANCE_TOP_TIER_EVALUATION_PLAN.md` | 性能路线?| 想看阶段目标和执行计?| 性能路线?|| `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md` | 性能优化明细 | 想追踪具体改了哪些热路径 | 性能细节文档 || `docs/history/performance/OPTIMIZATION_PROGRESS.md` | 性能进度记录 | 想看阶段性进?| 性能历史文档 || `docs/current/performance/BASELINE_V1.md` | 性能基线摘要 | 想看基线结论 | 性能数据摘要 || `docs/current/performance/baseline_v1.json` | 原始基线数据 | 想做对比或自动化处理 | 性能原始数据 |## 各主题的权威入口为避免“同一主题多个入口同时有效”,后续统一按下面的主入口理解:- 架构现状:`docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md`- 产品战略 / 超越 Claude Code 路线图:`docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md`- 执行计划 / 任务卡单:`docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 架构历史问题与修复备选:`docs/history/architecture/ARCHITECTURE_REVIEW.md` + `docs/history/architecture/FIX_PROPOSALS.md`- 任务/中间件当前落地状态:`docs/current/middleware/MIDDLEWARE_EVOLUTION_EXECUTION_2026-04-16.md`- 任务/中间件演进决策依据:`docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md`- 性能当前状态:`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 性能基线数据:`docs/current/performance/BASELINE_V1.md` + `docs/current/performance/baseline_v1.json`- MCP 使用:`docs/current/reference/MCP.md`- Agent 使用:`docs/current/reference/AGENT.MD`## 架构、性能、中间件的摘?### 架构综合 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` 与历史评审文档,目前可以将架构结论收敛为?- 方向上是合理的模块化单体- 关键主链路曾出现过工具暴露、权限硬闸、Agent 异步、Provider 适配等断?- 截至 `2026-04-16` 的评测文档,这些关键断点已完成一轮闭环修复,测试记录?`68 passed`- 更早?`docs/history/architecture/ARCHITECTURE_REVIEW.md` ?`docs/history/architecture/FIX_PROPOSALS.md` 主要用于追溯“为什么会这么改”,不再代表最新状?### 中间?/ 任务系统综合两份中间件文档,当前结论是:- 仓库原始模型是“进程内队列 + 文件持久化”,不是数据库队列架?- 对单?CLI 合理,但对平台化、多实例、可恢复场景风险较高- 当前落地重点已从“是否立刻上 Redis/MySQL”转为“先把任务模型、仓储、队列抽象收敛?- 已完成的工作包括:`tasks.json` schema 统一、文件锁、安全写入、durable 语义修复、`TaskRepository` / `TaskQueue` 抽象接入### 性能综合性能文档,当前结论是?- 已建立语法门禁、性能 smoke、基线脚本三件基础设施- 当前基线主数据见 `docs/current/performance/baseline_v1.json`- 单页摘要优先?`docs/current/performance/PERFORMANCE_ONE_PAGER.md`- 详细优化项优先看 `docs/history/performance/PERFORMANCE_OPTIMIZATION_REPORT.md`## 开发与验证```bash# 运行测试python -m pytest -q -c pytest.ini# 运行 Phase 0 主链回归门禁(提交前/合并前)python scripts/run_phase0_gate.py# 运行 Phase 1 控制面回归门禁(P1-01?python scripts/run_p1_control_plane_gate.py# 运行 Phase 1 事件日志回归门禁(P1-02?python scripts/run_p1_event_journal_gate.py# 运行 Phase 1 SQLite 状态后端回归门禁(P1-03?python scripts/run_p1_sqlite_state_backend_gate.py# 运行 Phase 1 Active Memory 回归门禁(P1-04?python scripts/run_p1_active_memory_gate.py# 运行 Phase 1 Hook/Permission/Audit 收敛门禁(P1-05?python scripts/run_p1_hook_permission_audit_gate.py# 运行 Phase 1 CLI thin-client 迁移门禁(P1-06?python scripts/run_p1_cli_thin_client_gate.py# 运行 Phase 2 ?Agent Supervisor 门禁(P2-01?python scripts/run_p2_multi_agent_supervisor_gate.py# 运行 Phase 2 Artifact Bus 门禁(P2-02?python scripts/run_p2_artifact_bus_gate.py# 运行 Phase 2 IDE 集成门禁(P2-03?python scripts/run_p2_ide_integration_gate.py# 运行 Phase 2 GitHub/CI workflow 门禁(P2-04?python scripts/run_p2_github_ci_workflow_gate.py# 运行 Phase 2 组织策略与审计门禁(P2-05?python scripts/run_p2_org_policy_audit_gate.py# 运行 Phase 2 Agent 运行时一致性门禁(P2-07?python scripts/run_p2_agent_runtime_parity_gate.py# 运行 Phase 2 自定?Agent 目录加载门禁(P2-08?python scripts/run_p2_custom_agents_loader_gate.py# 运行 Phase 2 JetBrains IDE 集成门禁(P2-10?python scripts/run_p2_jetbrains_ide_integration_gate.py# 运行 Phase 0-2 Linux 统一验尸门禁(P2-06?python scripts/run_linux_unified_gate.py# 运行 Phase 2 Linux 统一验尸执行门禁(P2-09,Linux 执行?python scripts/run_p2_linux_unified_execution_gate.py --dry-runpython scripts/run_p2_linux_unified_execution_gate.py --continue-on-failure# 分片并行示例? ?job 中的?2 片)python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2 --continue-on-failure# 聚合多分?summary 并给出全局通过/失败判定(P2-12,Linux 回收阶段执行?python scripts/run_p2_linux_shard_aggregation_gate.py --artifacts-dir .claude/reportspython scripts/run_p2_linux_shard_aggregation_gate.py --summary-glob ".claude/reports/**/summary.json"# ?merged summary 发布最终报告(P2-13,Linux 回收阶段执行?python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.jsonpython scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md# 一键串联执?>汇?>发布全链门禁(P2-14,Linux 编排入口?python scripts/run_p2_linux_unified_pipeline_gate.py --dry-runpython scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure --fail-fast# 仅做汇?发布(跳过执行阶段)python scripts/run_p2_linux_unified_pipeline_gate.py --skip-execution --summary-glob ".claude/reports/**/summary.json"# 生成 Linux 分片执行计划(P2-15,供 CI fan-out job 直接消费?python scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --dry-run --print-commandspython scripts/run_p2_linux_shard_plan_gate.py --shard-total 4 --output .claude/reports/linux_unified_gate/shard_plan.json --continue-on-failure -- -k runtime# 生成 Linux CI matrix 产物(P2-16,供 CI matrix 直接消费?python scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --dry-run --print-matrixpython scripts/run_p2_linux_ci_matrix_gate.py --shard-plan .claude/reports/linux_unified_gate/shard_plan.json --output .claude/reports/linux_unified_gate/ci_matrix.json --skip-empty-shards# 生成 Linux CI workflow 计划产物(P2-17,供 fan-out/fan-in job 直接消费?python scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --dry-run --print-planpython scripts/run_p2_linux_ci_workflow_plan_gate.py --ci-matrix .claude/reports/linux_unified_gate/ci_matrix.json --output .claude/reports/linux_unified_gate/ci_workflow_plan.json# ?workflow 计划渲染?GitHub Actions YAML(P2-18,供 CI 直接执行?python scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-yamlpython scripts/run_p2_linux_ci_workflow_yaml_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --output-workflow .github/workflows/linux_unified_gate.yml --output-metadata .claude/reports/linux_unified_gate/ci_workflow_render.json# 校验/同步 workflow 渲染产物漂移(P2-19,防?plan->yaml 漂移?python scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --print-diffpython scripts/run_p2_linux_ci_workflow_sync_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --write# 校验/规范?workflow 计划中的命令契约(P2-20,防?command/parts/路径 漂移?python scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_command_guard_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --write --output .claude/reports/linux_unified_gate/ci_workflow_command_guard.json# 汇?workflow 漂移 + 命令 + 元数据血缘治理(P2-21,生成单一治理结论?python scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_gate.py --ci-workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --metadata-path .claude/reports/linux_unified_gate/ci_workflow_render.json --output .claude/reports/linux_unified_gate/ci_workflow_governance.json# 发布治理结论?CI 可消费决策(P2-22,生?should_execute_workflow 与发布报告)python scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_governance_publish_gate.py --governance-report .claude/reports/linux_unified_gate/ci_workflow_governance.json --output-json .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_governance_publish.md# 将治理发布结果收敛为执行决策闸门(P2-23,统一 execute/blocked 判定与退出策略)python scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_decision_gate.py --governance-publish .claude/reports/linux_unified_gate/ci_workflow_governance_publish.json --workflow-plan .claude/reports/linux_unified_gate/ci_workflow_plan.json --workflow-path .github/workflows/linux_unified_gate.yml --on-block skip --output-json .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_execution_decision.md# 将执行决策收敛为可调度命令契约(P2-24,统一 dispatch-ready/blocked ?gh workflow run 命令?python scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_gate.py --execution-decision .claude/reports/linux_unified_gate/ci_workflow_execution_decision.json --workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch.md# 执行调度契约并输出最终执行回执(P2-25,统一 dispatched/blocked/fail 的执行结果)python scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py --dispatch-report .claude/reports/linux_unified_gate/ci_workflow_dispatch.json --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md# ?dispatch 执行结果收敛?run trace 契约(P2-27,统一 run_id/url/poll 命令与状态)python scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py --dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md# 等待 dispatch run 收敛为最终完成判定(P2-28,统一 completed/in_progress/timeout/failure 语义?python scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py --dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md# 发布 dispatch completion 的终局结论(P2-29,统一 promote/hold 发布语义?python scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py --dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md# ?terminal publish 收敛?release handoff 契约(P2-30,统一 release job 触发语义?python scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_handoff_gate.py --terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json --target-environment production --release-channel stable --output-json .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_handoff.md# 执行 release 最终触发门禁(P2-31,统一 release dispatch 执行语义?python scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trigger_gate.py --release-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_handoff.json --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trigger.md# 追踪 release trigger 执行后的 run 语义(P2-32,统一 release run trace/poll 状态)python scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_trace_gate.py --release-trigger-report .claude/reports/linux_unified_gate/ci_workflow_release_trigger.json --poll-now --project-root . --output-json .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_trace.md# 等待 release run 收敛为最终完成判定(P2-33,统一 release 完成态语义)python scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_completion_gate.py --release-trace-report .claude/reports/linux_unified_gate/ci_workflow_release_trace.json --project-root . --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_completion.md# 发布 release completion 的终局结论(P2-34,统一 release finalize/hold 语义?python scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py --release-completion-report .claude/reports/linux_unified_gate/ci_workflow_release_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md# ?release terminal publish 收敛为最?finalization 决策(P2-35,统一 finalize/hold/abort 语义?python scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_finalization_gate.py --release-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json --on-hold pass --output-json .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_finalization.md# ?release finalization 收敛为最?closure 发布契约(P2-36,统一 close/notify/rollback 语义?python scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_closure_gate.py --release-finalization-report .claude/reports/linux_unified_gate/ci_workflow_release_finalization.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_closure.md# ?release closure 收敛为最终证据归档契约(P2-37,统一 archive/publish/block 语义?python scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_archive_gate.py --release-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_archive.md# ?release archive 收敛为最终发布判词契约(P2-38,统一 ship/hold/block 语义?python scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_verdict_gate.py --release-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_verdict.md# ?release verdict 收敛?incident dispatch 执行契约(P2-39,统一告警触发语义?python scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_incident_gate.py --release-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_verdict.json --incident-repo acme/demo --incident-label release-incident --output-json .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_incident.md# ?release incident 收敛为终局 terminal verdict 契约(P2-40,统一 ship/hold/escalate/block 语义?python scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py --release-incident-report .claude/reports/linux_unified_gate/ci_workflow_release_incident.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md# ?release terminal verdict 收敛为最?delivery 契约(P2-41,统一 deliver/hold/escalate/block 语义?python scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_gate.py --release-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery.md# P2-42: converge release delivery to terminal publish contract (announce_release/announce_hold/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py --release-delivery-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md# P2-43: converge release delivery terminal publish to final verdict contract (close_release/open_follow_up/escalate_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py --release-delivery-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md# P2-44: converge release delivery final verdict to follow-up dispatch contract (no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch).python scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md# P2-45: converge release follow-up dispatch to follow-up closure contract (no_action/queue_follow_up/queue_escalation/abort_queue).python scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py --release-follow-up-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json --follow-up-repo acme/demo --follow-up-label release-follow-up --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md# P2-46: converge release follow-up closure to terminal publish contract (announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish).python scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py --release-follow-up-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md# P2-47: converge release follow-up terminal publish to final verdict contract (close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close).python scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py --release-follow-up-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md# P2-48: converge delivery/follow-up final verdicts to one release final outcome contract (ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome).python scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py --release-delivery-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json --release-follow-up-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md# P2-49: converge release final outcome to terminal publish contract (announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py --release-final-outcome-report .claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md# P2-50: converge release final terminal publish to final handoff contract (handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff).python scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py --release-final-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md# P2-51: converge release final handoff to final closure contract (close_release/close_with_follow_up/close_blocker/abort_close).python scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py --release-final-handoff-report .claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md# P2-52: publish release final closure contract (announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py --release-final-closure-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md# P2-53: archive release final closure publish contract (archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py --release-final-closure-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md# P2-54: converge release final archive to final verdict contract (ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict).python scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py --release-final-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md# P2-55: publish release final verdict contract (announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish).python scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py --release-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md# P2-56: archive release final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py --release-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md# P2-57: close gate-script/runtime-test/manifest drift contract.python scripts/run_p2_linux_gate_manifest_drift_gate.py --dry-run --print-reportpython scripts/run_p2_linux_gate_manifest_drift_gate.py --output-json .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-markdown .claude/reports/linux_unified_gate/linux_gate_manifest_drift.md# P2-59: converge P2-56+P2-57 to one Linux terminal verdict contract.python scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --dry-run --print-reportpython scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py --release-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json --gate-manifest-drift-report .claude/reports/linux_unified_gate/linux_gate_manifest_drift.json --output-json .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md# P2-60: dispatch Linux validation command from P2-59 terminal verdict contract.python scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_dispatch_gate.py --terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json --linux-validation-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md# P2-61: converge Linux validation dispatch into one final Linux validation verdict contract.python scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_gate.py --linux-validation-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md# P2-62: publish Linux validation verdict contract (announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish).python scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_verdict_publish_gate.py --linux-validation-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md# P2-63: terminal publish Linux validation verdict publish contract (announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish).python scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_publish_gate.py --linux-validation-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md# P2-64: converge Linux validation terminal publish contract to final verdict (accept_linux_validation_terminal/accept_linux_validation_terminal_with_follow_up/escalate_linux_validation_terminal_blocker/abort_linux_validation_terminal_verdict).python scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_gate.py --linux-validation-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md# P2-65: publish Linux validation final verdict contract (announce_linux_validation_final_validated/announce_linux_validation_final_validated_with_follow_up/announce_linux_validation_final_blocker/abort_publish).python scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_final_verdict_publish_gate.py --linux-validation-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md# P2-66: archive Linux validation final verdict publish contract (archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive).python scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_final_publish_archive_gate.py --linux-validation-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md# P2-67: converge Linux validation final publish archive contract to terminal verdict (proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation).python scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_gate.py --linux-validation-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md# P2-68: publish Linux validation terminal verdict contract (announce_linux_validation_terminal_ready/announce_linux_validation_terminal_ready_with_follow_up/announce_linux_validation_terminal_blocker/abort_publish).python scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_terminal_verdict_publish_gate.py --linux-validation-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md# P2-69: converge Linux validation terminal verdict publish to terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_dispatch_gate.py --linux-validation-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md# P2-70: execute Linux validation terminal dispatch contract (dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch).python scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --dry-run --print-reportpython scripts/run_p2_lv_td_execution_gate.py --linux-validation-terminal-dispatch-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json --linux-validation-terminal-timeout-seconds 7200 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md# P2-71: trace Linux validation terminal dispatch execution run reference (run_tracking_ready/run_tracking_missing/run_poll_failed/run_in_progress/run_completed_success/run_completed_failure).python scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --dry-run --print-reportpython scripts/run_p2_lv_td_trace_gate.py --linux-validation-terminal-dispatch-execution-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json --poll-timeout-seconds 600 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md# P2-72: await Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --dry-run --print-reportpython scripts/run_p2_lv_td_completion_gate.py --linux-validation-terminal-dispatch-trace-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json --poll-interval-seconds 20 --max-polls 15 --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md# P2-73: terminal publish Linux validation terminal dispatch completion contract (announce_linux_validation_terminal_dispatch_completed/announce_linux_validation_terminal_dispatch_completed_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failed/abort_linux_validation_terminal_dispatch_terminal_publish).python scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_publish_gate.py --linux-validation-terminal-dispatch-completion-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md# P2-74: final verdict Linux validation terminal dispatch terminal publish contract (accept_linux_validation_terminal_dispatch/accept_linux_validation_terminal_dispatch_with_follow_up/keep_linux_validation_terminal_dispatch_in_progress/escalate_linux_validation_terminal_dispatch_blocker/escalate_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict).python scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_gate.py --linux-validation-terminal-dispatch-terminal-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md# P2-75: publish Linux validation terminal dispatch final verdict contract (announce_linux_validation_terminal_dispatch_validated/announce_linux_validation_terminal_dispatch_validated_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_verdict_publish).python scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_verdict_publish_gate.py --linux-validation-terminal-dispatch-final-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md# P2-76: archive Linux validation terminal dispatch final verdict publish contract (archive_linux_validation_terminal_dispatch_validated/archive_linux_validation_terminal_dispatch_validated_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_final_publish_archive).python scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_final_publish_archive_gate.py --linux-validation-terminal-dispatch-final-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md# P2-77: terminal verdict Linux validation terminal dispatch final publish archive contract (proceed_linux_validation_terminal_dispatch/proceed_linux_validation_terminal_dispatch_with_follow_up/hold_linux_validation_terminal_dispatch_in_progress/halt_linux_validation_terminal_dispatch_blocker/halt_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --dry-run --print-reportpython scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78: publish Linux validation terminal dispatch terminal verdict contract (announce_linux_validation_terminal_dispatch_ready/announce_linux_validation_terminal_dispatch_ready_with_follow_up/announce_linux_validation_terminal_dispatch_in_progress/announce_linux_validation_terminal_dispatch_blocker/announce_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79: archive Linux validation terminal dispatch terminal verdict publish contract (archive_linux_validation_terminal_dispatch_ready/archive_linux_validation_terminal_dispatch_ready_with_follow_up/archive_linux_validation_terminal_dispatch_in_progress/archive_linux_validation_terminal_dispatch_blocker/archive_linux_validation_terminal_dispatch_failure/abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --dry-run --print-reportpython scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 一键串?P2-17 -> P2-79 CI workflow 全链门禁(P2-26,Linux CI 总入口)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --dry-runpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --fail-fast --sync-write --command-guard-write --dispatch-trace-poll-now --completion-poll-interval-seconds 20 --completion-max-polls 15 --target-environment production --release-channel stable --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --release-trace-poll-now --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --on-release-hold pass --follow-up-repo acme/demo --follow-up-label release-follow-up# 仅执行后半链(从治理发布开始)python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance# 仅执?P2-29 终局发布(复用已产出?completion 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执?P2-30 release handoff(复用已产出?terminal publish 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --target-environment staging --release-channel canary --skip-release-trigger --skip-release-trace --skip-release-completion# 仅执?P2-31 release trigger(复用已产出?release handoff 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --release-workflow-path .github/workflows/release.yml --release-workflow-ref main --skip-release-trace --skip-release-completion# 仅执?P2-32 release trace(复用已产出?release trigger 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --release-trace-poll-now --skip-release-completion# 仅执?P2-33 release completion(复用已产出?release trace 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --release-completion-poll-interval-seconds 20 --release-completion-max-polls 15 --skip-release-terminal-publish# 仅执?P2-34 release terminal publish(复用已产出?release completion 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-finalization --skip-release-closure --skip-release-archive# 仅执?P2-35 release finalization(复用已产出?release terminal publish 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --on-release-hold fail --skip-release-closure --skip-release-archive# 仅执?P2-36 release closure(复用已产出?release finalization 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-archive# 仅执?P2-37 release archive(复用已产出?release closure 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-verdict --skip-release-incident# 仅执?P2-38 release verdict(复用已产出?release archive 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-incident# 仅执?P2-39 release incident(复用已产出?release verdict 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-terminal-verdict --incident-repo acme/demo --incident-label release-incident# 仅执?P2-40 release terminal verdict(复用已产出?release incident 报告?python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-42 only: run release delivery terminal publish (reuse release delivery report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch# P2-43 only: run release delivery final verdict (reuse release delivery terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-follow-up-dispatch# P2-44 only: run release follow-up dispatch (reuse release delivery final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict# P2-45 only: run release follow-up closure (reuse release follow-up dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-46 only: run release follow-up terminal publish (reuse release follow-up closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-final-verdict --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-47 only: run release follow-up final verdict (reuse release follow-up terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --follow-up-repo acme/demo --follow-up-label release-follow-up# P2-48 only: run release final outcome (reuse P2-43 + P2-47 final verdict reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict# P2-49 only: run release final terminal publish (reuse P2-48 final outcome report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-50 only: run release final handoff (reuse P2-49 final terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-closure --skip-release-final-closure-publish# P2-51 only: run release final closure (reuse P2-50 final handoff report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure-publish# P2-52 only: run release final closure publish (reuse P2-51 final closure report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-archive# P2-53 only: run release final archive (reuse P2-52 final closure publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish# P2-54 only: run release final verdict (reuse P2-53 final archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive# P2-55 only: run release final verdict publish (reuse P2-54 final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict# P2-56 only: run release final publish archive (reuse P2-55 final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish# P2-57 only: run gate manifest drift closure (independent closure check over scripts/tests/manifest).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive# P2-59 only: run terminal verdict closure (reuse P2-56 + P2-57 reports).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift# P2-60 only: run Linux validation dispatch (reuse P2-59 terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --linux-validation-timeout-seconds 7200# P2-61 only: run Linux validation verdict (reuse P2-60 Linux validation dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch# P2-62 only: run Linux validation verdict publish (reuse P2-61 Linux validation verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict# P2-63 only: run Linux validation terminal publish (reuse P2-62 Linux validation verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish# P2-64 only: run Linux validation final verdict (reuse P2-63 Linux validation terminal publish report).# P2-65 only: run Linux validation final verdict publish (reuse P2-64 Linux validation final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publishpython scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict# P2-66 only: run Linux validation final publish archive (reuse P2-65 Linux validation final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish# P2-67 only: run Linux validation terminal verdict (reuse P2-66 Linux validation final publish archive report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive# P2-68 only: run Linux validation terminal verdict publish (reuse P2-67 Linux validation terminal verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict# P2-69 only: run Linux validation terminal dispatch (reuse P2-68 Linux validation terminal verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish# P2-70 only: run Linux validation terminal dispatch execution (reuse P2-69 Linux validation terminal dispatch report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch# P2-71 only: run Linux validation terminal dispatch trace (reuse P2-70 Linux validation terminal dispatch execution report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion# P2-72 only: run Linux validation terminal dispatch completion (reuse P2-71 Linux validation terminal dispatch trace report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-terminal-publish# P2-73 only: run Linux validation terminal dispatch terminal publish (reuse P2-72 Linux validation terminal dispatch completion report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-final-verdict# P2-74 only: run Linux validation terminal dispatch final verdict (reuse P2-73 Linux validation terminal dispatch terminal publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish# P2-75 only: run Linux validation terminal dispatch final verdict publish (reuse P2-74 Linux validation terminal dispatch final verdict report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-final-publish-archive# P2-76 only: run Linux validation terminal dispatch final publish archive (reuse P2-75 Linux validation terminal dispatch final verdict publish report).python scripts/run_p2_linux_ci_workflow_pipeline_gate.py --skip-plan --skip-yaml --skip-sync --skip-command-guard --skip-governance --skip-governance-publish --skip-decision --skip-dispatch --skip-dispatch-execution --skip-dispatch-trace --skip-dispatch-completion --skip-dispatch-terminal-publish --skip-dispatch-release-handoff --skip-release-trigger --skip-release-trace --skip-release-completion --skip-release-terminal-publish --skip-release-finalization --skip-release-closure --skip-release-archive --skip-release-verdict --skip-release-incident --skip-release-terminal-verdict --skip-release-delivery --skip-release-delivery-terminal-publish --skip-release-delivery-final-verdict --skip-release-follow-up-dispatch --skip-release-follow-up-closure --skip-release-follow-up-terminal-publish --skip-release-follow-up-final-verdict --skip-release-final-outcome --skip-release-final-terminal-publish --skip-release-final-handoff --skip-release-final-closure --skip-release-final-closure-publish --skip-release-final-archive --skip-release-final-verdict --skip-release-final-verdict-publish --skip-release-final-publish-archive --skip-gate-manifest-drift --skip-terminal-verdict --skip-linux-validation-dispatch --skip-linux-validation-verdict --skip-linux-validation-verdict-publish --skip-linux-validation-terminal-publish --skip-linux-validation-final-verdict --skip-linux-validation-final-verdict-publish --skip-linux-validation-final-publish-archive --skip-linux-validation-terminal-verdict --skip-linux-validation-terminal-verdict-publish --skip-linux-validation-terminal-dispatch --skip-linux-validation-terminal-dispatch-execution --skip-linux-validation-terminal-dispatch-trace --skip-linux-validation-terminal-dispatch-completion --skip-linux-validation-terminal-dispatch-terminal-publish --skip-linux-validation-terminal-dispatch-final-verdict --skip-linux-validation-terminal-dispatch-terminal-verdict# P2-77 only: run Linux validation terminal dispatch terminal verdict (reuse P2-76 Linux validation terminal dispatch final publish archive report).python scripts/run_p2_lv_td_terminal_verdict_gate.py --linux-validation-terminal-dispatch-final-publish-archive-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md# P2-78 only: run Linux validation terminal dispatch terminal verdict publish (reuse P2-77 Linux validation terminal dispatch terminal verdict report).python scripts/run_p2_lv_td_tverdict_publish_gate.py --linux-validation-terminal-dispatch-terminal-verdict-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md# P2-79 only: run Linux validation terminal dispatch terminal verdict publish archive (reuse P2-78 Linux validation terminal dispatch terminal verdict publish report).python scripts/run_p2_lv_td_tverdict_publish_archive_gate.py --linux-validation-terminal-dispatch-terminal-verdict-publish-report .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json --output-json .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json --output-markdown .claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md# 语法检?python scripts/check_syntax.py --root claude_code# 运行性能 smokepython -m pytest -q -c pytest.ini tests/perf/test_perf_smoke.py# 生成性能基线python scripts/perf/run_baseline.py --project-root . --iterations 20 --output docs/current/performance/baseline_v1.json```本次整理时实际复核过?```bashpython -m pytest -q -c pytest.ini```结果?`90 passed`?## 配置与运行时文件当前代码里常见的配置与运行时文件分布如下?| 位置 | 用?||---|---|| `~/.claude-code-python/base.json` | 全局基础配置 || `~/.claude-code-python/{env}.json` | 按环境分层配?|| `~/.claude-code-python/config.json` | 用户手工覆盖配置 || `.claude-code-python.json` | 项目本地设置 || `.claude/` | 项目运行数据目录,常见内容包括任务、计划、技能、hooks、todo、team、scheduled tasks || `.mcp.json` | MCP 相关配置 || `CLAUDE.md` / `CLAUDE.local.md` | 项目指导信息,由 `/init` 初始?|如果你要排查任务系统,优先关注:- `.claude/tasks.json`- `.claude/runtime_tasks.json`- `.claude/scheduled_tasks.json`## 示例与入口文?- `examples/basic_usage.py`:基础调用示例- `examples/configuration.py`:配置示?- `examples/custom_tools.py`:自定义工具示例- `examples/mcp_integration.py`:MCP 集成示例- `examples/session_management.py`:会话管理示?## 后续文档维护规则为了避免文档再次膨胀,后续建议遵守以下规则:1. README 只做入口、摘要和导航,不再追加大型专题说明?2. 同一主题最多保留一个“当前主文档”和一个“历史补充文档”?3. 新的评估/总结文档必须带日期,并明确自己是否取代旧文档?4. 超过一页的专题内容直接放到 `docs/`,README 只写摘要和链接?5. 如果某个专题已经有主文档,后续补充优先更新原文档,而不是新建平行文档?## 结论这次整理后,`README.md` 不再承担“所有知识都写在一处”的职责,而是成为仓库的稳定入口:- 想运行项目,先看“快速开始?- 想看当前架构/性能/中间件状态,直接看对应主文档- 想深?Agent ?MCP,再进入大文?- 想追溯历史问题和修复方案,再看历史文?这就是当前仓库文档的推荐阅读顺序和维护边界? \ No newline at end of file diff --git a/claude_code/agents/builtin.py b/claude_code/agents/builtin.py index 1855915..deb5413 100644 --- a/claude_code/agents/builtin.py +++ b/claude_code/agents/builtin.py @@ -111,6 +111,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Be concise and practical - Show code changes clearly - Ask clarifying questions when needed""", + memory="project", ), "editor": AgentDefinition( agent_type="editor", @@ -130,6 +131,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Be careful with indentation - Make one change at a time - Don't add unnecessary comments or formatting""", + memory="project", ), "reviewer": AgentDefinition( agent_type="reviewer", @@ -153,6 +155,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Suggested fix Be constructive and specific.""", + memory="project", ), "debugger": AgentDefinition( agent_type="debugger", @@ -172,6 +175,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Read relevant code thoroughly - Test potential fixes - Explain the root cause""", + memory="project", ), "tester": AgentDefinition( agent_type="tester", @@ -192,6 +196,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Test edge cases - Follow testing best practices - Make tests readable and maintainable""", + memory="project", ), "researcher": AgentDefinition( agent_type="researcher", @@ -213,6 +218,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Provide sources when available - Summarize key points clearly - Present options with tradeoffs""", + memory="user", ), "architect": AgentDefinition( agent_type="architect", @@ -233,6 +239,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Propose multiple approaches - Explain tradeoffs - Support decisions with rationale""", + memory="project", ), "docs-writer": AgentDefinition( agent_type="docs-writer", @@ -253,6 +260,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Use examples - Follow documentation best practices - Keep docs up to date""", + memory="project", ), "quick": AgentDefinition( agent_type="quick", @@ -269,6 +277,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: - Quick lookups Be fast and concise. Don't overthink.""", + memory="local", ), "deep": AgentDefinition( agent_type="deep", @@ -287,6 +296,7 @@ def create_builtin_agents() -> dict[str, AgentDefinition]: Be thorough. Take time to understand the full context. Consider multiple approaches before deciding.""", + memory="project", ), } @@ -306,4 +316,4 @@ def setup_builtin_agents() -> None: "list_agents", "create_builtin_agents", "setup_builtin_agents", -] \ No newline at end of file +] diff --git a/claude_code/agents/loader.py b/claude_code/agents/loader.py new file mode 100644 index 0000000..7bda206 --- /dev/null +++ b/claude_code/agents/loader.py @@ -0,0 +1,157 @@ +"""Custom agent loader for .claude/agents markdown definitions.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Optional + +import yaml + +from claude_code.agents.builtin import AgentDefinition + + +def _normalize_string_list(value: Any, *, default: list[str]) -> list[str]: + """Normalize string list values from frontmatter.""" + if value is None: + return list(default) + if isinstance(value, str): + text = value.strip() + return [text] if text else list(default) + if isinstance(value, list): + result: list[str] = [] + for item in value: + if isinstance(item, str): + text = item.strip() + if text: + result.append(text) + return result if result else list(default) + return list(default) + + +def _parse_agent_markdown(filepath: Path) -> Optional[AgentDefinition]: + """Parse a single agent markdown file with YAML frontmatter.""" + try: + content = filepath.read_text(encoding="utf-8") + except Exception: + return None + + if not content.startswith("---"): + return None + + parts = content.split("---", 2) + if len(parts) < 3: + return None + + try: + frontmatter = yaml.safe_load(parts[1]) + except yaml.YAMLError: + return None + + if not isinstance(frontmatter, dict): + return None + + name = frontmatter.get("name") + description = frontmatter.get("description") + prompt = parts[2].strip() + + if not isinstance(name, str) or not name.strip(): + return None + if not isinstance(description, str) or not description.strip(): + return None + if not prompt: + return None + + model = frontmatter.get("model", "sonnet") + if not isinstance(model, str) or not model.strip(): + model = "sonnet" + + tools = _normalize_string_list(frontmatter.get("tools"), default=["*"]) + disallowed_tools = _normalize_string_list( + frontmatter.get("disallowedTools", frontmatter.get("disallowed_tools")), + default=[], + ) + + background_raw = frontmatter.get("background", False) + background = bool(background_raw) if isinstance(background_raw, (bool, int)) else False + + permission_mode = frontmatter.get("permissionMode", frontmatter.get("permission_mode", "acceptEdits")) + if not isinstance(permission_mode, str) or not permission_mode.strip(): + permission_mode = "acceptEdits" + + max_turns_raw = frontmatter.get("maxTurns", frontmatter.get("max_turns")) + max_turns: int | None = None + if isinstance(max_turns_raw, int) and max_turns_raw > 0: + max_turns = max_turns_raw + + color = frontmatter.get("color") + if not isinstance(color, str) or not color.strip(): + color = None + + isolation = frontmatter.get("isolation") + if not isinstance(isolation, str) or isolation.strip() != "worktree": + isolation = None + else: + isolation = "worktree" + + memory = frontmatter.get("memory") + if not isinstance(memory, str): + memory = None + else: + memory = memory.strip().lower() + if memory not in {"user", "project", "local"}: + memory = None + + omit_claude_md_raw = frontmatter.get( + "omitClaudeMd", + frontmatter.get("omit_claude_md", False), + ) + omit_claude_md = bool(omit_claude_md_raw) if isinstance(omit_claude_md_raw, (bool, int)) else False + + return AgentDefinition( + agent_type=name.strip(), + description=description.strip(), + prompt=prompt, + model=model.strip(), + background=background, + isolation=isolation, + permission_mode=permission_mode.strip(), + color=color, + tools=tools, + disallowed_tools=disallowed_tools, + max_turns=max_turns, + memory=memory, + omit_claude_md=omit_claude_md, + ) + + +def _discover_agent_directories(cwd: str | Path) -> list[Path]: + """Discover .claude/agents directories from cwd up to git root.""" + resolved = Path(cwd).resolve() + parent_chain = [resolved, *resolved.parents] + found: list[Path] = [] + + for current in parent_chain: + agent_dir = current / ".claude" / "agents" + if agent_dir.is_dir(): + found.append(agent_dir) + if (current / ".git").exists(): + break + + # Load outermost first so deeper paths override duplicate agent names. + return list(reversed(found)) + + +def load_agents_from_directory(cwd: str | Path) -> dict[str, AgentDefinition]: + """Load custom agents from .claude/agents directories.""" + agents: dict[str, AgentDefinition] = {} + for agent_dir in _discover_agent_directories(cwd): + for md_file in sorted(agent_dir.glob("*.md")): + parsed = _parse_agent_markdown(md_file) + if parsed is None: + continue + agents[parsed.agent_type] = parsed + return agents + + +__all__ = ["load_agents_from_directory"] + diff --git a/claude_code/app.py b/claude_code/app.py index e6814a8..e724bf3 100644 --- a/claude_code/app.py +++ b/claude_code/app.py @@ -5,6 +5,7 @@ """ import asyncio +import inspect import signal import sys from dataclasses import dataclass @@ -55,10 +56,7 @@ ) # Import logger -from claude_code.utils.logging_system import ( - get_logger, - configure_logging, -) +from claude_code.utils.logging import get_logger, set_log_level, setup_logging class AppPhase(Enum): @@ -175,7 +173,8 @@ async def _setup(self) -> None: self._logger.info("Initializing %s v%s", self.config.service_name, self.config.version) # 1. Setup logging - configure_logging(self.config.log_level) + setup_logging(verbose=self.config.log_level.upper() == "DEBUG") + set_log_level(self.config.log_level.lower()) # 2. Initialize DI Container self._container = ServiceContainer() @@ -200,9 +199,6 @@ async def _setup(self) -> None: self._shutdown_manager = ShutdownManager(shutdown_config) await self._shutdown_manager.start() - # 6. Register signal handlers - self._register_signal_handlers() - self._phase = AppPhase.RUNNING self._running = True self._logger.info("Application initialized successfully") @@ -218,6 +214,8 @@ async def _teardown(self, error: Optional[Exception]) -> None: # Run cleanup callbacks for name, cleanup in self._services.items(): + if cleanup is None: + continue try: if asyncio.iscoroutinefunction(cleanup): await cleanup() @@ -277,6 +275,7 @@ def register_factory( self, name: str, factory: Callable[..., Any], + service_type: Optional[type] = None, cleanup: Optional[Callable[[], Any]] = None, ) -> None: """Register a service factory. @@ -284,11 +283,22 @@ def register_factory( Args: name: Service name. factory: Service factory function. + service_type: Service type used for DI resolution. cleanup: Optional cleanup callback. """ self._services[name] = cleanup + resolved_type = service_type + if resolved_type is None: + signature = inspect.signature(factory) + annotation = signature.return_annotation + if annotation is not inspect.Signature.empty and isinstance(annotation, type): + resolved_type = annotation + if resolved_type is None: + raise ValueError( + "register_factory requires service_type when factory has no concrete return annotation" + ) self._container.register_factory( - type(next(iter([factory()]))), # Get type from factory result + resolved_type, factory, ServiceLifecycle.SINGLETON, ) diff --git a/claude_code/commands/auth/__init__.py b/claude_code/commands/auth/__init__.py index 826409c..610441a 100644 --- a/claude_code/commands/auth/__init__.py +++ b/claude_code/commands/auth/__init__.py @@ -10,9 +10,73 @@ from __future__ import annotations import os -from typing import Any from claude_code.commands.base import Command, CommandContext, CommandResult, CommandType +from claude_code.config import get_config, save_config + + +_OPENAI_FAMILY_PROVIDERS: set[str] = {"openai", "ollama", "vllm", "deepseek"} +_KEYLESS_PROVIDERS: set[str] = {"bedrock", "vertex"} +_ALL_KEY_ENV_VARS: tuple[str, ...] = ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "AZURE_OPENAI_API_KEY") + + +def _resolve_provider() -> str: + """Resolve active provider using runtime precedence.""" + config = get_config() + provider = os.getenv("CLAUDE_API_PROVIDER", getattr(config, "api_provider", "anthropic")) + return str(provider).lower() + + +def _provider_env_key(provider: str) -> str | None: + """Return provider-specific API key environment variable name.""" + if provider == "anthropic": + return "ANTHROPIC_API_KEY" + if provider in _OPENAI_FAMILY_PROVIDERS: + return "OPENAI_API_KEY" + if provider == "azure": + return "AZURE_OPENAI_API_KEY" + return None + + +def _is_config_key_usable(provider: str, config_provider: str) -> bool: + """Check whether persisted config.api_key is applicable for active provider.""" + if provider == "anthropic": + return config_provider == "anthropic" + if provider in _OPENAI_FAMILY_PROVIDERS: + return config_provider in _OPENAI_FAMILY_PROVIDERS + if provider == "azure": + return config_provider == "azure" + return False + + +def _resolve_auth_key() -> tuple[str, str | None, str | None]: + """Resolve runtime authentication key and its source. + + Returns: + Tuple of (provider, key, source) where source is "env", "config", or None. + """ + config = get_config() + provider = _resolve_provider() + env_key_name = _provider_env_key(provider) + env_key_value = os.getenv(env_key_name) if env_key_name else None + + config_provider = str(getattr(config, "api_provider", "anthropic")).lower() + config_api_key = getattr(config, "api_key", None) + config_key_value = config_api_key if _is_config_key_usable(provider, config_provider) else None + + if env_key_value: + return provider, env_key_value, "env" + if config_key_value: + return provider, str(config_key_value), "config" + return provider, None, None + + +def _mask_key(key: str) -> str: + """Mask API key for display.""" + if len(key) <= 8: + return "*" * len(key) + suffix = key[-4:] if len(key) > 12 else key[-2:] + return f"{key[:8]}...{suffix}" class LoginCommand(Command): @@ -41,43 +105,52 @@ async def execute(self, args: str, context: CommandContext) -> CommandResult: Returns: CommandResult with login instructions. """ - api_key = os.environ.get("ANTHROPIC_API_KEY") - api_url = os.environ.get("ANTHROPIC_API_URL") - - if api_key: + provider, api_key, source = _resolve_auth_key() + + if provider in _KEYLESS_PROVIDERS: return CommandResult( - content="You are already logged in. Use /logout to sign out first.", + content=( + "# Login to Claude Code\n\n" + f"Provider `{provider}` uses cloud credentials and does not require an API key.\n" + "No /login action is required." + ), ) - - return CommandResult(content="""# Login to Claude Code - -To login, you need an Anthropic API key: - -1. Visit https://console.anthropic.com/settings/keys -2. Create a new API key -3. Set it as an environment variable: - export ANTHROPIC_API_KEY="sk-ant-api03-..." - -Or add it to your config: + if api_key: + source_label = "environment variable" if source == "env" else "config file" + return CommandResult( + content=( + "You are already logged in.\n\n" + f"Provider: {provider}\n" + f"Source: {source_label}\n" + f"Key: {_mask_key(api_key)}\n\n" + "Use /logout to sign out first." + ), + ) - claude config set api_key "sk-ant-api03-..." + env_key_name = _provider_env_key(provider) or "ANTHROPIC_API_KEY" + if provider == "anthropic": + login_help = ( + "1. Visit https://console.anthropic.com/settings/keys\n" + "2. Create a new API key\n" + "3. Set environment variable:\n\n" + f' export {env_key_name}="sk-ant-api03-..."\n' + ) + else: + login_help = ( + "Set your API key as environment variable:\n\n" + f' export {env_key_name}=""\n' + ) -For more login options, visit the documentation.""") - - def _check_existing_login(self) -> str | None: - """Check if user is already logged in. - - Returns: - Masked API key if logged in, None otherwise. - """ - api_key = os.environ.get("ANTHROPIC_API_KEY") - - if api_key: - if api_key.startswith("sk-ant-"): - return f"sk-ant-{'*' * 20}{api_key[-4:]}" - - return None + return CommandResult( + content=( + "# Login to Claude Code\n\n" + f"Active provider: {provider}\n\n" + f"{login_help}\n" + "Or persist in config:\n\n" + ' claude config set api_key ""\n' + ), + ) class LogoutCommand(Command): @@ -106,20 +179,28 @@ async def execute(self, args: str, context: CommandContext) -> CommandResult: Returns: CommandResult indicating logout success. """ - api_key = os.environ.get("ANTHROPIC_API_KEY") - - if not api_key: + cleared_sources: list[str] = [] + + for env_key in _ALL_KEY_ENV_VARS: + if os.environ.pop(env_key, None) is not None: + cleared_sources.append(f"env:{env_key}") + + config = get_config() + if getattr(config, "api_key", None): + config.api_key = None + save_config() + cleared_sources.append("config:api_key") + + if not cleared_sources: return CommandResult(content="You are not logged in.") - - config = ConfigManager() - saved_key = config.get("api_key") - - if saved_key: - config.set("api_key", None) - - os.environ.pop("ANTHROPIC_API_KEY", None) - - return CommandResult(content="Logged out successfully.") + + return CommandResult( + content=( + "Logged out successfully.\n\n" + "Cleared sources:\n" + + "\n".join(f"- {source}" for source in cleared_sources) + ), + ) class AuthStatusCommand(Command): @@ -147,31 +228,43 @@ async def execute(self, args: str, context: CommandContext) -> CommandResult: Returns: CommandResult with authentication status. """ - from claude_code.api.client import APIClient, APIClientConfig - - api_key = os.environ.get("ANTHROPIC_API_KEY") - - config = ConfigManager() - saved_key = config.get("api_key") - - has_key = bool(api_key or saved_key) - - if has_key: - masked = "sk-ant-***" if (api_key or saved_key) else None - return CommandResult(content=f"""# Authentication Status - -Status: Logged in -API Key: {masked} + provider, api_key, source = _resolve_auth_key() + env_key_name = _provider_env_key(provider) -To logout: claude logout -""") - else: - return CommandResult(content="""# Authentication Status + if provider in _KEYLESS_PROVIDERS: + return CommandResult( + content=( + "# Authentication Status\n\n" + "Status: Credential-based\n" + f"Provider: {provider}\n" + "API Key: not required\n\n" + "This provider uses cloud credentials instead of API keys." + ), + ) -Status: Not logged in + if api_key: + source_label = "environment variable" if source == "env" else "config file" + return CommandResult( + content=( + "# Authentication Status\n\n" + "Status: Logged in\n" + f"Provider: {provider}\n" + f"Source: {source_label}\n" + f"API Key: {_mask_key(api_key)}\n\n" + "To logout: claude logout" + ), + ) -To login, visit: https://console.anthropic.com/settings/keys -""") + return CommandResult( + content=( + "# Authentication Status\n\n" + "Status: Not logged in\n" + f"Provider: {provider}\n" + f"Expected Key: {env_key_name or 'N/A'}\n\n" + f"To login, set {env_key_name or 'an appropriate key env var'} " + "or run: claude config set api_key \"\"" + ), + ) def create_auth_commands() -> dict[str, Command]: diff --git a/claude_code/commands/compact/__init__.py b/claude_code/commands/compact/__init__.py index 57554fc..3ab207a 100644 --- a/claude_code/commands/compact/__init__.py +++ b/claude_code/commands/compact/__init__.py @@ -9,6 +9,7 @@ from __future__ import annotations +from datetime import datetime from typing import Any from claude_code.commands.base import Command, CommandContext, CommandResult, CommandType @@ -97,36 +98,74 @@ async def execute(self, args: str, context: CommandContext) -> CommandResult: CommandResult with resume information. """ session_id = args.strip() - - if context.session is None: + engine = context.engine + if engine is None: + return CommandResult(success=False, error="Engine not available") + + # No args: show recent resumable sessions. + if not session_id: + sessions = self._list_sessions(engine) + if not sessions: + return CommandResult(content="No previous sessions found.") + + lines: list[str] = ["# Available Sessions\n"] + for session in sessions[:10]: + session_time = self._format_ts(session.last_active) + lines.append(f"\n## {session.id}") + lines.append(f"Last Active: {session_time}") + lines.append(f"Messages: {session.message_count}") + lines.append(f"Working Directory: {session.working_directory}") + lines.append("\nUse /resume to continue a session.") + return CommandResult(content="\n".join(lines)) + + if not hasattr(engine, "resume_session"): return CommandResult( success=False, - error="Session not available", + error="Current engine does not support session resume", ) - - if session_id: - return CommandResult( - content=f"""Resuming session: {session_id} -Use /resume without arguments to see available sessions.""", + try: + resumed = await engine.resume_session(session_id) + except Exception as exc: + return CommandResult( + success=False, + error=f"Failed to resume session '{session_id}': {exc}", ) - - from claude_code.engine.session import SessionManager - - sessions = SessionManager.list_sessions(context.working_directory) - - if not sessions: + + if not resumed: return CommandResult( - content="No previous sessions found.", + success=False, + error=f"Session not found or unreadable: {session_id}", ) - - lines: list[str] = ["# Available Sessions\n"] - for session in sessions[:10]: - lines.append(f"\n## {session['id']}") - lines.append(f"Created: {session.get('created_at', 'unknown')}") - lines.append(f"Messages: {session.get('message_count', 0)}") - - return CommandResult(content="\n".join(lines)) + + active_session = getattr(engine.config, "session_id", session_id) + message_count = len(getattr(engine, "messages", [])) + working_directory = getattr(engine.config, "working_directory", context.working_directory) + return CommandResult( + content=( + "# Session Resumed\n\n" + f"Session ID: {active_session}\n" + f"Messages Restored: {message_count}\n" + f"Working Directory: {working_directory}" + ) + ) + + def _list_sessions(self, engine: Any) -> list[Any]: + """List resumable sessions from engine-bound manager or fallback manager.""" + from claude_code.engine.session import SessionManager + + manager = getattr(engine, "session_manager", None) + if not isinstance(manager, SessionManager): + manager = SessionManager() + return manager.list_sessions() + + @staticmethod + def _format_ts(ts: float) -> str: + """Format unix timestamp for human-readable output.""" + try: + return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return "unknown" class RewindCommand(Command): diff --git a/claude_code/commands/hooks/__init__.py b/claude_code/commands/hooks/__init__.py index 172ea53..dbfadf4 100644 --- a/claude_code/commands/hooks/__init__.py +++ b/claude_code/commands/hooks/__init__.py @@ -4,10 +4,10 @@ from __future__ import annotations -import json from pathlib import Path from claude_code.commands.base import Command, CommandContext, CommandResult, CommandType +from claude_code.services.hooks_manager import HookEvent, HooksManager class HooksCommand(Command): @@ -44,61 +44,76 @@ async def execute(self, args: str, context: CommandContext) -> CommandResult: return CommandResult(success=False, error=f"Unknown: {subcmd}") - async def _list_hooks(self, context: CommandContext) -> CommandResult: + def _resolve_manager(self, context: CommandContext) -> HooksManager: + engine = getattr(context, "engine", None) + manager = getattr(engine, "hooks_manager", None) + if ( + manager is not None + and hasattr(manager, "list_hooks") + and hasattr(manager, "add_hook") + and hasattr(manager, "remove_hook") + ): + return manager + hooks_file = Path(context.working_directory) / ".claude" / "hooks.json" - - if not hooks_file.exists(): - return CommandResult(content="No hooks configured") - - with open(hooks_file) as f: - hooks = json.load(f) - + return HooksManager(config_path=hooks_file) + + def _parse_event(self, raw_event: str) -> HookEvent | None: + normalized = raw_event.strip().lower().replace("-", "_") + for event in HookEvent: + if normalized == event.value: + return event + return None + + def _next_hook_name(self, manager: HooksManager) -> str: + existing = {hook.name for hook in manager.list_hooks(include_disabled=True)} + candidate = 1 + while True: + name = f"hook-{candidate}" + if name not in existing: + return name + candidate += 1 + + async def _list_hooks(self, context: CommandContext) -> CommandResult: + manager = self._resolve_manager(context) + hooks = manager.list_hooks(include_disabled=True) if not hooks: return CommandResult(content="No hooks configured") - + lines = ["# Hooks\n"] - for name, config in hooks.items(): - lines.append(f"\n## {name}") - lines.append(f"Event: {config.get('event', 'N/A')}") - lines.append(f"Command: {config.get('command', 'N/A')}") + for hook in hooks: + lines.append(f"\n## {hook.name}") + lines.append(f"Event: {hook.event.value}") + lines.append(f"Command: {hook.command}") + lines.append(f"Enabled: {hook.enabled}") + lines.append(f"Timeout: {hook.timeout_seconds}s") return CommandResult(content="\n".join(lines)) async def _add_hook(self, event: str, command: str, context: CommandContext) -> CommandResult: - hooks_dir = Path(context.working_directory) / ".claude" - hooks_dir.mkdir(exist_ok=True) - hooks_file = hooks_dir / "hooks.json" - - hooks = {} - if hooks_file.exists(): - with open(hooks_file) as f: - hooks = json.load(f) - - name = f"hook-{len(hooks) + 1}" - hooks[name] = {"event": event, "command": command} - - with open(hooks_file, "w") as f: - json.dump(hooks, f, indent=2) - + parsed_event = self._parse_event(event) + if parsed_event is None: + allowed_events = ", ".join(h.value for h in HookEvent) + return CommandResult( + success=False, + error=f"Invalid event '{event}'. Allowed events: {allowed_events}", + ) + + manager = self._resolve_manager(context) + name = self._next_hook_name(manager) + manager.add_hook( + name=name, + event=parsed_event, + command=command, + ) return CommandResult(content=f"Added hook: {name}") async def _remove_hook(self, name: str, context: CommandContext) -> CommandResult: - hooks_file = Path(context.working_directory) / ".claude" / "hooks.json" - - if not hooks_file.exists(): - return CommandResult(success=False, error="No hooks configured") - - with open(hooks_file) as f: - hooks = json.load(f) - - if name not in hooks: + manager = self._resolve_manager(context) + removed = manager.remove_hook(name) + if not removed: return CommandResult(success=False, error=f"Hook not found: {name}") - - del hooks[name] - - with open(hooks_file, "w") as f: - json.dump(hooks, f, indent=2) - + return CommandResult(content=f"Removed hook: {name}") diff --git a/claude_code/commands/review/__init__.py b/claude_code/commands/review/__init__.py index 1d2f583..732a33a 100644 --- a/claude_code/commands/review/__init__.py +++ b/claude_code/commands/review/__init__.py @@ -8,7 +8,10 @@ from __future__ import annotations +import shlex + from claude_code.commands.base import Command, CommandContext, CommandResult, CommandType +from claude_code.services.review_service import ReviewResult, ReviewService, ReviewServiceError class ReviewCommand(Command): @@ -23,37 +26,82 @@ def __init__(self) -> None: super().__init__( name="review", description="Review code changes", - command_type=CommandType.PROMPT, + command_type=CommandType.LOCAL, ) async def execute(self, args: str, context: CommandContext) -> CommandResult: """Execute the review command. Args: - args: Command arguments (unused for review command). + args: Optional file paths (space separated, supports quoted paths). context: The command execution context. Returns: - CommandResult with review prompt. + CommandResult with structured review findings. """ - return CommandResult(content="""# Code Review + review_service = ReviewService() + cleaned_args = args.strip() -I'll review the code changes in this session. + try: + if not cleaned_args: + result = review_service.review_git_changes(context.working_directory) + return CommandResult(content=self._render_result(result, "current git diff")) -Please tell me what files or changes you'd like me to review, or I can check: + targets = shlex.split(cleaned_args) + if not targets: + return CommandResult(success=False, error="Usage: /review [file1 file2 ...]") -1. Recent git changes -2. Modified files in this session -3. Specific files you mention + result = review_service.review_files(context.working_directory, targets) + return CommandResult(content=self._render_result(result, "specified files")) + except ReviewServiceError as exc: + return CommandResult(success=False, error=str(exc)) + except ValueError as exc: + return CommandResult(success=False, error=f"Invalid arguments: {exc}") + except Exception as exc: + return CommandResult(success=False, error=f"Review failed: {exc}") -What would you like me to review?""") - - def get_commands(self) -> dict[str, Command]: - """Get command mappings including aliases.""" - commands: dict[str, Command] = {f"/{self.name}": self} - for alias in self.aliases: - commands[f"/{alias}"] = self - return commands + def _render_result(self, result: ReviewResult, scope_label: str) -> str: + """Render structured review result as markdown output.""" + lines: list[str] = [ + "# Code Review", + "", + f"Scope: {scope_label}", + f"Files Reviewed: {len(result.files_reviewed)}", + f"Findings: {len(result.findings)}", + ] + + if not result.files_reviewed and result.scope == "git_diff": + lines.extend( + [ + "", + "No changes found to review.", + "", + "Tip: modify files and run `/review` again, or run `/review ` for explicit paths.", + ] + ) + return "\n".join(lines) + + if result.warnings: + lines.extend(["", "## Warnings"]) + for warning in result.warnings: + lines.append(f"- {warning}") + + if not result.findings: + lines.extend(["", "No findings detected in the reviewed scope."]) + return "\n".join(lines) + + lines.extend(["", "## Findings"]) + for index, finding in enumerate(result.findings, start=1): + lines.extend( + [ + "", + f"{index}. [{finding.severity.upper()}] {finding.file_path}:{finding.line}", + f" - Issue: {finding.issue}", + f" - Recommendation: {finding.recommendation}", + ] + ) + + return "\n".join(lines) def create_review_command() -> ReviewCommand: diff --git a/claude_code/config.py b/claude_code/config.py index 8726fec..2b3b352 100644 --- a/claude_code/config.py +++ b/claude_code/config.py @@ -37,7 +37,18 @@ from claude_code.permissions import PermissionMode, PERMISSION_MODES # Constant sets using frozenset -VALID_PROVIDERS: frozenset[str] = frozenset({"anthropic", "openai", "bedrock", "vertex", "azure"}) +VALID_PROVIDERS: frozenset[str] = frozenset( + { + "anthropic", + "openai", + "ollama", + "vllm", + "deepseek", + "bedrock", + "vertex", + "azure", + } +) VALID_PERMISSION_MODES: frozenset[str] = frozenset(PERMISSION_MODES) SUPPORTED_AWS_REGIONS: frozenset[str] = frozenset({ "us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1", "ap-northeast-1" @@ -104,6 +115,7 @@ class ProviderConfig(BaseModel): vertex_location: str | None = None azure_endpoint: str | None = None azure_api_version: str = DEFAULT_AZURE_API_VERSION + openai_base_url: str | None = None @field_validator('aws_region') @classmethod @@ -159,6 +171,7 @@ class Config(BaseModel): vertex_location: str | None = None azure_endpoint: str | None = None azure_api_version: str = DEFAULT_AZURE_API_VERSION + openai_base_url: str | None = None # Behavior settings permission_mode: PermissionMode = PermissionMode.DEFAULT @@ -319,6 +332,8 @@ def update_from_env(self) -> None: Reads from standard environment variables: - ANTHROPIC_API_KEY + - OPENAI_API_KEY + - OPENAI_BASE_URL - CLAUDE_API_PROVIDER - CLAUDE_MODEL - CLAUDE_MAX_TOKENS @@ -334,16 +349,24 @@ def _set(attr: str, value: Any) -> None: object.__setattr__(self, attr, value) # API settings - if api_key := os.getenv("ANTHROPIC_API_KEY"): - _set("api_key", api_key) - if provider := os.getenv("CLAUDE_API_PROVIDER"): if provider in VALID_PROVIDERS: _set("api_provider", provider) + provider_value = str(getattr(self, "api_provider", "anthropic")).lower() + if provider_value == "anthropic": + if api_key := os.getenv("ANTHROPIC_API_KEY"): + _set("api_key", api_key) + elif provider_value in {"openai", "ollama", "vllm", "deepseek"}: + if api_key := os.getenv("OPENAI_API_KEY"): + _set("api_key", api_key) + if model := os.getenv("CLAUDE_MODEL"): _set("model", model) + if openai_base_url := os.getenv("OPENAI_BASE_URL"): + _set("openai_base_url", openai_base_url) + if max_tokens := os.getenv("CLAUDE_MAX_TOKENS"): try: tokens = int(max_tokens) diff --git a/claude_code/engine/context.py b/claude_code/engine/context.py index 41225fa..ab99802 100644 --- a/claude_code/engine/context.py +++ b/claude_code/engine/context.py @@ -6,6 +6,7 @@ from __future__ import annotations import json +import logging import os import re import subprocess @@ -15,6 +16,8 @@ from pathlib import Path from typing import Any +logger = logging.getLogger(__name__) + @dataclass(frozen=True, slots=True) class ContextParts: @@ -250,7 +253,7 @@ def find_claude_md_files(self) -> list[Path]: files: list[Path] = [] current = self._working_dir - while current and current != current.parent: + while True: md_path = current / "CLAUDE.md" if md_path.is_file(): files.append(md_path) @@ -259,8 +262,16 @@ def find_claude_md_files(self) -> list[Path]: break current = current.parent + files.reverse() return files + def _display_path(self, filepath: Path) -> str: + """Return display path relative to working dir when possible.""" + try: + return str(filepath.relative_to(self._working_dir)) + except ValueError: + return os.path.relpath(filepath, self._working_dir) + def load_content(self) -> str | None: """Load combined CLAUDE.md content. @@ -280,9 +291,10 @@ def load_content(self) -> str | None: try: content = filepath.read_text(encoding="utf-8").strip() if content: - rel_path = filepath.relative_to(self._working_dir) + rel_path = self._display_path(filepath) contents.append(f"# {rel_path}\n\n{content}") - except Exception: + except (OSError, UnicodeError) as exc: + logger.warning("Failed to load %s: %s", filepath, exc) continue if contents: @@ -501,4 +513,4 @@ def _matches_pattern( input_str = json.dumps(input_data) return bool(re.search(input_pattern, input_str)) - return True \ No newline at end of file + return True diff --git a/claude_code/engine/query.py b/claude_code/engine/query.py index c1a1c96..694a2ab 100644 --- a/claude_code/engine/query.py +++ b/claude_code/engine/query.py @@ -27,6 +27,7 @@ from claude_code.tools import Tool, ToolRegistry, ToolContext, ToolResult, ToolDefinition from claude_code.tools.registry import create_default_registry from claude_code.permissions import create_permission_checker +from claude_code.services.hooks_manager import HookEvent class StopReason(Enum): @@ -154,6 +155,8 @@ class QueryConfig: always_allow: list[str] = field(default_factory=list) always_deny: list[str] = field(default_factory=list) session_id: Optional[str] = None + working_directory: Optional[str] = None + memory_scope: str = "project" @dataclass @@ -206,6 +209,31 @@ def __init__( # Ensure model-visible tool list is populated from registry by default. self._ensure_configured_tools() + + def _record_event( + self, + *, + event_type: str, + payload: Optional[dict[str, Any]] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + """Best-effort event journal write for runtime audit trail.""" + journal = getattr(self, "event_journal", None) + append_event = getattr(journal, "append_event", None) + if not callable(append_event): + return + try: + append_event( + event_type=event_type, + payload=dict(payload or {}), + session_id=self.config.session_id, + conversation_id=self.conversation_id, + source="query_engine", + metadata=dict(metadata or {}), + ) + except Exception: + # Journal persistence must not interrupt query execution. + pass def _ensure_configured_tools(self) -> None: """Populate query config tools from registry when not explicitly set.""" @@ -245,6 +273,23 @@ async def query( # Add user message user_message = self._create_user_message(user_input, attachments) self.messages.append(user_message) + self._sync_message_to_active_session(user_message) + self._record_event( + event_type="query.started", + payload={ + "input": user_input, + "has_attachments": bool(attachments), + "memory_scope": self.config.memory_scope, + }, + ) + self._record_active_memory_hit(query_text=user_input) + self._record_event( + event_type="message.user", + payload={ + "message_id": user_message.id, + "content": user_message.content, + }, + ) yield user_message # Build API messages @@ -309,7 +354,15 @@ async def query( # Add to conversation self.messages.append(assistant_message) + self._sync_message_to_active_session(assistant_message) api_messages.append(assistant_message.to_param()) + self._record_event( + event_type="message.assistant", + payload={ + "message_id": assistant_message.id, + "content": assistant_message.content, + }, + ) yield assistant_message # Extract tool uses @@ -317,6 +370,14 @@ async def query( if not tool_uses: # No tool calls - done + self._record_event( + event_type="query.completed", + payload={ + "stop_reason": StopReason.END_TURN.value, + "message_count": len(self.messages), + "tool_call_count": len(self.tool_results), + }, + ) yield { "type": "stop_reason", "reason": StopReason.END_TURN.value @@ -333,6 +394,7 @@ async def query( yield result tool_result_message = self._create_tool_result_message(result) self.messages.append(tool_result_message) + self._sync_message_to_active_session(tool_result_message) api_messages.append(tool_result_message.to_param()) else: # Multiple tools — execute in parallel when safe @@ -354,6 +416,7 @@ async def query( yield result tool_result_message = self._create_tool_result_message(result) self.messages.append(tool_result_message) + self._sync_message_to_active_session(tool_result_message) api_messages.append(tool_result_message.to_param()) else: # Mixed or unsafe tools — execute sequentially @@ -364,10 +427,29 @@ async def query( yield result tool_result_message = self._create_tool_result_message(result) self.messages.append(tool_result_message) + self._sync_message_to_active_session(tool_result_message) api_messages.append(tool_result_message.to_param()) except Exception as e: + await self._trigger_hook_event( + HookEvent.ON_ERROR, + { + "source": "query_engine", + "phase": "query_loop", + "conversation_id": self.conversation_id, + "session_id": self.config.session_id or self.conversation_id, + "working_directory": self.config.working_directory or os.getcwd(), + "model": self.config.model, + "error": str(e), + }, + ) yield {"type": "error", "error": str(e)} + self._record_event( + event_type="query.error", + payload={ + "error": str(e), + }, + ) yield {"type": "stop_reason", "reason": StopReason.ERROR.value} break @@ -544,28 +626,361 @@ def _build_tools(self) -> list[ToolParam]: self._cached_config_hash = config_hash self._cached_tools = tools return tools + + def _sync_message_to_active_session(self, message: Message) -> None: + """Persist runtime message into active session source-of-truth.""" + try: + from claude_code.engine.session import SessionManager + except Exception: + return + + manager = getattr(self, "session_manager", None) + if not isinstance(manager, SessionManager): + return + + session = manager.get_current_session() + target_session_id = self.config.session_id + if target_session_id and (session is None or session.id != target_session_id): + session = manager.switch_session(target_session_id) + + if session is None: + session = manager.ensure_current_session() + self.config.session_id = session.id + + if self.config.working_directory: + session.metadata.working_directory = self.config.working_directory + if self.config.model: + session.metadata.model = self.config.model + + if session.has_message(message.id): + return + + message_metadata = { + "conversation_id": self.conversation_id, + "source": "query_engine", + } + if message.name: + message_metadata["name"] = message.name + if message.tool_input is not None: + message_metadata["tool_input"] = message.tool_input + + session.add_message( + role=message.role, + content=message.content, + id=message.id, + timestamp=message.timestamp, + tool_call_id=message.tool_call_id, + tool_name=message.tool_name, + metadata=message_metadata, + ) + + def _record_active_memory_hit(self, *, query_text: str) -> None: + """Record active memory match information for runtime observability.""" + memory = getattr(self, "memory", None) + if memory is None: + return + snapshot_scoped = getattr(memory, "snapshot_scoped", None) + if not callable(snapshot_scoped): + return + scope = self.config.memory_scope or "project" + try: + snapshot = snapshot_scoped( + scope, + working_directory=self.config.working_directory, + limit=20, + ) + except Exception: + return + if not snapshot: + return + query_terms = {part.lower() for part in query_text.split() if part.strip()} + matched_keys = sorted( + key + for key, value in snapshot.items() + if key.lower() in query_terms + or any(term in str(value).lower() for term in query_terms) + ) + if not matched_keys: + return + self._record_event( + event_type="memory.hit", + payload={ + "scope": scope, + "matched_keys": matched_keys, + "matched_count": len(matched_keys), + }, + ) + + def _archive_session_to_history(self, session: Any) -> int: + """Archive persisted session into history storage.""" + try: + from claude_code.services.history_manager import HistoryManager + except Exception: + return 0 + + history_manager = getattr(self, "history_manager", None) + if not isinstance(history_manager, HistoryManager): + return 0 + + messages = [ + { + "id": message.id, + "role": message.role, + "content": message.content, + "timestamp": message.timestamp, + "tool_call_id": message.tool_call_id, + "tool_name": message.tool_name, + "metadata": message.metadata, + } + for message in getattr(session, "messages", []) + ] + if not messages: + return 0 + + session_metadata = getattr(session, "metadata", None) + working_directory = getattr(session_metadata, "working_directory", None) + model = getattr(session_metadata, "model", None) + session_id = getattr(session, "id", None) + if not session_id: + return 0 + + return history_manager.archive_session_messages( + session_id=session_id, + messages=messages, + working_directory=working_directory, + model=model, + ) + + async def _trigger_hook_event( + self, + event: HookEvent, + context: dict[str, Any], + ) -> list[Any]: + """Trigger hook event if runtime hooks manager is attached.""" + hooks_manager = getattr(self, "hooks_manager", None) + if hooks_manager is None or not hasattr(hooks_manager, "trigger"): + return [] + + try: + return await hooks_manager.trigger(event, context) + except Exception: + return [] + + def _serialize_hook_results(self, results: list[Any]) -> list[dict[str, Any]]: + """Normalize hook trigger results for event journal payloads.""" + serialized: list[dict[str, Any]] = [] + for item in results: + to_dict = getattr(item, "to_dict", None) + if callable(to_dict): + try: + data = to_dict() + if isinstance(data, dict): + serialized.append(data) + continue + except Exception: + pass + + hook_name = getattr(item, "hook_name", None) + success = getattr(item, "success", None) + output = getattr(item, "output", None) + error = getattr(item, "error", None) + duration_ms = getattr(item, "duration_ms", None) + serialized.append( + { + "hook_name": hook_name if hook_name is not None else "", + "success": bool(success) if success is not None else False, + "output": output if output is not None else "", + "error": error, + "duration_ms": float(duration_ms) if duration_ms is not None else 0.0, + } + ) + return serialized + + def _record_hook_execution_event( + self, + *, + event: HookEvent, + hook_context: dict[str, Any], + results: list[Any], + ) -> None: + """Record hook execution outcomes into event journal.""" + self._record_event( + event_type="hook.execution", + payload={ + "hook_event": event.value, + "tool_name": hook_context.get("tool_name"), + "tool_use_id": hook_context.get("tool_use_id"), + "phase": hook_context.get("phase"), + "results": self._serialize_hook_results(results), + }, + ) + + def _build_tool_hook_context( + self, + tool_use: ToolUse, + *, + phase: str, + duration_ms: Optional[float] = None, + result: Optional[ToolResult] = None, + error: Optional[str] = None, + ) -> dict[str, Any]: + """Build normalized hook context payload for tool lifecycle events.""" + payload: dict[str, Any] = { + "source": "query_engine", + "phase": phase, + "conversation_id": self.conversation_id, + "session_id": self.config.session_id or self.conversation_id, + "working_directory": self.config.working_directory or os.getcwd(), + "model": self.config.model, + "tool_name": tool_use.name, + "tool_use_id": tool_use.id, + "tool_input": tool_use.input, + } + if duration_ms is not None: + payload["duration_ms"] = duration_ms + if result is not None: + payload["is_error"] = result.is_error + payload["tool_output"] = result.content + if error: + payload["error"] = error + return payload + + async def _emit_tool_outcome_hooks( + self, + tool_use: ToolUse, + result: ToolResult, + duration_ms: float, + *, + error_message: Optional[str] = None, + ) -> None: + """Emit after_tool and on_error hooks for tool completion.""" + after_context = self._build_tool_hook_context( + tool_use, + phase=HookEvent.AFTER_TOOL.value, + duration_ms=duration_ms, + result=result, + ) + after_results = await self._trigger_hook_event( + HookEvent.AFTER_TOOL, + after_context, + ) + self._record_hook_execution_event( + event=HookEvent.AFTER_TOOL, + hook_context=after_context, + results=after_results, + ) + + if result.is_error: + error_context = self._build_tool_hook_context( + tool_use, + phase=HookEvent.ON_ERROR.value, + duration_ms=duration_ms, + result=result, + error=error_message or str(result.content), + ) + error_results = await self._trigger_hook_event( + HookEvent.ON_ERROR, + error_context, + ) + self._record_hook_execution_event( + event=HookEvent.ON_ERROR, + hook_context=error_context, + results=error_results, + ) async def _execute_tool(self, tool_use: ToolUse) -> ToolCallResult: """Execute a tool call.""" start_time = time.time() + permission_checker = create_permission_checker( + mode=self.config.permission_mode, + always_allow=list(self.config.always_allow), + always_deny=list(self.config.always_deny), + ) + permission_evaluation = permission_checker.evaluate(tool_use.name) + self._record_event( + event_type="permission.requested", + payload={ + "tool_use_id": tool_use.id, + "tool_name": tool_use.name, + "tool_input": tool_use.input, + "permission_mode": self.config.permission_mode, + }, + ) + self._record_event( + event_type="permission.decided", + payload={ + "tool_use_id": tool_use.id, + "tool_name": tool_use.name, + "allowed": permission_evaluation.allowed, + "decision_reason": permission_evaluation.reason, + "permission_mode": self.config.permission_mode, + }, + ) + self._record_event( + event_type="tool.started", + payload={ + "tool_use_id": tool_use.id, + "tool_name": tool_use.name, + "tool_input": tool_use.input, + }, + ) + before_context = self._build_tool_hook_context( + tool_use, + phase=HookEvent.BEFORE_TOOL.value, + ) + before_results = await self._trigger_hook_event( + HookEvent.BEFORE_TOOL, + before_context, + ) + self._record_hook_execution_event( + event=HookEvent.BEFORE_TOOL, + hook_context=before_context, + results=before_results, + ) + + async def _finalize(result: ToolResult, error_message: Optional[str] = None) -> ToolCallResult: + duration_ms = (time.time() - start_time) * 1000 + await self._emit_tool_outcome_hooks( + tool_use, + result, + duration_ms, + error_message=error_message, + ) + event_type = "tool.failed" if result.is_error else "tool.completed" + self._record_event( + event_type=event_type, + payload={ + "tool_use_id": tool_use.id, + "tool_name": tool_use.name, + "duration_ms": duration_ms, + "is_error": result.is_error, + "tool_output": result.content, + "error": error_message, + }, + ) + return ToolCallResult( + tool_use=tool_use, + result=result, + duration_ms=duration_ms, + ) # Get tool from registry tool = self.tool_registry.get(tool_use.name) if not tool: - return ToolCallResult( - tool_use=tool_use, - result=ToolResult( - content=f"Unknown tool: {tool_use.name}", + error_message = f"Unknown tool: {tool_use.name}" + return await _finalize( + ToolResult( + content=error_message, is_error=True, tool_use_id=tool_use.id, ), - duration_ms=0, + error_message=error_message, ) # Create context context = ToolContext( - working_directory=os.getcwd(), + working_directory=self.config.working_directory or os.getcwd(), environment={}, abort_signal=self.abort_event, permission_mode=self.config.permission_mode, @@ -573,57 +988,60 @@ async def _execute_tool(self, tool_use: ToolUse) -> ToolCallResult: always_deny=list(self.config.always_deny), model=self.config.model, session_id=self.config.session_id or self.conversation_id, + memory_scope=self.config.memory_scope, + conversation_id=self.conversation_id, + api_provider=( + getattr(getattr(self.api_client, "config", None), "provider", None).value + if hasattr(getattr(getattr(self.api_client, "config", None), "provider", None), "value") + else str(getattr(getattr(self.api_client, "config", None), "provider", "")) + ), + task_manager=getattr(self, "task_manager", None), + tool_registry=self.tool_registry, ) - - permission_checker = create_permission_checker( - mode=self.config.permission_mode, - always_allow=list(self.config.always_allow), - always_deny=list(self.config.always_deny), - ) - if not permission_checker.can_execute(tool_use.name): - return ToolCallResult( - tool_use=tool_use, - result=ToolResult( - content=f"Permission denied for tool: {tool_use.name} (mode={context.permission_mode})", + + if not permission_evaluation.allowed: + error_message = ( + f"Permission denied for tool: {tool_use.name} " + f"(mode={context.permission_mode})" + ) + return await _finalize( + ToolResult( + content=error_message, is_error=True, tool_use_id=tool_use.id, ), - duration_ms=(time.time() - start_time) * 1000, + error_message=error_message, ) try: # Validate input is_valid, error = tool.validate_input(tool_use.input) if not is_valid: - return ToolCallResult( - tool_use=tool_use, - result=ToolResult( - content=f"Invalid input: {error}", + error_message = f"Invalid input: {error}" + return await _finalize( + ToolResult( + content=error_message, is_error=True, tool_use_id=tool_use.id, ), - duration_ms=time.time() - start_time, + error_message=error_message, ) # Execute result = await tool.execute(tool_use.input, context) result.tool_use_id = tool_use.id - return ToolCallResult( - tool_use=tool_use, - result=result, - duration_ms=(time.time() - start_time) * 1000, - ) + return await _finalize(result) except Exception as e: - return ToolCallResult( - tool_use=tool_use, - result=ToolResult( - content=f"Tool execution error: {str(e)}", + error_message = f"Tool execution error: {str(e)}" + return await _finalize( + ToolResult( + content=error_message, is_error=True, tool_use_id=tool_use.id, ), - duration_ms=(time.time() - start_time) * 1000, + error_message=error_message, ) def interrupt(self): @@ -673,9 +1091,23 @@ def get_hooks(self) -> list[dict]: """Get configured hooks.""" return getattr(self, '_hooks', []) - async def load_skill(self, skill_name: str): + async def load_skill(self, skill_name: str) -> dict[str, Any]: """Load a skill by name.""" - pass # Placeholder - skills loading not yet implemented + from claude_code.skills.registry import get_skill_registry, load_all_skills + + load_all_skills( + project_dir=self.config.working_directory or os.getcwd(), + include_bundled=True, + ) + registry = get_skill_registry() + skill = registry.get(skill_name) + if skill is None: + raise ValueError(f"Skill not found: {skill_name}") + return { + "status": "loaded", + "skill": skill_name, + "source": str(skill.source.value if hasattr(skill.source, "value") else skill.source), + } def set_effort(self, level: str): """Set effort level (low, medium, high).""" @@ -740,5 +1172,49 @@ async def create_branch(self, branch_name: Optional[str] = None) -> dict: async def resume_session(self, session_id: Optional[str] = None) -> bool: """Resume a previous session.""" - pass # Placeholder - session loading not yet implemented + from claude_code.engine.session import SessionManager + + manager = getattr(self, "session_manager", None) + if not isinstance(manager, SessionManager): + manager = SessionManager() + self.session_manager = manager + + current_session = manager.get_current_session() + target_session_id = session_id + if target_session_id is None: + sessions = manager.list_sessions() + if not sessions: + return False + target_session_id = sessions[0].id + + if ( + current_session is not None + and target_session_id is not None + and current_session.id != target_session_id + ): + self._archive_session_to_history(current_session) + + session = manager.load_session(target_session_id) + if session is None: + return False + + restored: list[Message] = [] + for record in session.messages: + restored.append( + Message( + id=record.id, + role=record.role, + content=record.content, + timestamp=record.timestamp, + tool_call_id=record.tool_call_id, + tool_name=record.tool_name, + ) + ) + + self.messages = restored + self.tool_results = [] + self.conversation_id = session.id + self.config.session_id = session.id + if session.metadata.working_directory: + self.config.working_directory = session.metadata.working_directory return True diff --git a/claude_code/engine/session.py b/claude_code/engine/session.py index f1258f8..4c48bca 100644 --- a/claude_code/engine/session.py +++ b/claude_code/engine/session.py @@ -8,6 +8,7 @@ import json import logging import os +import sqlite3 import time from dataclasses import dataclass, field from pathlib import Path @@ -19,6 +20,8 @@ DEFAULT_STORAGE_DIR = Path.home() / ".claude-code-python" / "sessions" DEFAULT_MODEL = "claude-sonnet-4-20250514" +SESSION_STORE_SCHEMA_VERSION = 1 +SESSION_STORE_SCHEMA_KEY = "session_store_schema_version" @dataclass(slots=True) @@ -248,6 +251,151 @@ def delete_session(self, session_id: str) -> bool: return False +class SQLiteSessionStore(SessionStore): + """SQLite-backed session storage.""" + + def __init__(self, sqlite_db_path: Path | str) -> None: + self._db_path = Path(sqlite_db_path) + self._db_path.parent.mkdir(parents=True, exist_ok=True) + self._initialize_schema() + + @property + def storage_dir(self) -> Path: + return self._db_path.parent + + def save_session(self, session: "Session") -> None: + metadata_json = json.dumps(session.metadata.to_dict(), ensure_ascii=False) + messages_json = json.dumps([msg.to_dict() for msg in session.messages], ensure_ascii=False) + with self._connect() as conn: + conn.execute( + """ + INSERT INTO sessions (id, metadata_json, messages_json, last_active) + VALUES (?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + metadata_json = excluded.metadata_json, + messages_json = excluded.messages_json, + last_active = excluded.last_active + """, + ( + session.id, + metadata_json, + messages_json, + float(session.metadata.last_active), + ), + ) + + def load_session(self, session_id: str) -> Session | None: + with self._connect() as conn: + row = conn.execute( + "SELECT metadata_json, messages_json FROM sessions WHERE id = ?", + (session_id,), + ).fetchone() + if row is None: + return None + try: + metadata_payload = json.loads(row[0]) + messages_payload = json.loads(row[1]) + metadata = SessionMetadata.from_dict(metadata_payload) + messages = [MessageRecord.from_dict(item) for item in messages_payload] + return Session(metadata=metadata, messages=messages) + except Exception as e: + logger.error(f"Error loading session from sqlite: {e}") + return None + + def list_sessions(self) -> list[SessionMetadata]: + with self._connect() as conn: + rows = conn.execute( + "SELECT metadata_json FROM sessions ORDER BY last_active DESC" + ).fetchall() + sessions: list[SessionMetadata] = [] + for row in rows: + try: + payload = json.loads(row[0]) + sessions.append(SessionMetadata.from_dict(payload)) + except Exception: + continue + return sessions + + def delete_session(self, session_id: str) -> bool: + with self._connect() as conn: + cursor = conn.execute( + "DELETE FROM sessions WHERE id = ?", + (session_id,), + ) + return cursor.rowcount > 0 + + def _initialize_schema(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS session_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + metadata_json TEXT NOT NULL, + messages_json TEXT NOT NULL, + last_active REAL NOT NULL + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_sessions_last_active ON sessions (last_active DESC)" + ) + + version = self._read_schema_version(conn) + if version is None: + self._write_schema_version(conn, SESSION_STORE_SCHEMA_VERSION) + elif version > SESSION_STORE_SCHEMA_VERSION: + raise RuntimeError( + "Session SQLite schema is newer than this binary: " + f"{version} > {SESSION_STORE_SCHEMA_VERSION}" + ) + elif version < SESSION_STORE_SCHEMA_VERSION: + self._migrate_schema(conn, version, SESSION_STORE_SCHEMA_VERSION) + self._write_schema_version(conn, SESSION_STORE_SCHEMA_VERSION) + + def _connect(self) -> sqlite3.Connection: + return sqlite3.connect(self._db_path, timeout=5.0) + + @staticmethod + def _read_schema_version(conn: sqlite3.Connection) -> int | None: + row = conn.execute( + "SELECT value FROM session_metadata WHERE key = ?", + (SESSION_STORE_SCHEMA_KEY,), + ).fetchone() + if row is None: + return None + try: + return int(row[0]) + except (TypeError, ValueError): + return None + + @staticmethod + def _write_schema_version(conn: sqlite3.Connection, version: int) -> None: + conn.execute( + """ + INSERT INTO session_metadata (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """, + (SESSION_STORE_SCHEMA_KEY, str(version)), + ) + + @staticmethod + def _migrate_schema(conn: sqlite3.Connection, from_version: int, to_version: int) -> None: + if from_version < 1: + raise RuntimeError(f"Unsupported session schema version: {from_version}") + if to_version != SESSION_STORE_SCHEMA_VERSION: + raise RuntimeError(f"Unsupported target session schema version: {to_version}") + # Schema v1 is current. Future migrations will be appended here. + + class Session: """A conversation session. @@ -328,6 +476,8 @@ def add_message( ) self._messages.append(msg) self._metadata.message_count += 1 + if msg.tool_call_id or msg.tool_name: + self._metadata.tool_call_count += 1 self._metadata.last_active = time.time() if self._store: @@ -335,6 +485,10 @@ def add_message( return msg + def has_message(self, message_id: str) -> bool: + """Check whether a message with the given ID already exists.""" + return any(message.id == message_id for message in self._messages) + def add_user_message(self, content: str) -> MessageRecord: """Add a user message. @@ -450,6 +604,15 @@ def __init__(self, storage_dir: Path | str | None = None) -> None: self._current_session: Session | None = None self._sessions: dict[str, Session] = {} + @classmethod + def from_store(cls, store: SessionStore) -> "SessionManager": + """Create session manager with explicit session store.""" + manager = cls.__new__(cls) + manager._store = store + manager._current_session = None + manager._sessions = {} + return manager + @property def current_session(self) -> Session | None: """Get the current session.""" @@ -545,4 +708,4 @@ def ensure_current_session(self) -> Session: """ if not self._current_session: return self.create_session() - return self._current_session \ No newline at end of file + return self._current_session diff --git a/claude_code/features.py b/claude_code/features.py index 1309a74..cb320c7 100644 --- a/claude_code/features.py +++ b/claude_code/features.py @@ -6,8 +6,8 @@ from __future__ import annotations import os +from dataclasses import dataclass, replace from typing import Callable, Optional -from dataclasses import dataclass, field from enum import Enum @@ -42,7 +42,16 @@ class FeatureRegistry: def __init__(self): self._features: dict[Feature, FeatureConfig] = {} self._feature_checks: dict[Feature, Callable[[], bool]] = {} + self._explicit_overrides: set[Feature] = set() self._initialize_defaults() + + @staticmethod + def _env_enabled(env_var: str) -> bool: + """Resolve boolean value from environment variable.""" + value = os.environ.get(env_var) + if value is None: + return False + return str(value).strip().lower() in {"1", "true", "yes", "on"} def _initialize_defaults(self) -> None: """Initialize default feature configurations.""" @@ -61,7 +70,7 @@ def _initialize_defaults(self) -> None: ] for feature, env_var, description in defaults: - default_value = os.environ.get(env_var, "") == "1" + default_value = self._env_enabled(env_var) self._features[feature] = FeatureConfig( name=feature, env_var=env_var, @@ -80,21 +89,38 @@ def is_enabled(self, feature: Feature) -> bool: return self._feature_checks[feature]() config = self._features.get(feature) - if config: + if config is None: + env_var = f"FEATURE_{feature.value.upper()}" + return self._env_enabled(env_var) + + if feature in self._explicit_overrides: return config.enabled - - env_var = f"FEATURE_{feature.value.upper()}" - return os.environ.get(env_var, "") == "1" + if config.env_var in os.environ: + return self._env_enabled(config.env_var) + return config.default_value def enable(self, feature: Feature) -> None: """Enable a feature.""" if feature in self._features: - self._features[feature].enabled = True + self._features[feature] = replace(self._features[feature], enabled=True) + self._explicit_overrides.add(feature) def disable(self, feature: Feature) -> None: """Disable a feature.""" if feature in self._features: - self._features[feature].enabled = False + self._features[feature] = replace(self._features[feature], enabled=False) + self._explicit_overrides.add(feature) + + def _resolve_source(self, feature: Feature) -> str: + """Resolve the active source for a feature value.""" + if feature in self._feature_checks: + return "check" + if feature in self._explicit_overrides: + return "runtime" + config = self._features[feature] + if config.env_var in os.environ: + return "env" + return "default" def get_enabled_features(self) -> list[Feature]: """Get list of enabled features.""" @@ -104,9 +130,11 @@ def get_all_configs(self) -> dict[str, dict]: """Get all feature configurations.""" return { f.value: { - "enabled": config.enabled, + "enabled": self.is_enabled(f), "env_var": config.env_var, "description": config.description, + "default_value": config.default_value, + "source": self._resolve_source(f), } for f, config in self._features.items() } @@ -130,7 +158,7 @@ def feature(name: str) -> bool: return get_feature_registry().is_enabled(feature_enum) except ValueError: env_var = f"FEATURE_{name.upper()}" - return os.environ.get(env_var, "") == "1" + return FeatureRegistry._env_enabled(env_var) __all__ = [ diff --git a/claude_code/main.py b/claude_code/main.py index aac4f2b..54c9447 100644 --- a/claude_code/main.py +++ b/claude_code/main.py @@ -17,15 +17,177 @@ import asyncio import argparse import io -from typing import Optional +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Any from claude_code.api.client import APIClient, APIClientConfig, APIProvider from claude_code.engine.query import QueryEngine, QueryConfig from claude_code.engine.context import ContextBuilder +from claude_code.engine.session import SessionManager, SQLiteSessionStore from claude_code.repl import REPL, REPLConfig, PipeMode from claude_code.tools.registry import create_default_registry +from claude_code.tasks.factory import TaskBackendConfig, create_task_manager_with_event_journal +from claude_code.tasks.manager import TaskManager +from claude_code.services.event_journal import EventJournal, SQLiteEventJournal +from claude_code.services.hooks_manager import HooksManager +from claude_code.services.history_manager import HistoryManager +from claude_code.services.memory_service import SessionMemory, get_memory from claude_code.commands.registry import setup_default_commands -from claude_code.config import get_config +from claude_code.config import Config, get_config +from claude_code.app import Application, AppConfig + + +@dataclass(frozen=True, slots=True) +class RuntimeContext: + """Runtime bootstrap result shared by all execution modes.""" + + application: Application + app_config: Config + api_client: APIClient + tool_registry: Any + query_engine: QueryEngine + session_manager: SessionManager + history_manager: HistoryManager + task_manager: TaskManager + hooks_manager: HooksManager + memory: SessionMemory + event_journal: EventJournal | SQLiteEventJournal + working_directory: str + + +@dataclass(frozen=True, slots=True) +class DaemonClientMode: + """Resolved daemon thin-client mode for CLI query paths.""" + + enabled: bool + base_url: str + timeout_seconds: float + required: bool + + +def _resolve_memory_scope(raw_scope: str | None) -> str: + """Resolve runtime memory scope with safe fallback.""" + candidate = (raw_scope or "").strip().lower() + if candidate in {"user", "project", "local"}: + return candidate + return "project" + + +@dataclass(frozen=True, slots=True) +class InterpreterDiagnostics: + """Interpreter source diagnostics for doctor output.""" + + executable: str + python_on_path: Optional[str] + executable_source: str + python_on_path_source: str + has_windowsapps_stub_risk: bool + warning: Optional[str] + recommended_launcher: str + + +def _resolve_path_for_diagnostics(path: Optional[str]) -> Optional[str]: + """Resolve path for diagnostics output without requiring file existence.""" + if not path: + return None + try: + return str(Path(path).expanduser().resolve(strict=False)) + except Exception: + return str(path) + + +def _classify_interpreter_path(path: Optional[str], platform_name: Optional[str] = None) -> str: + """Classify interpreter source by path pattern.""" + if not path: + return "unknown" + + platform_value = platform_name or sys.platform + lower = path.replace("/", "\\").lower() + + if platform_value == "win32": + if "\\appdata\\local\\microsoft\\windowsapps\\python" in lower: + return "windowsapps_stub" + if "\\anaconda" in lower or "\\miniconda" in lower or "\\conda\\" in lower: + return "conda" + if "\\.venv\\" in lower or "\\venv\\" in lower: + return "virtualenv" + return "system" + + posix_lower = path.replace("\\", "/").lower() + if "/.venv/" in posix_lower or "/venv/" in posix_lower: + return "virtualenv" + if "/anaconda" in posix_lower or "/miniconda" in posix_lower or "/conda/" in posix_lower: + return "conda" + return "system" + + +def _is_windowsapps_stub(path: Optional[str], platform_name: Optional[str] = None) -> bool: + """Return True when path resolves to WindowsApps Python alias stub.""" + platform_value = platform_name or sys.platform + if platform_value != "win32": + return False + return _classify_interpreter_path(path, platform_name=platform_value) == "windowsapps_stub" + + +def _collect_interpreter_diagnostics( + *, + platform_name: Optional[str] = None, + executable_path: Optional[str] = None, + python_on_path: Optional[str] = None, +) -> InterpreterDiagnostics: + """Collect interpreter diagnostics for doctor and runtime troubleshooting.""" + platform_value = platform_name or sys.platform + resolved_executable = _resolve_path_for_diagnostics(executable_path or sys.executable) or "N/A" + resolved_python_on_path = _resolve_path_for_diagnostics( + python_on_path if python_on_path is not None else shutil.which("python") + ) + + executable_source = _classify_interpreter_path(resolved_executable, platform_name=platform_value) + python_on_path_source = _classify_interpreter_path( + resolved_python_on_path, + platform_name=platform_value, + ) + + warning: Optional[str] = None + if platform_value == "win32": + risky_locations: list[str] = [] + if _is_windowsapps_stub(resolved_executable, platform_name=platform_value): + risky_locations.append("sys.executable") + if _is_windowsapps_stub(resolved_python_on_path, platform_name=platform_value): + risky_locations.append("python on PATH") + if risky_locations: + warning = ( + "WindowsApps python.exe stub detected in " + + " + ".join(risky_locations) + + ". Use a real interpreter instead of Microsoft Store alias." + ) + + has_risk = warning is not None + if platform_value == "win32": + if has_risk: + recommended_launcher = ( + "Use `py -3 -m claude_code.main` or `\\Scripts\\python -m claude_code.main`; " + "disable App execution aliases for python/python3 if needed." + ) + else: + recommended_launcher = ( + "Use `py -3 -m claude_code.main` (preferred) or " + "`\\Scripts\\python -m claude_code.main`." + ) + else: + recommended_launcher = "Use `python -m claude_code.main` from the project virtualenv." + + return InterpreterDiagnostics( + executable=resolved_executable, + python_on_path=resolved_python_on_path, + executable_source=executable_source, + python_on_path_source=python_on_path_source, + has_windowsapps_stub_risk=has_risk, + warning=warning, + recommended_launcher=recommended_launcher, + ) def _configure_windows_console_encoding() -> None: @@ -49,15 +211,12 @@ def _configure_windows_console_encoding() -> None: pass -def setup_api_client() -> APIClient: +def setup_api_client(app_config: Optional[Config] = None) -> APIClient: """Setup the API client based on environment. - Reads from environment variables: - - CLAUDE_API_PROVIDER: anthropic/openai/ollama/vllm/deepseek/bedrock/vertex/azure - - ANTHROPIC_API_KEY: API key for Anthropic - - AWS_REGION, AWS_PROFILE: For Bedrock - - VERTEX_PROJECT, VERTEX_LOCATION: For Vertex - - AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_VERSION, AZURE_OPENAI_API_KEY: For Azure + Resolution order: + 1) Environment variables + 2) Loaded config values Returns: Configured APIClient instance. @@ -65,14 +224,19 @@ def setup_api_client() -> APIClient: Raises: ValueError: If required environment variables are missing. """ + if app_config is None: + app_config = get_config() + # Check for API provider - provider = os.getenv("CLAUDE_API_PROVIDER", "anthropic").lower() + provider = os.getenv("CLAUDE_API_PROVIDER", app_config.api_provider).lower() # Build config based on provider if provider == "anthropic": + config_api_key = app_config.api_key if app_config.api_provider == "anthropic" else None + api_key = os.getenv("ANTHROPIC_API_KEY") or config_api_key config = APIClientConfig( provider=APIProvider.ANTHROPIC, - api_key=os.getenv("ANTHROPIC_API_KEY"), + api_key=api_key, ) elif provider in ("openai", "ollama", "vllm", "deepseek"): default_urls = { @@ -80,8 +244,17 @@ def setup_api_client() -> APIClient: "vllm": "http://localhost:8000/v1", "deepseek": "https://api.deepseek.com/v1", } - base_url = os.getenv("OPENAI_BASE_URL") or default_urls.get(provider) - api_key = os.getenv("OPENAI_API_KEY") + base_url = ( + os.getenv("OPENAI_BASE_URL") + or app_config.openai_base_url + or default_urls.get(provider) + ) + config_api_key = ( + app_config.api_key + if app_config.api_provider in ("openai", "ollama", "vllm", "deepseek") + else None + ) + api_key = os.getenv("OPENAI_API_KEY") or config_api_key if provider in ("openai", "deepseek") and not api_key: raise ValueError( f"{provider} provider requires OPENAI_API_KEY to be set. " @@ -95,14 +268,18 @@ def setup_api_client() -> APIClient: elif provider == "bedrock": config = APIClientConfig( provider=APIProvider.AWS_BEDROCK, - aws_region=os.getenv("AWS_REGION", "us-east-1"), - aws_profile=os.getenv("AWS_PROFILE"), + aws_region=os.getenv("AWS_REGION") or app_config.aws_region or "us-east-1", + aws_profile=os.getenv("AWS_PROFILE") or app_config.aws_profile, ) elif provider == "vertex": config = APIClientConfig( provider=APIProvider.GOOGLE_VERTEX, - vertex_project=os.getenv("VERTEX_PROJECT"), - vertex_location=os.getenv("VERTEX_LOCATION", "us-central1"), + vertex_project=os.getenv("VERTEX_PROJECT") or app_config.vertex_project, + vertex_location=( + os.getenv("VERTEX_LOCATION") + or app_config.vertex_location + or "us-central1" + ), ) elif provider == "azure": raise ValueError( @@ -151,6 +328,22 @@ def build_system_prompt(working_dir: Optional[str] = None) -> str: - Explain what you're doing, not just what you did - When stuck, explain the issue and suggest alternatives""", ] + + runtime_scope = _resolve_memory_scope(os.getenv("CLAUDE_MEMORY_SCOPE")) + memory = get_memory() + memory_snapshot = memory.snapshot_scoped( + runtime_scope, + working_directory=working_dir or os.getcwd(), + limit=12, + ) + if memory_snapshot: + snapshot_lines = [ + "Active memory snapshot:", + f"- scope: {runtime_scope}", + ] + for key, value in memory_snapshot.items(): + snapshot_lines.append(f"- {key}: {value}") + parts.append("\n".join(snapshot_lines)) # Add context parts context_parts = builder.build_system_prompt_parts() @@ -159,48 +352,209 @@ def build_system_prompt(working_dir: Optional[str] = None) -> str: return "\n\n".join(parts) -def create_engine( +def _create_application(app_config: Config) -> Application: + """Create the canonical Application object for runtime ownership.""" + return Application( + AppConfig( + service_name="claude-code-python", + version="1.0.0", + log_level="DEBUG" if app_config.verbose else "INFO", + enable_telemetry=app_config.enable_telemetry, + ) + ) + + +def _attach_runtime_services( + engine: QueryEngine, + *, + session_manager: SessionManager, + history_manager: HistoryManager, + task_manager: TaskManager, + hooks_manager: HooksManager, + memory: SessionMemory, + event_journal: EventJournal | SQLiteEventJournal, +) -> None: + """Attach runtime services to QueryEngine for downstream command wiring.""" + engine.session_manager = session_manager # type: ignore[attr-defined] + engine.history_manager = history_manager # type: ignore[attr-defined] + engine.task_manager = task_manager # type: ignore[attr-defined] + engine.hooks_manager = hooks_manager # type: ignore[attr-defined] + engine.memory = memory # type: ignore[attr-defined] + engine.event_journal = event_journal # type: ignore[attr-defined] + + +def create_runtime( model: Optional[str] = None, working_dir: Optional[str] = None, -) -> QueryEngine: - """Create a query engine with default configuration. - - Args: - model: Optional model override. - working_dir: Optional working directory. - - Returns: - Configured QueryEngine instance. - """ - api_client = setup_api_client() - - # Get model from arg or environment - if model is None: - model = os.getenv("CLAUDE_MODEL", "claude-sonnet-4-20250514") - - # Load app config for permission propagation +) -> RuntimeContext: + """Create full runtime context through a single bootstrap path.""" + resolved_working_dir = working_dir or os.getcwd() + + # Load app config once and propagate into runtime app_config = get_config() - + api_client = setup_api_client(app_config) + + # Get model from arg, config, then environment fallback + if model is None: + model = app_config.model or os.getenv("CLAUDE_MODEL", "claude-sonnet-4-20250514") + # Build query config with permission propagation query_config = QueryConfig( model=model, max_tokens=8192, - system_prompt=build_system_prompt(working_dir), + system_prompt=build_system_prompt(resolved_working_dir), permission_mode=app_config.permission_mode.value if app_config.permission_mode else "default", always_allow=list(app_config.always_allow), always_deny=list(app_config.always_deny), + working_directory=resolved_working_dir, + memory_scope=_resolve_memory_scope(os.getenv("CLAUDE_MEMORY_SCOPE")), ) - - # Get tools from environment or use defaults + tool_registry = create_default_registry() - - engine = QueryEngine( + query_engine = QueryEngine( api_client=api_client, config=query_config, tool_registry=tool_registry, ) + + runtime_state_db = Path(resolved_working_dir) / ".claude" / "runtime_state.db" + session_store = SQLiteSessionStore(runtime_state_db) + session_manager = SessionManager.from_store(session_store) + current_session = session_manager.ensure_current_session() + current_session.metadata.working_directory = resolved_working_dir + current_session.metadata.model = model + query_engine.config.session_id = current_session.id + + history_manager = HistoryManager( + storage_path=Path(resolved_working_dir) / ".claude" / "history.json" + ) + event_journal = SQLiteEventJournal(runtime_state_db) + + task_manager = create_task_manager_with_event_journal( + TaskBackendConfig( + working_directory=resolved_working_dir, + queue_backend="memory", + runtime_backend="sqlite", + ), + event_journal=event_journal, + ) + hooks_manager = HooksManager( + config_path=Path(resolved_working_dir) / ".claude" / "hooks.json" + ) + memory = get_memory() + + _attach_runtime_services( + query_engine, + session_manager=session_manager, + history_manager=history_manager, + task_manager=task_manager, + hooks_manager=hooks_manager, + memory=memory, + event_journal=event_journal, + ) + + return RuntimeContext( + application=_create_application(app_config), + app_config=app_config, + api_client=api_client, + tool_registry=tool_registry, + query_engine=query_engine, + session_manager=session_manager, + history_manager=history_manager, + task_manager=task_manager, + hooks_manager=hooks_manager, + memory=memory, + event_journal=event_journal, + working_directory=resolved_working_dir, + ) + + +def create_engine( + model: Optional[str] = None, + working_dir: Optional[str] = None, +) -> QueryEngine: + """Create a query engine with default configuration. - return engine + Args: + model: Optional model override. + working_dir: Optional working directory. + + Returns: + Configured QueryEngine instance. + """ + runtime = create_runtime(model=model, working_dir=working_dir) + return runtime.query_engine + + +def _build_default_daemon_url(host: str, port: int) -> str: + """Build daemon base URL from host/port.""" + return f"http://{host}:{port}" + + +def _resolve_daemon_client_mode(args: argparse.Namespace) -> DaemonClientMode: + """Resolve daemon thin-client mode from CLI args and environment.""" + env_url = (os.getenv("CLAUDE_DAEMON_URL") or "").strip() + cli_url = (args.daemon_url or "").strip() + default_url = _build_default_daemon_url(args.daemon_host, args.daemon_port) + base_url = cli_url or env_url or default_url + enabled = bool(args.daemon_client or cli_url or env_url) + return DaemonClientMode( + enabled=enabled, + base_url=base_url, + timeout_seconds=float(args.daemon_timeout), + required=bool(args.daemon_required), + ) + + +async def _query_via_daemon( + query: str, + *, + base_url: str, + timeout_seconds: float, +) -> str: + """Execute a single query through daemon control-plane client.""" + from claude_code.server.control_plane import ControlPlaneClient + + client = ControlPlaneClient(base_url, timeout_seconds=timeout_seconds) + response = await asyncio.to_thread(client.query, query) + return str(response.get("output", "")) + + +async def run_pipe_mode_daemon( + *, + base_url: str, + timeout_seconds: float, +) -> int: + """Run pipe mode via daemon thin client.""" + user_input = sys.stdin.read() + if not user_input.strip(): + return 0 + output = await _query_via_daemon( + user_input, + base_url=base_url, + timeout_seconds=timeout_seconds, + ) + if output: + sys.stdout.write(output) + sys.stdout.flush() + return 0 + + +async def run_single_query_daemon( + query: str, + *, + base_url: str, + timeout_seconds: float, +) -> int: + """Run single query mode via daemon thin client.""" + output = await _query_via_daemon( + query, + base_url=base_url, + timeout_seconds=timeout_seconds, + ) + if output: + print(output) + return 0 async def run_repl( @@ -215,7 +569,7 @@ async def run_repl( verbose: Enable verbose output. system: Optional system prompt override. """ - engine = create_engine(model=model) + engine = create_engine(model=model, working_dir=os.getcwd()) # Add custom system prompt if provided if system: @@ -244,7 +598,7 @@ async def run_pipe_mode( Returns: Exit code (0 for success, non-zero for failure). """ - engine = create_engine(model=model) + engine = create_engine(model=model, working_dir=os.getcwd()) pipe = PipeMode(engine) return await pipe.run() @@ -256,7 +610,7 @@ async def run_single_query( system: Optional[str] = None, ) -> int: """Run a single query and print result to stdout.""" - engine = create_engine(model=model) + engine = create_engine(model=model, working_dir=os.getcwd()) if system: engine.config.system_prompt = system @@ -304,6 +658,27 @@ async def run_doctor() -> None: # Check Python version version = sys.version_info table.add_row("Python Version", "OK", f"{version.major}.{version.minor}.{version.micro}") + + interpreter_diag = _collect_interpreter_diagnostics() + interpreter_status = "WARN" if interpreter_diag.has_windowsapps_stub_risk else "OK" + table.add_row( + "Interpreter", + interpreter_status, + " ; ".join( + [ + f"sys.executable={interpreter_diag.executable} ({interpreter_diag.executable_source})", + ( + "python on PATH=" + + (interpreter_diag.python_on_path or "N/A") + + f" ({interpreter_diag.python_on_path_source})" + ), + ] + ), + ) + if interpreter_diag.warning: + table.add_row("Interpreter Risk", "WARN", interpreter_diag.warning) + if sys.platform == "win32": + table.add_row("Windows Launcher", "INFO", interpreter_diag.recommended_launcher) # Check API key api_key = os.getenv("ANTHROPIC_API_KEY") @@ -326,7 +701,8 @@ async def run_doctor() -> None: # Check model availability try: - engine = create_engine() + runtime = create_runtime(working_dir=cwd) + engine = runtime.query_engine table.add_row("API Connection", "OK", "Engine initialized") except Exception as e: table.add_row("API Connection", "ERROR", str(e)) @@ -390,6 +766,53 @@ def main() -> None: action="store_true", help="Run as MCP server (STDIO mode) for external MCP clients", ) + + parser.add_argument( + "--daemon-serve", + action="store_true", + help="Run as local daemon/API control plane server", + ) + + parser.add_argument( + "--daemon-host", + default="127.0.0.1", + help="Daemon bind host (default: 127.0.0.1)", + ) + + parser.add_argument( + "--daemon-port", + type=int, + default=8787, + help="Daemon bind port (default: 8787)", + ) + + parser.add_argument( + "--daemon-timeout", + type=float, + default=30.0, + help="Daemon request timeout in seconds (default: 30.0)", + ) + + parser.add_argument( + "--daemon-client", + action="store_true", + help="Use local daemon/API control plane as thin client for non-REPL query paths", + ) + + parser.add_argument( + "--daemon-url", + default=None, + help=( + "Daemon base URL for thin-client mode " + "(default from CLAUDE_DAEMON_URL or --daemon-host/--daemon-port)" + ), + ) + + parser.add_argument( + "--daemon-required", + action="store_true", + help="Fail instead of falling back to local runtime when daemon thin-client call fails", + ) parser.add_argument( "--mcp-name", @@ -434,21 +857,56 @@ def main() -> None: if args.doctor: asyncio.run(run_doctor()) return - + + if args.daemon_serve: + from claude_code.server.control_plane import run_control_plane_daemon + + runtime = create_runtime(model=args.model, working_dir=os.getcwd()) + run_control_plane_daemon( + runtime, + host=args.daemon_host, + port=args.daemon_port, + request_timeout_seconds=args.daemon_timeout, + ) + return + # Run MCP server if args.mcp_serve: from claude_code.mcp.server import run_mcp_server + runtime = create_runtime(model=args.model, working_dir=os.getcwd()) asyncio.run(run_mcp_server( working_directory=os.getcwd(), server_name=args.mcp_name, + tool_registry=runtime.tool_registry, )) return # Initialize commands setup_default_commands() + daemon_client_mode = _resolve_daemon_client_mode(args) + # Pipe mode if args.pipe: + if daemon_client_mode.enabled: + try: + exit_code = asyncio.run( + run_pipe_mode_daemon( + base_url=daemon_client_mode.base_url, + timeout_seconds=daemon_client_mode.timeout_seconds, + ) + ) + sys.exit(exit_code) + except Exception as exc: + if daemon_client_mode.required: + print(f"Error: daemon thin-client request failed: {exc}", file=sys.stderr) + sys.exit(1) + if args.verbose: + print( + f"[daemon-fallback] thin-client request failed, fallback to local runtime: {exc}", + file=sys.stderr, + ) + exit_code = asyncio.run(run_pipe_mode( model=args.model, verbose=args.verbose, @@ -460,6 +918,26 @@ def main() -> None: if query: # Single query mode + if daemon_client_mode.enabled: + try: + exit_code = asyncio.run( + run_single_query_daemon( + query=query, + base_url=daemon_client_mode.base_url, + timeout_seconds=daemon_client_mode.timeout_seconds, + ) + ) + sys.exit(exit_code) + except Exception as exc: + if daemon_client_mode.required: + print(f"Error: daemon thin-client request failed: {exc}", file=sys.stderr) + sys.exit(1) + if args.verbose: + print( + f"[daemon-fallback] thin-client request failed, fallback to local runtime: {exc}", + file=sys.stderr, + ) + exit_code = asyncio.run(run_single_query( query=query, model=args.model, diff --git a/claude_code/mcp/server.py b/claude_code/mcp/server.py index 8edc0a4..461ebda 100644 --- a/claude_code/mcp/server.py +++ b/claude_code/mcp/server.py @@ -444,6 +444,7 @@ async def start_stdio(self) -> None: async def create_mcp_server( working_directory: str = ".", server_name: str = "claude-code-python", + tool_registry: Any | None = None, ) -> MCPServer: """Create and configure an MCP server with the full tool registry. @@ -453,12 +454,11 @@ async def create_mcp_server( Args: working_directory: Working directory for tool execution server_name: Name of the MCP server + tool_registry: Optional pre-built tool registry from runtime bootstrap. Returns: Configured MCPServer instance ready to start """ - from claude_code.tools import create_default_registry - config = MCPServerConfig( name=server_name, version=DEFAULT_SERVER_VERSION, @@ -467,8 +467,11 @@ async def create_mcp_server( server = MCPServer(config) - registry = create_default_registry() - server.set_tool_registry(registry) + if tool_registry is None: + from claude_code.tools import create_default_registry + + tool_registry = create_default_registry() + server.set_tool_registry(tool_registry) return server @@ -476,6 +479,7 @@ async def create_mcp_server( async def run_mcp_server( working_directory: str = ".", server_name: str = "claude-code-python", + tool_registry: Any | None = None, ) -> None: """Run the MCP server in STDIO mode. @@ -485,8 +489,13 @@ async def run_mcp_server( Args: working_directory: Working directory for tool execution server_name: Name of the MCP server + tool_registry: Optional pre-built tool registry from runtime bootstrap. """ - server = await create_mcp_server(working_directory, server_name) + server = await create_mcp_server( + working_directory=working_directory, + server_name=server_name, + tool_registry=tool_registry, + ) await server.start_stdio() @@ -496,4 +505,4 @@ async def run_mcp_server( "MCPProtocolHandler", "create_mcp_server", "run_mcp_server", -] \ No newline at end of file +] diff --git a/claude_code/permissions.py b/claude_code/permissions.py index cfa251b..0b4b6a9 100644 --- a/claude_code/permissions.py +++ b/claude_code/permissions.py @@ -40,6 +40,15 @@ class PermissionMode(Enum): _STRICT_DENY_PREFIXES: tuple[str, ...] = () +@dataclass(frozen=True, slots=True) +class PermissionEvaluation: + """Structured permission decision for runtime audit and diagnostics.""" + + tool_name: str + allowed: bool + reason: str + + @dataclass(frozen=True, slots=True) class ToolPermissionContext: """Simplified permission context following Claw design. @@ -67,25 +76,7 @@ def blocks(self, tool_name: str) -> bool: Returns: True if tool should be blocked """ - lowered = tool_name.lower() - - # Check deny list - if lowered in self.deny_names: - return True - - for prefix in self.deny_prefixes: - if lowered.startswith(prefix.lower()): - return True - - # Check allow list - if lowered in self.allow_names: - return False - - for prefix in self.allow_prefixes: - if lowered.startswith(prefix.lower()): - return False - - return not self.default_allow + return not self.evaluate(tool_name).allowed def allows(self, tool_name: str) -> bool: """Check if tool execution is allowed. @@ -96,7 +87,31 @@ def allows(self, tool_name: str) -> bool: Returns: True if tool is allowed """ - return not self.blocks(tool_name) + return self.evaluate(tool_name).allowed + + def evaluate(self, tool_name: str) -> PermissionEvaluation: + """Return structured allow/deny decision with reason.""" + lowered = tool_name.lower() + + if lowered in self.deny_names: + return PermissionEvaluation(tool_name=tool_name, allowed=False, reason="deny_name") + + for prefix in self.deny_prefixes: + if lowered.startswith(prefix.lower()): + return PermissionEvaluation(tool_name=tool_name, allowed=False, reason="deny_prefix") + + if lowered in self.allow_names: + return PermissionEvaluation(tool_name=tool_name, allowed=True, reason="allow_name") + + for prefix in self.allow_prefixes: + if lowered.startswith(prefix.lower()): + return PermissionEvaluation(tool_name=tool_name, allowed=True, reason="allow_prefix") + + return PermissionEvaluation( + tool_name=tool_name, + allowed=bool(self.default_allow), + reason="default_allow" if self.default_allow else "default_deny", + ) @classmethod def from_rules( @@ -221,7 +236,11 @@ def can_execute(self, tool_name: str) -> bool: Returns: True if tool is allowed to execute """ - return self._context.allows(tool_name) + return self.evaluate(tool_name).allowed + + def evaluate(self, tool_name: str) -> PermissionEvaluation: + """Return structured permission decision for a tool.""" + return self._context.evaluate(tool_name) def check_or_raise(self, tool_name: str) -> None: """Check permission, raise exception if not allowed. @@ -300,6 +319,7 @@ def create_permission_checker( __all__ = [ "PermissionMode", "PERMISSION_MODES", + "PermissionEvaluation", "ToolPermissionContext", "PermissionChecker", "create_permission_checker", diff --git a/claude_code/plugins/__init__.py b/claude_code/plugins/__init__.py index b12e3dc..5c056ef 100644 --- a/claude_code/plugins/__init__.py +++ b/claude_code/plugins/__init__.py @@ -187,7 +187,9 @@ def get_all_tools(self) -> list[Any]: tools: list[Any] = [] for plugin in self.list_plugins(enabled_only=True): instance = plugin.instance - if isinstance(instance, ToolPlugin): + if instance is None: + continue + if isinstance(instance, ToolPlugin) or hasattr(instance, "get_tools"): tools.extend(instance.get_tools()) return tools @@ -195,7 +197,9 @@ def get_all_commands(self) -> list[Any]: commands: list[Any] = [] for plugin in self.list_plugins(enabled_only=True): instance = plugin.instance - if isinstance(instance, CommandPlugin): + if instance is None: + continue + if isinstance(instance, CommandPlugin) or hasattr(instance, "get_commands"): commands.extend(instance.get_commands()) return commands @@ -203,7 +207,9 @@ def get_all_hooks(self) -> dict[str, list[Callable[..., Any]]]: hooks: dict[str, list[Callable[..., Any]]] = {} for plugin in self.list_plugins(enabled_only=True): instance = plugin.instance - if isinstance(instance, HookPlugin): + if instance is None: + continue + if isinstance(instance, HookPlugin) or hasattr(instance, "get_hooks"): plugin_hooks = instance.get_hooks() for event, handlers in plugin_hooks.items(): hooks.setdefault(event, []).extend(handlers) @@ -260,6 +266,22 @@ async def load(config: dict[str, Any]) -> Optional[Plugin]: return None metadata = json.loads(metadata_file.read_text(encoding="utf-8")) + plugin_instance = None + entry_file = metadata.get("entry", "plugin.py") + entry_path = plugin_path / entry_file + if entry_path.is_file(): + module_name = ( + f"claude_code_local_plugin_{plugin_id}_" + f"{abs(hash(str(entry_path.resolve())))}" + ) + spec = importlib.util.spec_from_file_location(module_name, str(entry_path)) + if spec is not None and spec.loader is not None: + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + if hasattr(module, "get_plugin"): + plugin_instance = module.get_plugin() + return Plugin( metadata=PluginMetadata( id=metadata.get("id", plugin_id), @@ -270,6 +292,7 @@ async def load(config: dict[str, Any]) -> Optional[Plugin]: plugin_type=PluginType.LOCAL, ), root_path=plugin_path, + _instance=plugin_instance, ) diff --git a/claude_code/repl/__init__.py b/claude_code/repl/__init__.py index 2e7cb7a..8f8aade 100644 --- a/claude_code/repl/__init__.py +++ b/claude_code/repl/__init__.py @@ -24,7 +24,9 @@ from rich.panel import Panel from rich.prompt import Prompt +from claude_code.commands.base import CommandContext, CommandResult from claude_code.commands.registry import get_all_commands +from claude_code.services.hooks_manager import HookEvent @dataclass(frozen=True, slots=True) @@ -237,19 +239,190 @@ async def run_command(self, command: str) -> bool: True if command was handled, False otherwise. """ parts = command.split(maxsplit=1) - cmd = parts[0].lower() + raw_cmd = parts[0].lower() args = parts[1] if len(parts) > 1 else "" + cmd = raw_cmd[1:] if raw_cmd.startswith("/") else raw_cmd - handler = self._commands.get(cmd) + handler = self._resolve_command_handler(cmd) if handler: - return await handler.execute(self, args) + context = self._build_command_context() + await self._trigger_command_hook( + HookEvent.BEFORE_COMMAND, + self._build_command_hook_context( + command_name=cmd, + command_args=args, + phase=HookEvent.BEFORE_COMMAND.value, + ), + ) + try: + result = await handler.execute(args, context) + except Exception as exc: + error_message = f"Command '/{cmd}' failed: {exc}" + await self._trigger_command_hook( + HookEvent.ON_ERROR, + self._build_command_hook_context( + command_name=cmd, + command_args=args, + phase=HookEvent.ON_ERROR.value, + error=error_message, + ), + ) + self.print_error(error_message) + return True + + await self._trigger_command_hook( + HookEvent.AFTER_COMMAND, + self._build_command_hook_context( + command_name=cmd, + command_args=args, + phase=HookEvent.AFTER_COMMAND.value, + result=result, + ), + ) + if not result.success: + await self._trigger_command_hook( + HookEvent.ON_ERROR, + self._build_command_hook_context( + command_name=cmd, + command_args=args, + phase=HookEvent.ON_ERROR.value, + result=result, + error=result.error or "Command failed", + ), + ) + self._render_command_result(result) + return True - if cmd.startswith("/"): - self.print_error(f"Unknown command: {cmd}") + if raw_cmd.startswith("/"): + await self._trigger_command_hook( + HookEvent.ON_ERROR, + self._build_command_hook_context( + command_name=cmd, + command_args=args, + phase=HookEvent.ON_ERROR.value, + error=f"Unknown command: /{cmd}", + ), + ) + self.print_error(f"Unknown command: /{cmd}") return True return False + def _resolve_command_handler(self, name: str) -> Any | None: + """Resolve command handler by name or alias.""" + handler = self._commands.get(name) + if handler is not None: + return handler + + for candidate in self._commands.values(): + if name in getattr(candidate, "aliases", []): + return candidate + return None + + def _build_command_context(self) -> CommandContext: + """Build command execution context from current runtime state.""" + return CommandContext( + working_directory=self._config.working_directory, + console=self._console, + engine=self._engine, + session=self._build_session_payload(), + config=getattr(self._engine, "config", None), + ) + + def _build_session_payload(self) -> dict[str, Any] | None: + """Build session metadata payload for command compatibility.""" + session_manager = getattr(self._engine, "session_manager", None) + if session_manager is not None and hasattr(session_manager, "get_current_session"): + current = session_manager.get_current_session() + if current is not None and hasattr(current, "metadata"): + metadata = current.metadata + return { + "id": metadata.id, + "created_at": metadata.created_at, + "last_active": metadata.last_active, + "working_directory": metadata.working_directory, + "message_count": metadata.message_count, + "tool_call_count": metadata.tool_call_count, + } + + engine_config = getattr(self._engine, "config", None) + session_id = getattr(engine_config, "session_id", None) + if session_id is None: + return None + + return { + "id": session_id, + "message_count": len(getattr(self._engine, "messages", [])), + "tool_call_count": len(getattr(self._engine, "tool_results", [])), + } + + def _render_command_result(self, result: CommandResult) -> None: + """Render command execution result to console.""" + if result is None: + return + if not result.success: + self.print_error(result.error or "Command failed") + return + if result.is_silent: + return + if result.content: + content = str(result.content) + try: + self._console.print(Markdown(content)) + except Exception: + self._console.print(content) + + def _get_hooks_manager(self) -> Any | None: + """Resolve hooks manager from runtime engine.""" + manager = getattr(self._engine, "hooks_manager", None) + if manager is None or not hasattr(manager, "trigger"): + return None + return manager + + def _build_command_hook_context( + self, + *, + command_name: str, + command_args: str, + phase: str, + result: CommandResult | None = None, + error: str | None = None, + ) -> dict[str, Any]: + """Build normalized command hook context payload.""" + engine_config = getattr(self._engine, "config", None) + context: dict[str, Any] = { + "source": "repl", + "phase": phase, + "command_name": command_name, + "command_args": command_args, + "working_directory": self._config.working_directory, + "session_id": getattr(engine_config, "session_id", None), + "model": getattr(engine_config, "model", None), + } + if result is not None: + context["success"] = result.success + if result.content is not None: + context["result_content"] = str(result.content) + if result.error: + context["result_error"] = result.error + if error: + context["error"] = error + return context + + async def _trigger_command_hook( + self, + event: HookEvent, + context: dict[str, Any], + ) -> None: + """Trigger command hook event when hooks manager is available.""" + manager = self._get_hooks_manager() + if manager is None: + return + try: + await manager.trigger(event, context) + except Exception: + return + async def run(self) -> None: """Run the REPL loop.""" self._running = True diff --git a/claude_code/server/__init__.py b/claude_code/server/__init__.py new file mode 100644 index 0000000..07f7aa1 --- /dev/null +++ b/claude_code/server/__init__.py @@ -0,0 +1,31 @@ +"""Runtime daemon / control-plane exports.""" + +from claude_code.server.control_plane import ( + ControlPlaneClient, + ControlPlaneDaemon, + DaemonResponseError, + DaemonServerConfig, + DaemonTimeoutError, + DaemonUnavailableError, + run_control_plane_daemon, +) +from claude_code.server.ide_adapter import ( + IDEWorkspaceSnapshot, + JetBrainsClientAdapter, + JetBrainsWorkspaceSnapshot, + VSCodeClientAdapter, +) + +__all__ = [ + "ControlPlaneClient", + "ControlPlaneDaemon", + "DaemonResponseError", + "DaemonServerConfig", + "DaemonTimeoutError", + "DaemonUnavailableError", + "IDEWorkspaceSnapshot", + "JetBrainsClientAdapter", + "JetBrainsWorkspaceSnapshot", + "VSCodeClientAdapter", + "run_control_plane_daemon", +] diff --git a/claude_code/server/control_plane.py b/claude_code/server/control_plane.py new file mode 100644 index 0000000..65d02ce --- /dev/null +++ b/claude_code/server/control_plane.py @@ -0,0 +1,1638 @@ +"""Daemon/API control plane for Claude Code runtime. + +Provides: +- Local HTTP daemon entrypoint +- Runtime service dispatch (session/task/tool/query/artifact) +- Minimal stdlib client for CLI/IDE integration +""" + +from __future__ import annotations + +import asyncio +import concurrent.futures +import json +import os +import shlex +import socket +import subprocess +import threading +from dataclasses import dataclass +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any, Optional +from urllib import error as urllib_error +from urllib import parse as urllib_parse +from urllib import request as urllib_request + +from pydantic import BaseModel, Field, ValidationError + +from claude_code.permissions import create_permission_checker +from claude_code.services.github_ci_workflow import ( + GitHubCIWorkflowError, + GitHubCIWorkflowService, +) +from claude_code.services.org_policy_audit import ( + OrgPolicyAuditError, + OrgPolicyAuditService, +) +from claude_code.services.review_service import ReviewService, ReviewServiceError +from claude_code.tasks.types import BashTask, Task, TaskResult, TaskStatus, TaskType +from claude_code.tools.base import ToolContext + + +class ControlPlaneError(RuntimeError): + """Control-plane exception carrying API error metadata.""" + + def __init__( + self, + message: str, + *, + code: str = "control_plane_error", + status_code: int = 500, + details: Any = None, + ) -> None: + super().__init__(message) + self.message = message + self.code = code + self.status_code = status_code + self.details = details + + +class DaemonClientError(RuntimeError): + """Base class for control-plane client errors.""" + + +class DaemonUnavailableError(DaemonClientError): + """Raised when daemon endpoint is unreachable.""" + + +class DaemonTimeoutError(DaemonClientError): + """Raised when daemon call times out.""" + + +class DaemonResponseError(DaemonClientError): + """Raised on non-2xx daemon responses.""" + + def __init__(self, status_code: int, code: str, message: str, details: Any = None) -> None: + super().__init__(f"{status_code} {code}: {message}") + self.status_code = status_code + self.code = code + self.message = message + self.details = details + + +@dataclass(frozen=True, slots=True) +class DaemonServerConfig: + """Runtime daemon server configuration.""" + + host: str = "127.0.0.1" + port: int = 8787 + request_timeout_seconds: float = 30.0 + artifact_max_bytes: int = 512 * 1024 + + +class QueryRequest(BaseModel): + """POST /api/v1/query payload.""" + + query: str = Field(min_length=1) + session_id: str | None = None + max_events: int = Field(default=256, ge=1, le=4096) + + +class ResumeSessionRequest(BaseModel): + """POST /api/v1/sessions/resume payload.""" + + session_id: str | None = None + + +class BashTaskCreateRequest(BaseModel): + """POST /api/v1/tasks/bash payload.""" + + command: str = Field(min_length=1) + cwd: str | None = None + env: dict[str, str] = Field(default_factory=dict) + timeout: float | None = Field(default=None, gt=0) + background: bool = True + description: str | None = None + idempotency_key: str | None = None + max_retries: int = Field(default=0, ge=0, le=10) + retry_delay: float = Field(default=0.0, ge=0.0, le=60.0) + wait_timeout: float | None = Field(default=None, gt=0) + + +class ToolExecuteRequest(BaseModel): + """POST /api/v1/tools/execute payload.""" + + name: str = Field(min_length=1) + input: dict[str, Any] = Field(default_factory=dict) + permission_mode: str | None = None + session_id: str | None = None + + +class ArtifactReadRequest(BaseModel): + """GET /api/v1/artifacts/read query schema.""" + + path: str = Field(min_length=1) + encoding: str = Field(default="utf-8") + max_bytes: int = Field(default=256 * 1024, ge=1, le=1024 * 1024) + + +class JournalListRequest(BaseModel): + """GET /api/v1/events query schema.""" + + session_id: str | None = None + event_type: str | None = None + limit: int = Field(default=200, ge=1, le=5000) + since_sequence: int | None = Field(default=None, ge=1) + until_sequence: int | None = Field(default=None, ge=1) + + +class JournalReplayRequest(BaseModel): + """GET /api/v1/events/replay query schema.""" + + session_id: str | None = None + event_type: str | None = None + limit: int = Field(default=200, ge=1, le=5000) + since_sequence: int | None = Field(default=None, ge=1) + until_sequence: int | None = Field(default=None, ge=1) + + +class IDEWorkspaceRequest(BaseModel): + """GET /api/v1/ide/workspace query schema.""" + + include_diff: bool = True + diff_max_lines: int = Field(default=400, ge=1, le=4000) + findings_limit: int = Field(default=200, ge=1, le=2000) + session_limit: int = Field(default=20, ge=1, le=200) + task_limit: int = Field(default=50, ge=1, le=500) + + +class GitHubCIWorkflowRequest(BaseModel): + """POST /api/v1/workflows/github-ci payload.""" + + issue_reference: str | None = None + plan_summary: str | None = None + branch_name: str | None = None + base_branch: str = "main" + commit_message: str | None = None + pr_title: str | None = None + pr_body: str | None = None + ci_commands: list[str] = Field(default_factory=list) + headless_ci: bool = True + allow_dirty_repo: bool = False + push_remote: bool = True + create_pull_request: bool = True + + +class OrgPolicyEvaluateRequest(BaseModel): + """POST /api/v1/org/policy/evaluate payload.""" + + tool_name: str = Field(min_length=1) + operation: str = "execute" + payload: Any = None + actor: str | None = None + context: dict[str, Any] = Field(default_factory=dict) + policies: list[dict[str, Any]] = Field(default_factory=list) + create_approval: bool = True + + +class OrgPolicyApprovalDecisionRequest(BaseModel): + """POST /api/v1/org/policy/approvals/decide payload.""" + + approval_id: str = Field(min_length=1) + decision: str = Field(min_length=1) + decided_by: str | None = None + reason: str | None = None + + +class OrgPolicyAuditListRequest(BaseModel): + """GET /api/v1/org/policy/audit query schema.""" + + event_type: str | None = None + tool_name: str | None = None + limit: int = Field(default=200, ge=1, le=2000) + + +class OrgPolicyApprovalsListRequest(BaseModel): + """GET /api/v1/org/policy/approvals query schema.""" + + status: str | None = None + limit: int = Field(default=100, ge=1, le=1000) + + +def _iso_or_none(value: Optional[datetime]) -> Optional[str]: + if value is None: + return None + return value.isoformat() + + +def _extract_assistant_text(content: Any) -> str: + if isinstance(content, str): + return content + if not isinstance(content, list): + return str(content) + parts: list[str] = [] + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + parts.append(str(block.get("text", ""))) + return "".join(parts) + + +def _serialize_task(task: Task) -> dict[str, Any]: + payload: dict[str, Any] = { + "id": task.id, + "type": task.type.value if isinstance(task.type, TaskType) else str(task.type), + "status": task.status.value if isinstance(task.status, TaskStatus) else str(task.status), + "description": task.description, + "created_at": _iso_or_none(task.created_at), + "started_at": _iso_or_none(task.started_at), + "completed_at": _iso_or_none(task.completed_at), + "error": task.error, + "is_backgrounded": task.is_backgrounded, + "parent_id": task.parent_id, + "tags": list(task.tags), + "metadata": dict(task.metadata), + "duration_seconds": task.duration, + } + if isinstance(task, BashTask): + payload["command"] = task.command + payload["cwd"] = task.cwd + payload["timeout"] = task.timeout + if task.result: + payload["result"] = { + "code": task.result.code, + "stdout": task.result.stdout, + "stderr": task.result.stderr, + "error": task.result.error, + } + else: + payload["result"] = None + return payload + + +class ControlPlaneService: + """Runtime-backed API service implementation.""" + + def __init__( + self, + runtime: Any, + *, + artifact_max_bytes: int, + ) -> None: + self._runtime = runtime + self._artifact_max_bytes = artifact_max_bytes + self._query_lock = asyncio.Lock() + + @property + def runtime(self) -> Any: + return self._runtime + + async def health(self) -> dict[str, Any]: + return { + "status": "ok", + "service": "claude-code-python-daemon", + "working_directory": self.runtime.working_directory, + } + + async def query(self, request: QueryRequest) -> dict[str, Any]: + if request.session_id: + resumed = await self.runtime.query_engine.resume_session(request.session_id) + if not resumed: + raise ControlPlaneError( + f"Session not found: {request.session_id}", + code="session_not_found", + status_code=404, + ) + + text_parts: list[str] = [] + stop_reason: Optional[str] = None + event_count = 0 + + async with self._query_lock: + async for event in self.runtime.query_engine.query(request.query): + event_count += 1 + if event_count > request.max_events: + break + + if isinstance(event, dict): + event_type = str(event.get("type", "")) + if event_type == "text": + text_parts.append(str(event.get("content", ""))) + elif event_type == "stop_reason": + stop_reason = str(event.get("reason", "")) + continue + + if getattr(event, "role", None) == "assistant": + text_parts.append(_extract_assistant_text(getattr(event, "content", ""))) + + return { + "output": "".join(text_parts), + "stop_reason": stop_reason, + "event_count": event_count, + "session_id": self.runtime.query_engine.config.session_id, + "message_count": len(self.runtime.query_engine.get_messages()), + } + + async def list_sessions(self) -> dict[str, Any]: + sessions = self.runtime.session_manager.list_sessions() + return { + "sessions": [ + { + "id": item.id, + "created_at": item.created_at, + "last_active": item.last_active, + "working_directory": item.working_directory, + "model": item.model, + "message_count": item.message_count, + "tool_call_count": item.tool_call_count, + } + for item in sessions + ] + } + + async def get_session(self, session_id: str) -> dict[str, Any]: + session = self.runtime.session_manager.load_session(session_id) + if session is None: + raise ControlPlaneError( + f"Session not found: {session_id}", + code="session_not_found", + status_code=404, + ) + return { + "session": { + "id": session.id, + "metadata": session.metadata.to_dict(), + "messages": [message.to_dict() for message in session.messages], + } + } + + async def resume_session(self, request: ResumeSessionRequest) -> dict[str, Any]: + resumed = await self.runtime.query_engine.resume_session(request.session_id) + if not resumed: + target_id = request.session_id or "(latest)" + raise ControlPlaneError( + f"Session not found: {target_id}", + code="session_not_found", + status_code=404, + ) + return { + "session_id": self.runtime.query_engine.config.session_id, + "message_count": len(self.runtime.query_engine.get_messages()), + } + + async def list_tasks( + self, + *, + status: Optional[str] = None, + task_type: Optional[str] = None, + ) -> dict[str, Any]: + status_filter: Optional[TaskStatus] = None + type_filter: Optional[TaskType] = None + if status: + try: + status_filter = TaskStatus(status) + except ValueError as exc: + raise ControlPlaneError( + f"Unknown task status: {status}", + code="validation_error", + status_code=400, + ) from exc + if task_type: + try: + type_filter = TaskType(task_type) + except ValueError as exc: + raise ControlPlaneError( + f"Unknown task type: {task_type}", + code="validation_error", + status_code=400, + ) from exc + + tasks = self.runtime.task_manager.list_tasks(status=status_filter, task_type=type_filter) + return {"tasks": [_serialize_task(task) for task in tasks]} + + async def get_task(self, task_id: str) -> dict[str, Any]: + task = self.runtime.task_manager.get_task(task_id) + if task is None: + raise ControlPlaneError( + f"Task not found: {task_id}", + code="task_not_found", + status_code=404, + ) + return {"task": _serialize_task(task)} + + async def create_bash_task(self, request: BashTaskCreateRequest) -> dict[str, Any]: + task = await self.runtime.task_manager.create_bash_task( + command=request.command, + cwd=request.cwd, + env=request.env, + timeout=request.timeout, + background=request.background, + description=request.description, + idempotency_key=request.idempotency_key, + ) + await self.runtime.task_manager.start_task( + task.id, + executor=self._execute_bash_task, + max_retries=request.max_retries, + retry_delay=request.retry_delay, + ) + + if not request.background: + await self.runtime.task_manager.wait_for_task( + task.id, + timeout=request.wait_timeout or request.timeout, + ) + + latest = self.runtime.task_manager.get_task(task.id) or task + return {"task": _serialize_task(latest)} + + async def execute_tool(self, request: ToolExecuteRequest) -> dict[str, Any]: + tool = self.runtime.tool_registry.get(request.name) + if tool is None: + raise ControlPlaneError( + f"Tool not found: {request.name}", + code="tool_not_found", + status_code=404, + ) + + config = self.runtime.query_engine.config + permission_mode = request.permission_mode or config.permission_mode + checker = create_permission_checker( + mode=permission_mode, + always_allow=list(config.always_allow), + always_deny=list(config.always_deny), + ) + if not checker.can_execute(request.name): + raise ControlPlaneError( + f"Permission denied for tool: {request.name}", + code="permission_denied", + status_code=403, + ) + + is_valid, validation_error = tool.validate_input(request.input) + if not is_valid: + raise ControlPlaneError( + f"Invalid tool input: {validation_error}", + code="validation_error", + status_code=400, + ) + + context = ToolContext( + working_directory=config.working_directory or self.runtime.working_directory, + environment={}, + permission_mode=permission_mode, + always_allow=list(config.always_allow), + always_deny=list(config.always_deny), + model=config.model, + session_id=request.session_id or config.session_id, + ) + result = await tool.execute(request.input, context) + return { + "tool_name": request.name, + "content": result.content, + "is_error": result.is_error, + "tool_use_id": result.tool_use_id, + } + + async def read_artifact(self, request: ArtifactReadRequest) -> dict[str, Any]: + artifact_path = self._resolve_artifact_path(request.path) + if not artifact_path.is_file(): + raise ControlPlaneError( + f"Artifact not found: {request.path}", + code="artifact_not_found", + status_code=404, + ) + + raw = artifact_path.read_bytes() + max_bytes = min(request.max_bytes, self._artifact_max_bytes) + truncated = len(raw) > max_bytes + payload = raw[:max_bytes] + content = payload.decode(request.encoding, errors="replace") + + return { + "path": str(artifact_path), + "size_bytes": len(raw), + "returned_bytes": len(payload), + "truncated": truncated, + "encoding": request.encoding, + "content": content, + } + + async def list_events(self, request: JournalListRequest) -> dict[str, Any]: + journal = getattr(self.runtime, "event_journal", None) + if journal is None: + raise ControlPlaneError( + "Event journal is not configured", + code="event_journal_unavailable", + status_code=503, + ) + + event_types = [request.event_type] if request.event_type else None + entries = journal.query_events( + session_id=request.session_id, + event_types=event_types, + limit=request.limit, + since_sequence=request.since_sequence, + until_sequence=request.until_sequence, + ) + return { + "events": [entry.to_dict() for entry in entries], + "diagnostics": journal.get_diagnostics(), + } + + async def replay_events(self, request: JournalReplayRequest) -> dict[str, Any]: + journal = getattr(self.runtime, "event_journal", None) + if journal is None: + raise ControlPlaneError( + "Event journal is not configured", + code="event_journal_unavailable", + status_code=503, + ) + + event_types = [request.event_type] if request.event_type else None + entries = journal.replay_events( + session_id=request.session_id, + event_types=event_types, + since_sequence=request.since_sequence, + until_sequence=request.until_sequence, + limit=request.limit, + ) + return { + "events": [entry.to_dict() for entry in entries], + "diagnostics": journal.get_diagnostics(), + } + + async def ide_workspace(self, request: IDEWorkspaceRequest) -> dict[str, Any]: + """Collect IDE-facing workspace snapshot for VS Code client adapters.""" + working_directory = str(self.runtime.working_directory) + sessions_payload = await self.list_sessions() + tasks_payload = await self.list_tasks() + files_payload = self._collect_changed_files(working_directory) + findings_payload = self._collect_findings( + working_directory, + limit=request.findings_limit, + ) + + sessions = sessions_payload.get("sessions", []) + tasks = tasks_payload.get("tasks", []) + diff_text = self._collect_git_diff( + working_directory, + include_diff=request.include_diff, + max_lines=request.diff_max_lines, + ) + + return { + "workspace": { + "working_directory": working_directory, + "diff": diff_text, + "changed_files": files_payload, + "sessions": sessions[: request.session_limit], + "tasks": tasks[: request.task_limit], + "findings": findings_payload[: request.findings_limit], + } + } + + async def run_github_ci_workflow(self, request: GitHubCIWorkflowRequest) -> dict[str, Any]: + """Run P2-04 issue->plan->code->test->review->PR workflow.""" + workflow_service = GitHubCIWorkflowService() + try: + workflow = workflow_service.run_workflow( + working_directory=str(self.runtime.working_directory), + issue_reference=request.issue_reference, + plan_summary=request.plan_summary, + branch_name=request.branch_name, + base_branch=request.base_branch, + commit_message=request.commit_message, + pr_title=request.pr_title, + pr_body=request.pr_body, + ci_commands=request.ci_commands, + headless_ci=request.headless_ci, + allow_dirty_repo=request.allow_dirty_repo, + push_remote=request.push_remote, + create_pull_request=request.create_pull_request, + ) + except GitHubCIWorkflowError as exc: + raise ControlPlaneError( + exc.message, + code=exc.code, + status_code=exc.status_code, + details=exc.details, + ) from exc + return {"workflow": workflow} + + async def evaluate_org_policy(self, request: OrgPolicyEvaluateRequest) -> dict[str, Any]: + """Run P2-05 org policy matching with approval/audit flow.""" + org_service = self._get_org_policy_service() + if request.policies: + org_service.configure_policies( + policies=request.policies, + replace=True, + ) + try: + decision = org_service.evaluate( + tool_name=request.tool_name, + operation=request.operation, + payload=request.payload, + actor=request.actor, + context=request.context, + create_approval=request.create_approval, + ) + except OrgPolicyAuditError as exc: + raise ControlPlaneError( + exc.message, + code=exc.code, + status_code=exc.status_code, + details=exc.details, + ) from exc + return {"policy_decision": decision} + + async def decide_org_approval(self, request: OrgPolicyApprovalDecisionRequest) -> dict[str, Any]: + """Apply approval queue transition for org policy flow.""" + org_service = self._get_org_policy_service() + try: + approval = org_service.decide_approval( + approval_id=request.approval_id, + decision=request.decision, + decided_by=request.decided_by, + reason=request.reason, + ) + except OrgPolicyAuditError as exc: + raise ControlPlaneError( + exc.message, + code=exc.code, + status_code=exc.status_code, + details=exc.details, + ) from exc + return {"approval": approval} + + async def list_org_approvals(self, request: OrgPolicyApprovalsListRequest) -> dict[str, Any]: + """List org policy approval queue.""" + approvals = self._get_org_policy_service().list_approvals( + status=request.status, + limit=request.limit, + ) + return {"approvals": approvals} + + async def list_org_audit_events(self, request: OrgPolicyAuditListRequest) -> dict[str, Any]: + """List org policy audit events.""" + events = self._get_org_policy_service().list_audit_events( + event_type=request.event_type, + tool_name=request.tool_name, + limit=request.limit, + ) + return {"events": events} + + async def get_org_audit_report(self, *, limit: int = 200) -> dict[str, Any]: + """Get summarized org policy audit report.""" + report = self._get_org_policy_service().build_report(limit=limit) + return {"report": report} + + def _get_org_policy_service(self) -> OrgPolicyAuditService: + """Resolve runtime org policy service, create one lazily if needed.""" + existing = getattr(self.runtime, "org_policy_audit_service", None) + if isinstance(existing, OrgPolicyAuditService): + return existing + + journal = getattr(self.runtime, "event_journal", None) + append_event = getattr(journal, "append_event", None) + service = OrgPolicyAuditService( + append_event=append_event if callable(append_event) else None, + ) + setattr(self.runtime, "org_policy_audit_service", service) + return service + + @staticmethod + def _collect_changed_files(working_directory: str) -> list[dict[str, str]]: + """Collect changed files for IDE list display.""" + result = subprocess.run( + ["git", "status", "--porcelain=v1", "-uall"], + cwd=working_directory, + capture_output=True, + text=True, + ) + if result.returncode != 0: + error = result.stderr.strip() or "unknown git error" + raise ControlPlaneError( + f"Unable to collect changed files: {error}", + code="git_error", + status_code=503, + ) + + files: list[dict[str, str]] = [] + seen_paths: set[str] = set() + for raw_line in result.stdout.splitlines(): + line = raw_line.rstrip() + if len(line) < 4: + continue + status = line[:2] + path_token = line[3:].strip() + if " -> " in path_token: + path_token = path_token.split(" -> ", 1)[1].strip() + if ( + not path_token + or path_token in seen_paths + ): + continue + seen_paths.add(path_token) + files.append({"path": path_token, "status": status}) + return files + + @staticmethod + def _collect_git_diff( + working_directory: str, + *, + include_diff: bool, + max_lines: int, + ) -> dict[str, Any]: + """Collect bounded git diff text for IDE panel rendering.""" + if not include_diff: + return { + "content": "", + "truncated": False, + "line_count": 0, + "max_lines": max_lines, + } + + result = subprocess.run( + ["git", "diff"], + cwd=working_directory, + capture_output=True, + text=True, + ) + if result.returncode != 0: + error = result.stderr.strip() or "unknown git error" + raise ControlPlaneError( + f"Unable to collect git diff: {error}", + code="git_error", + status_code=503, + ) + + content = result.stdout + lines = content.splitlines() + truncated = len(lines) > max_lines + if truncated: + content = "\n".join(lines[:max_lines]) + if content: + content += "\n" + content += f"... [truncated: showing first {max_lines} lines]" + return { + "content": content, + "truncated": truncated, + "line_count": len(lines), + "max_lines": max_lines, + } + + @staticmethod + def _collect_findings(working_directory: str, *, limit: int) -> list[dict[str, Any]]: + """Collect structured findings from review service for IDE diagnostics.""" + review_service = ReviewService() + try: + result = review_service.review_git_changes(working_directory) + except ReviewServiceError as exc: + raise ControlPlaneError( + f"Unable to collect review findings: {exc}", + code="review_unavailable", + status_code=503, + ) from exc + return [ + { + "file_path": finding.file_path, + "line": finding.line, + "severity": finding.severity, + "issue": finding.issue, + "recommendation": finding.recommendation, + } + for finding in result.findings[:limit] + ] + + async def _execute_bash_task(self, task: Task) -> TaskResult: + if not isinstance(task, BashTask): + raise TypeError("executor expects BashTask") + + if os.name == "nt": + cmd = ["cmd", "/c", task.command] + else: + cmd = shlex.split(task.command) + + env = {**os.environ, **(task.env or {})} + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=task.cwd, + env=env, + ) + try: + stdout_bytes, stderr_bytes = await asyncio.wait_for( + process.communicate(), + timeout=task.timeout, + ) + except asyncio.TimeoutError: + try: + process.kill() + except Exception: + pass + try: + await process.wait() + except Exception: + pass + raise + + return TaskResult( + code=process.returncode or 0, + stdout=stdout_bytes.decode("utf-8", errors="replace"), + stderr=stderr_bytes.decode("utf-8", errors="replace"), + error=None, + ) + + def _resolve_artifact_path(self, raw_path: str) -> Path: + root = Path(self.runtime.working_directory).resolve() + path = Path(raw_path).expanduser() + if not path.is_absolute(): + path = (root / path).resolve() + else: + path = path.resolve() + + try: + path.relative_to(root) + except ValueError as exc: + raise ControlPlaneError( + f"Artifact path escapes working directory: {raw_path}", + code="validation_error", + status_code=400, + ) from exc + return path + + +class ControlPlaneDaemon: + """Threaded HTTP daemon exposing runtime control-plane APIs.""" + + def __init__(self, runtime: Any, config: Optional[DaemonServerConfig] = None) -> None: + self.config = config or DaemonServerConfig() + self._service = ControlPlaneService( + runtime, + artifact_max_bytes=self.config.artifact_max_bytes, + ) + self._loop = asyncio.new_event_loop() + self._loop_thread: Optional[threading.Thread] = None + self._httpd: Optional[ThreadingHTTPServer] = None + self._http_thread: Optional[threading.Thread] = None + self._lock = threading.Lock() + self._running = False + self._port = self.config.port + + @property + def port(self) -> int: + return self._port + + @property + def base_url(self) -> str: + return f"http://{self.config.host}:{self.port}" + + def start(self) -> None: + with self._lock: + if self._running: + return + + self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True) + self._loop_thread.start() + + handler = self._build_handler() + self._httpd = ThreadingHTTPServer((self.config.host, self.config.port), handler) + self._port = int(self._httpd.server_address[1]) + self._http_thread = threading.Thread(target=self._httpd.serve_forever, daemon=True) + self._http_thread.start() + self._running = True + + def stop(self) -> None: + with self._lock: + if not self._running: + return + if self._httpd is not None: + self._httpd.shutdown() + self._httpd.server_close() + self._running = False + + if self._http_thread is not None: + self._http_thread.join(timeout=5.0) + self._http_thread = None + + if self._loop.is_running(): + self._loop.call_soon_threadsafe(self._loop.stop) + if self._loop_thread is not None: + self._loop_thread.join(timeout=5.0) + self._loop_thread = None + + try: + self._loop.close() + except Exception: + pass + + def serve_forever(self) -> None: + self.start() + if self._http_thread is None: + return + try: + self._http_thread.join() + except KeyboardInterrupt: + self.stop() + + def _run_event_loop(self) -> None: + asyncio.set_event_loop(self._loop) + self._loop.run_forever() + + def _build_handler(self): + daemon = self + + class _Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: # noqa: N802 + daemon._handle_http(self, "GET") + + def do_POST(self) -> None: # noqa: N802 + daemon._handle_http(self, "POST") + + def log_message(self, format: str, *args: Any) -> None: + return + + return _Handler + + def _handle_http(self, handler: BaseHTTPRequestHandler, method: str) -> None: + try: + parsed = urllib_parse.urlparse(handler.path) + path = parsed.path + query_params = urllib_parse.parse_qs(parsed.query, keep_blank_values=True) + payload = self._read_json_payload(handler) if method == "POST" else {} + data = self._dispatch(method=method, path=path, payload=payload, query_params=query_params) + self._write_response(handler, status_code=200, payload={"ok": True, "data": data}) + except ControlPlaneError as exc: + self._write_response( + handler, + status_code=exc.status_code, + payload={ + "ok": False, + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details, + }, + }, + ) + except Exception as exc: + self._write_response( + handler, + status_code=500, + payload={ + "ok": False, + "error": { + "code": "internal_error", + "message": str(exc), + }, + }, + ) + + def _read_json_payload(self, handler: BaseHTTPRequestHandler) -> dict[str, Any]: + raw_length = handler.headers.get("Content-Length", "0") + try: + length = int(raw_length) + except ValueError as exc: + raise ControlPlaneError( + "Invalid Content-Length", + code="validation_error", + status_code=400, + ) from exc + + if length <= 0: + return {} + + raw = handler.rfile.read(length) + if not raw: + return {} + try: + parsed = json.loads(raw.decode("utf-8")) + except json.JSONDecodeError as exc: + raise ControlPlaneError( + "Invalid JSON payload", + code="invalid_json", + status_code=400, + ) from exc + if not isinstance(parsed, dict): + raise ControlPlaneError( + "JSON payload must be an object", + code="validation_error", + status_code=400, + ) + return parsed + + def _dispatch( + self, + *, + method: str, + path: str, + payload: dict[str, Any], + query_params: dict[str, list[str]], + ) -> dict[str, Any]: + if method == "GET" and path == "/health": + return self._await_service_call(self._service.health()) + + if method == "POST" and path == "/api/v1/query": + request = self._validate_payload(QueryRequest, payload) + return self._await_service_call(self._service.query(request)) + + if method == "GET" and path == "/api/v1/sessions": + return self._await_service_call(self._service.list_sessions()) + + if method == "GET" and path.startswith("/api/v1/sessions/"): + session_id = path.removeprefix("/api/v1/sessions/").strip() + if not session_id: + raise ControlPlaneError("Missing session id", code="validation_error", status_code=400) + return self._await_service_call(self._service.get_session(session_id)) + + if method == "POST" and path == "/api/v1/sessions/resume": + request = self._validate_payload(ResumeSessionRequest, payload) + return self._await_service_call(self._service.resume_session(request)) + + if method == "GET" and path == "/api/v1/tasks": + status = self._first_query_value(query_params, "status") + task_type = self._first_query_value(query_params, "type") + return self._await_service_call(self._service.list_tasks(status=status, task_type=task_type)) + + if method == "GET" and path.startswith("/api/v1/tasks/"): + task_id = path.removeprefix("/api/v1/tasks/").strip() + if not task_id: + raise ControlPlaneError("Missing task id", code="validation_error", status_code=400) + return self._await_service_call(self._service.get_task(task_id)) + + if method == "POST" and path == "/api/v1/tasks/bash": + request = self._validate_payload(BashTaskCreateRequest, payload) + return self._await_service_call(self._service.create_bash_task(request)) + + if method == "POST" and path == "/api/v1/tools/execute": + request = self._validate_payload(ToolExecuteRequest, payload) + return self._await_service_call(self._service.execute_tool(request)) + + if method == "GET" and path == "/api/v1/artifacts/read": + max_bytes_raw = self._first_query_value(query_params, "max_bytes", default=str(256 * 1024)) + try: + max_bytes = int(max_bytes_raw) if max_bytes_raw is not None else 256 * 1024 + except ValueError as exc: + raise ControlPlaneError( + f"Invalid max_bytes value: {max_bytes_raw}", + code="validation_error", + status_code=400, + ) from exc + request = self._validate_payload( + ArtifactReadRequest, + { + "path": self._first_query_value(query_params, "path"), + "encoding": self._first_query_value(query_params, "encoding", default="utf-8"), + "max_bytes": max_bytes, + }, + ) + return self._await_service_call(self._service.read_artifact(request)) + + if method == "GET" and path == "/api/v1/events": + request = self._validate_payload( + JournalListRequest, + { + "session_id": self._first_query_value(query_params, "session_id"), + "event_type": self._first_query_value(query_params, "event_type"), + "limit": self._parse_optional_int( + self._first_query_value(query_params, "limit"), + default=200, + field_name="limit", + ), + "since_sequence": self._parse_optional_int( + self._first_query_value(query_params, "since_sequence"), + field_name="since_sequence", + ), + "until_sequence": self._parse_optional_int( + self._first_query_value(query_params, "until_sequence"), + field_name="until_sequence", + ), + }, + ) + return self._await_service_call(self._service.list_events(request)) + + if method == "GET" and path == "/api/v1/events/replay": + request = self._validate_payload( + JournalReplayRequest, + { + "session_id": self._first_query_value(query_params, "session_id"), + "event_type": self._first_query_value(query_params, "event_type"), + "limit": self._parse_optional_int( + self._first_query_value(query_params, "limit"), + default=200, + field_name="limit", + ), + "since_sequence": self._parse_optional_int( + self._first_query_value(query_params, "since_sequence"), + field_name="since_sequence", + ), + "until_sequence": self._parse_optional_int( + self._first_query_value(query_params, "until_sequence"), + field_name="until_sequence", + ), + }, + ) + return self._await_service_call(self._service.replay_events(request)) + + if method == "GET" and path == "/api/v1/ide/workspace": + request = self._validate_payload( + IDEWorkspaceRequest, + { + "include_diff": self._parse_optional_bool( + self._first_query_value(query_params, "include_diff"), + default=True, + field_name="include_diff", + ), + "diff_max_lines": self._parse_optional_int( + self._first_query_value(query_params, "diff_max_lines"), + default=400, + field_name="diff_max_lines", + ), + "findings_limit": self._parse_optional_int( + self._first_query_value(query_params, "findings_limit"), + default=200, + field_name="findings_limit", + ), + "session_limit": self._parse_optional_int( + self._first_query_value(query_params, "session_limit"), + default=20, + field_name="session_limit", + ), + "task_limit": self._parse_optional_int( + self._first_query_value(query_params, "task_limit"), + default=50, + field_name="task_limit", + ), + }, + ) + return self._await_service_call(self._service.ide_workspace(request)) + + if method == "POST" and path == "/api/v1/workflows/github-ci": + request = self._validate_payload(GitHubCIWorkflowRequest, payload) + return self._await_service_call(self._service.run_github_ci_workflow(request)) + + if method == "POST" and path == "/api/v1/org/policy/evaluate": + request = self._validate_payload(OrgPolicyEvaluateRequest, payload) + return self._await_service_call(self._service.evaluate_org_policy(request)) + + if method == "POST" and path == "/api/v1/org/policy/approvals/decide": + request = self._validate_payload(OrgPolicyApprovalDecisionRequest, payload) + return self._await_service_call(self._service.decide_org_approval(request)) + + if method == "GET" and path == "/api/v1/org/policy/approvals": + request = self._validate_payload( + OrgPolicyApprovalsListRequest, + { + "status": self._first_query_value(query_params, "status"), + "limit": self._parse_optional_int( + self._first_query_value(query_params, "limit"), + default=100, + field_name="limit", + ), + }, + ) + return self._await_service_call(self._service.list_org_approvals(request)) + + if method == "GET" and path == "/api/v1/org/policy/audit": + request = self._validate_payload( + OrgPolicyAuditListRequest, + { + "event_type": self._first_query_value(query_params, "event_type"), + "tool_name": self._first_query_value(query_params, "tool_name"), + "limit": self._parse_optional_int( + self._first_query_value(query_params, "limit"), + default=200, + field_name="limit", + ), + }, + ) + return self._await_service_call(self._service.list_org_audit_events(request)) + + if method == "GET" and path == "/api/v1/org/policy/report": + limit = self._parse_optional_int( + self._first_query_value(query_params, "limit"), + default=200, + field_name="limit", + ) + return self._await_service_call(self._service.get_org_audit_report(limit=limit or 200)) + + raise ControlPlaneError( + f"Route not found: {method} {path}", + code="route_not_found", + status_code=404, + ) + + def _await_service_call(self, coroutine: Any) -> dict[str, Any]: + future = asyncio.run_coroutine_threadsafe(coroutine, self._loop) + try: + result = future.result(timeout=self.config.request_timeout_seconds) + except concurrent.futures.TimeoutError as exc: + future.cancel() + raise ControlPlaneError( + "Daemon request timed out", + code="timeout", + status_code=504, + ) from exc + except ControlPlaneError: + raise + except Exception as exc: + raise ControlPlaneError(str(exc), code="internal_error", status_code=500) from exc + if not isinstance(result, dict): + return {"result": result} + return result + + @staticmethod + def _write_response( + handler: BaseHTTPRequestHandler, + *, + status_code: int, + payload: dict[str, Any], + ) -> None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + handler.send_response(status_code) + handler.send_header("Content-Type", "application/json; charset=utf-8") + handler.send_header("Content-Length", str(len(body))) + handler.end_headers() + handler.wfile.write(body) + + @staticmethod + def _first_query_value( + query_params: dict[str, list[str]], + name: str, + *, + default: Optional[str] = None, + ) -> Optional[str]: + values = query_params.get(name) + if not values: + return default + value = values[0] + if value is None or value == "": + return default + return value + + @staticmethod + def _parse_optional_int( + value: Optional[str], + *, + default: Optional[int] = None, + field_name: str, + ) -> Optional[int]: + if value is None or value == "": + return default + try: + return int(value) + except ValueError as exc: + raise ControlPlaneError( + f"Invalid {field_name} value: {value}", + code="validation_error", + status_code=400, + ) from exc + + @staticmethod + def _parse_optional_bool( + value: Optional[str], + *, + default: bool, + field_name: str, + ) -> bool: + if value is None or value == "": + return default + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + raise ControlPlaneError( + f"Invalid {field_name} value: {value}", + code="validation_error", + status_code=400, + ) + + @staticmethod + def _validate_payload(model: type[BaseModel], payload: dict[str, Any]) -> Any: + try: + return model.model_validate(payload) + except ValidationError as exc: + raise ControlPlaneError( + "Request validation failed", + code="validation_error", + status_code=400, + details=exc.errors(), + ) from exc + + +class ControlPlaneClient: + """Minimal stdlib HTTP client for local daemon integration.""" + + def __init__(self, base_url: str, *, timeout_seconds: float = 10.0) -> None: + self._base_url = base_url.rstrip("/") + self._timeout = timeout_seconds + + def health(self) -> dict[str, Any]: + return self._request("GET", "/health") + + def query(self, query: str, *, session_id: Optional[str] = None) -> dict[str, Any]: + payload: dict[str, Any] = {"query": query} + if session_id: + payload["session_id"] = session_id + return self._request("POST", "/api/v1/query", payload=payload) + + def list_sessions(self) -> dict[str, Any]: + return self._request("GET", "/api/v1/sessions") + + def get_session(self, session_id: str) -> dict[str, Any]: + return self._request("GET", f"/api/v1/sessions/{session_id}") + + def resume_session(self, session_id: Optional[str] = None) -> dict[str, Any]: + payload: dict[str, Any] = {} + if session_id: + payload["session_id"] = session_id + return self._request("POST", "/api/v1/sessions/resume", payload=payload) + + def list_tasks(self) -> dict[str, Any]: + return self._request("GET", "/api/v1/tasks") + + def get_task(self, task_id: str) -> dict[str, Any]: + return self._request("GET", f"/api/v1/tasks/{task_id}") + + def create_bash_task(self, command: str, **kwargs: Any) -> dict[str, Any]: + payload = {"command": command, **kwargs} + return self._request("POST", "/api/v1/tasks/bash", payload=payload) + + def execute_tool(self, name: str, input_data: Optional[dict[str, Any]] = None) -> dict[str, Any]: + payload = {"name": name, "input": input_data or {}} + return self._request("POST", "/api/v1/tools/execute", payload=payload) + + def read_artifact(self, path: str, *, encoding: str = "utf-8", max_bytes: int = 262144) -> dict[str, Any]: + query = urllib_parse.urlencode( + {"path": path, "encoding": encoding, "max_bytes": str(max_bytes)} + ) + return self._request("GET", f"/api/v1/artifacts/read?{query}") + + def list_events( + self, + *, + session_id: Optional[str] = None, + event_type: Optional[str] = None, + limit: int = 200, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + ) -> dict[str, Any]: + params: dict[str, str] = {"limit": str(limit)} + if session_id: + params["session_id"] = session_id + if event_type: + params["event_type"] = event_type + if since_sequence is not None: + params["since_sequence"] = str(since_sequence) + if until_sequence is not None: + params["until_sequence"] = str(until_sequence) + query = urllib_parse.urlencode(params) + return self._request("GET", f"/api/v1/events?{query}") + + def replay_events( + self, + *, + session_id: Optional[str] = None, + event_type: Optional[str] = None, + limit: int = 200, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + ) -> dict[str, Any]: + params: dict[str, str] = {"limit": str(limit)} + if session_id: + params["session_id"] = session_id + if event_type: + params["event_type"] = event_type + if since_sequence is not None: + params["since_sequence"] = str(since_sequence) + if until_sequence is not None: + params["until_sequence"] = str(until_sequence) + query = urllib_parse.urlencode(params) + return self._request("GET", f"/api/v1/events/replay?{query}") + + def get_ide_workspace( + self, + *, + include_diff: bool = True, + diff_max_lines: int = 400, + findings_limit: int = 200, + session_limit: int = 20, + task_limit: int = 50, + ) -> dict[str, Any]: + params = { + "include_diff": "true" if include_diff else "false", + "diff_max_lines": str(diff_max_lines), + "findings_limit": str(findings_limit), + "session_limit": str(session_limit), + "task_limit": str(task_limit), + } + query = urllib_parse.urlencode(params) + return self._request("GET", f"/api/v1/ide/workspace?{query}") + + def run_github_ci_workflow( + self, + *, + issue_reference: str | None = None, + plan_summary: str | None = None, + branch_name: str | None = None, + base_branch: str = "main", + commit_message: str | None = None, + pr_title: str | None = None, + pr_body: str | None = None, + ci_commands: list[str] | None = None, + headless_ci: bool = True, + allow_dirty_repo: bool = False, + push_remote: bool = True, + create_pull_request: bool = True, + ) -> dict[str, Any]: + payload: dict[str, Any] = { + "base_branch": base_branch, + "ci_commands": list(ci_commands or []), + "headless_ci": headless_ci, + "allow_dirty_repo": allow_dirty_repo, + "push_remote": push_remote, + "create_pull_request": create_pull_request, + } + if issue_reference is not None: + payload["issue_reference"] = issue_reference + if plan_summary is not None: + payload["plan_summary"] = plan_summary + if branch_name is not None: + payload["branch_name"] = branch_name + if commit_message is not None: + payload["commit_message"] = commit_message + if pr_title is not None: + payload["pr_title"] = pr_title + if pr_body is not None: + payload["pr_body"] = pr_body + return self._request("POST", "/api/v1/workflows/github-ci", payload=payload) + + def evaluate_org_policy( + self, + *, + tool_name: str, + operation: str = "execute", + payload: Any = None, + actor: str | None = None, + context: Optional[dict[str, Any]] = None, + policies: Optional[list[dict[str, Any]]] = None, + create_approval: bool = True, + ) -> dict[str, Any]: + request_payload: dict[str, Any] = { + "tool_name": tool_name, + "operation": operation, + "payload": payload, + "context": dict(context or {}), + "policies": list(policies or []), + "create_approval": create_approval, + } + if actor is not None: + request_payload["actor"] = actor + return self._request("POST", "/api/v1/org/policy/evaluate", payload=request_payload) + + def decide_org_approval( + self, + *, + approval_id: str, + decision: str, + decided_by: str | None = None, + reason: str | None = None, + ) -> dict[str, Any]: + payload: dict[str, Any] = { + "approval_id": approval_id, + "decision": decision, + } + if decided_by is not None: + payload["decided_by"] = decided_by + if reason is not None: + payload["reason"] = reason + return self._request("POST", "/api/v1/org/policy/approvals/decide", payload=payload) + + def list_org_approvals( + self, + *, + status: str | None = None, + limit: int = 100, + ) -> dict[str, Any]: + params: dict[str, str] = {"limit": str(limit)} + if status: + params["status"] = status + query = urllib_parse.urlencode(params) + return self._request("GET", f"/api/v1/org/policy/approvals?{query}") + + def list_org_audit_events( + self, + *, + event_type: str | None = None, + tool_name: str | None = None, + limit: int = 200, + ) -> dict[str, Any]: + params: dict[str, str] = {"limit": str(limit)} + if event_type: + params["event_type"] = event_type + if tool_name: + params["tool_name"] = tool_name + query = urllib_parse.urlencode(params) + return self._request("GET", f"/api/v1/org/policy/audit?{query}") + + def get_org_policy_report(self, *, limit: int = 200) -> dict[str, Any]: + query = urllib_parse.urlencode({"limit": str(limit)}) + return self._request("GET", f"/api/v1/org/policy/report?{query}") + + def _request( + self, + method: str, + path: str, + *, + payload: Optional[dict[str, Any]] = None, + ) -> dict[str, Any]: + url = f"{self._base_url}{path}" + data_bytes: Optional[bytes] = None + headers = {"Accept": "application/json"} + if payload is not None: + data_bytes = json.dumps(payload, ensure_ascii=False).encode("utf-8") + headers["Content-Type"] = "application/json; charset=utf-8" + + request = urllib_request.Request(url=url, method=method, data=data_bytes, headers=headers) + try: + with urllib_request.urlopen(request, timeout=self._timeout) as response: + raw = response.read() + parsed = json.loads(raw.decode("utf-8")) if raw else {} + return self._unwrap_response(parsed) + except urllib_error.HTTPError as exc: + raw = exc.read() + parsed: dict[str, Any] + try: + parsed = json.loads(raw.decode("utf-8")) if raw else {} + except Exception: + parsed = {} + error_obj = parsed.get("error", {}) if isinstance(parsed, dict) else {} + raise DaemonResponseError( + exc.code, + str(error_obj.get("code", "http_error")), + str(error_obj.get("message", exc.reason)), + error_obj.get("details"), + ) from exc + except urllib_error.URLError as exc: + reason = exc.reason + if isinstance(reason, socket.timeout): + raise DaemonTimeoutError("Daemon request timed out") from exc + raise DaemonUnavailableError(f"Daemon unavailable: {reason}") from exc + except TimeoutError as exc: + raise DaemonTimeoutError("Daemon request timed out") from exc + + @staticmethod + def _unwrap_response(payload: Any) -> dict[str, Any]: + if not isinstance(payload, dict): + return {"result": payload} + + ok = payload.get("ok") + if ok is False: + error_obj = payload.get("error", {}) + raise DaemonResponseError( + 500, + str(error_obj.get("code", "daemon_error")), + str(error_obj.get("message", "daemon request failed")), + error_obj.get("details"), + ) + + data = payload.get("data") + if isinstance(data, dict): + return data + return payload + + +def run_control_plane_daemon( + runtime: Any, + *, + host: str = "127.0.0.1", + port: int = 8787, + request_timeout_seconds: float = 30.0, +) -> None: + """Blocking daemon runner used by CLI entrypoint.""" + daemon = ControlPlaneDaemon( + runtime, + DaemonServerConfig( + host=host, + port=port, + request_timeout_seconds=request_timeout_seconds, + ), + ) + daemon.serve_forever() + + +__all__ = [ + "ControlPlaneError", + "DaemonClientError", + "DaemonUnavailableError", + "DaemonTimeoutError", + "DaemonResponseError", + "DaemonServerConfig", + "ControlPlaneDaemon", + "ControlPlaneClient", + "run_control_plane_daemon", +] diff --git a/claude_code/server/ide_adapter.py b/claude_code/server/ide_adapter.py new file mode 100644 index 0000000..42ff98c --- /dev/null +++ b/claude_code/server/ide_adapter.py @@ -0,0 +1,167 @@ +"""IDE protocol adapters backed by daemon control-plane.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from claude_code.server.control_plane import ControlPlaneClient + + +@dataclass(frozen=True, slots=True) +class IDEWorkspaceSnapshot: + """Normalized IDE snapshot payload from daemon.""" + + working_directory: str + diff: dict[str, Any] + changed_files: list[dict[str, str]] + sessions: list[dict[str, Any]] + tasks: list[dict[str, Any]] + findings: list[dict[str, Any]] + + @classmethod + def from_payload(cls, payload: dict[str, Any]) -> "IDEWorkspaceSnapshot": + workspace = payload.get("workspace", {}) if isinstance(payload, dict) else {} + return cls( + working_directory=str(workspace.get("working_directory", "")), + diff=dict(workspace.get("diff", {})), + changed_files=list(workspace.get("changed_files", [])), + sessions=list(workspace.get("sessions", [])), + tasks=list(workspace.get("tasks", [])), + findings=list(workspace.get("findings", [])), + ) + + +def _map_inspection_highlight(severity: Any) -> str: + normalized = str(severity or "").strip().lower() + if normalized in {"critical", "high"}: + return "error" + if normalized in {"medium"}: + return "warning" + if normalized in {"low"}: + return "weak_warning" + return "warning" + + +def _safe_int(value: Any, default: int = 0) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +@dataclass(frozen=True, slots=True) +class JetBrainsWorkspaceSnapshot: + """Normalized workspace payload for JetBrains client workflows.""" + + project_root: str + vcs: dict[str, Any] + sessions: list[dict[str, Any]] + tasks: list[dict[str, Any]] + inspections: list[dict[str, Any]] + + @classmethod + def from_payload(cls, payload: dict[str, Any]) -> "JetBrainsWorkspaceSnapshot": + workspace = payload.get("workspace", {}) if isinstance(payload, dict) else {} + findings = list(workspace.get("findings", [])) + inspections: list[dict[str, Any]] = [] + for finding in findings: + if not isinstance(finding, dict): + continue + severity = str(finding.get("severity", "")) + inspections.append( + { + "file_path": str(finding.get("file_path", "")), + "line": _safe_int(finding.get("line")), + "severity": severity, + "problem": str(finding.get("issue", "")), + "suggestion": str(finding.get("recommendation", "")), + "highlight": _map_inspection_highlight(severity), + } + ) + return cls( + project_root=str(workspace.get("working_directory", "")), + vcs={ + "diff": dict(workspace.get("diff", {})), + "changed_files": list(workspace.get("changed_files", [])), + }, + sessions=list(workspace.get("sessions", [])), + tasks=list(workspace.get("tasks", [])), + inspections=inspections, + ) + + +class _BaseIDEClientAdapter: + """Shared adapter behavior for IDE clients.""" + + def __init__(self, daemon_client: ControlPlaneClient) -> None: + self._daemon = daemon_client + + def _fetch_workspace_payload( + self, + *, + include_diff: bool = True, + diff_max_lines: int = 400, + findings_limit: int = 200, + session_limit: int = 20, + task_limit: int = 50, + ) -> dict[str, Any]: + return self._daemon.get_ide_workspace( + include_diff=include_diff, + diff_max_lines=diff_max_lines, + findings_limit=findings_limit, + session_limit=session_limit, + task_limit=task_limit, + ) + + +class VSCodeClientAdapter(_BaseIDEClientAdapter): + """Thin IDE adapter for VS Code client workflows.""" + + def fetch_workspace_snapshot( + self, + *, + include_diff: bool = True, + diff_max_lines: int = 400, + findings_limit: int = 200, + session_limit: int = 20, + task_limit: int = 50, + ) -> IDEWorkspaceSnapshot: + payload = self._fetch_workspace_payload( + include_diff=include_diff, + diff_max_lines=diff_max_lines, + findings_limit=findings_limit, + session_limit=session_limit, + task_limit=task_limit, + ) + return IDEWorkspaceSnapshot.from_payload(payload) + + +class JetBrainsClientAdapter(_BaseIDEClientAdapter): + """Thin IDE adapter for JetBrains client workflows.""" + + def fetch_workspace_snapshot( + self, + *, + include_diff: bool = True, + diff_max_lines: int = 400, + findings_limit: int = 200, + session_limit: int = 20, + task_limit: int = 50, + ) -> JetBrainsWorkspaceSnapshot: + payload = self._fetch_workspace_payload( + include_diff=include_diff, + diff_max_lines=diff_max_lines, + findings_limit=findings_limit, + session_limit=session_limit, + task_limit=task_limit, + ) + return JetBrainsWorkspaceSnapshot.from_payload(payload) + + +__all__ = [ + "IDEWorkspaceSnapshot", + "JetBrainsClientAdapter", + "JetBrainsWorkspaceSnapshot", + "VSCodeClientAdapter", +] diff --git a/claude_code/services/__init__.py b/claude_code/services/__init__.py index 3bfaa69..8d75a6b 100644 --- a/claude_code/services/__init__.py +++ b/claude_code/services/__init__.py @@ -164,6 +164,47 @@ APIEvents, SystemEvents, ) +from claude_code.services.event_journal import ( + EVENT_JOURNAL_SCHEMA_VERSION, + EVENT_JOURNAL_SCHEMA_KEY, + EventJournal, + EventJournalEntry, + SQLiteEventJournal, + get_event_journal, + set_event_journal, +) +from claude_code.services.multi_agent_supervisor import ( + SupervisorConfigError, + NodeExecutionStatus, + AgentNodeSpec, + SupervisorBudget, + NodeExecutionResult, + SupervisorRunResult, + MultiAgentSupervisor, +) +from claude_code.services.artifact_bus import ( + ArtifactSchemaError, + ArtifactConflictError, + ArtifactType, + ArtifactMergeStrategy, + ArtifactRecord, + ArtifactBus, +) +from claude_code.services.github_ci_workflow import ( + GitHubCIWorkflowError, + WorkflowStepStatus, + WorkflowStepRecord, + GitHubCIWorkflowService, +) +from claude_code.services.org_policy_audit import ( + OrgPolicyAuditError, + PolicyEffect, + ApprovalStatus, + OrgPolicyRule, + ApprovalRequest, + AuditEvent, + OrgPolicyAuditService, +) from claude_code.services.websocket import ( WebSocketServer, @@ -314,6 +355,39 @@ "ToolEvents", "APIEvents", "SystemEvents", + + # Event Journal + "EVENT_JOURNAL_SCHEMA_VERSION", + "EVENT_JOURNAL_SCHEMA_KEY", + "EventJournal", + "EventJournalEntry", + "SQLiteEventJournal", + "get_event_journal", + "set_event_journal", + "SupervisorConfigError", + "NodeExecutionStatus", + "AgentNodeSpec", + "SupervisorBudget", + "NodeExecutionResult", + "SupervisorRunResult", + "MultiAgentSupervisor", + "ArtifactSchemaError", + "ArtifactConflictError", + "ArtifactType", + "ArtifactMergeStrategy", + "ArtifactRecord", + "ArtifactBus", + "GitHubCIWorkflowError", + "WorkflowStepStatus", + "WorkflowStepRecord", + "GitHubCIWorkflowService", + "OrgPolicyAuditError", + "PolicyEffect", + "ApprovalStatus", + "OrgPolicyRule", + "ApprovalRequest", + "AuditEvent", + "OrgPolicyAuditService", # WebSocket "WebSocketServer", diff --git a/claude_code/services/artifact_bus.py b/claude_code/services/artifact_bus.py new file mode 100644 index 0000000..65199c2 --- /dev/null +++ b/claude_code/services/artifact_bus.py @@ -0,0 +1,336 @@ +"""Artifact bus for multi-agent workflow output exchange and merge control.""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Iterable + + +class ArtifactSchemaError(ValueError): + """Raised when an artifact payload fails schema validation.""" + + +class ArtifactConflictError(RuntimeError): + """Raised when artifact merge policy rejects a conflict.""" + + +class ArtifactType(Enum): + """Supported artifact categories for multi-agent workflows.""" + + PATCH = "patch" + NOTE = "note" + DIFF = "diff" + REPORT = "report" + FINDING = "finding" + + +class ArtifactMergeStrategy(Enum): + """Conflict policy used when two artifacts target the same key.""" + + FAIL = "fail" + APPEND = "append" + REPLACE = "replace" + + +@dataclass(frozen=True, slots=True) +class ArtifactRecord: + """Canonical artifact record stored on the artifact bus.""" + + artifact_id: str + node_id: str + artifact_type: ArtifactType + key: str + content: Any + merge_strategy: ArtifactMergeStrategy = ArtifactMergeStrategy.FAIL + ownership: tuple[str, ...] = () + metadata: dict[str, Any] = field(default_factory=dict) + created_at: float = field(default_factory=time.time) + + @classmethod + def from_dict( + cls, + payload: dict[str, Any], + *, + node_id: str, + index: int, + default_ownership: tuple[str, ...] = (), + ) -> "ArtifactRecord": + """Build one validated artifact record from raw payload.""" + if not isinstance(payload, dict): + raise ArtifactSchemaError(f"artifact[{index}] must be an object") + + artifact_type_raw = str(payload.get("type") or "").strip().lower() + if not artifact_type_raw: + raise ArtifactSchemaError(f"artifact[{index}].type is required") + try: + artifact_type = ArtifactType(artifact_type_raw) + except ValueError as exc: + supported = ", ".join(item.value for item in ArtifactType) + raise ArtifactSchemaError( + f"artifact[{index}].type must be one of: {supported}" + ) from exc + + key = str(payload.get("key") or "").strip() + if not key: + raise ArtifactSchemaError(f"artifact[{index}].key is required") + + has_content = "content" in payload + has_value = "value" in payload + if not has_content and not has_value: + raise ArtifactSchemaError(f"artifact[{index}].content is required") + content = payload.get("content", payload.get("value")) + + merge_raw = str(payload.get("merge") or payload.get("merge_strategy") or "fail").strip().lower() + try: + merge_strategy = ArtifactMergeStrategy(merge_raw) + except ValueError as exc: + supported = ", ".join(item.value for item in ArtifactMergeStrategy) + raise ArtifactSchemaError( + f"artifact[{index}].merge must be one of: {supported}" + ) from exc + + ownership_raw = payload.get("ownership", default_ownership) + ownership = _coerce_string_tuple( + ownership_raw, + field_name=f"artifact[{index}].ownership", + ) + + metadata_raw = payload.get("metadata", {}) + if metadata_raw is None: + metadata = {} + elif isinstance(metadata_raw, dict): + metadata = dict(metadata_raw) + else: + raise ArtifactSchemaError(f"artifact[{index}].metadata must be an object") + + created_at_raw = payload.get("created_at") + if created_at_raw is None: + created_at = time.time() + else: + try: + created_at = float(created_at_raw) + except Exception as exc: + raise ArtifactSchemaError( + f"artifact[{index}].created_at must be a number" + ) from exc + + artifact_id = str(payload.get("id") or f"{node_id}:{artifact_type.value}:{key}:{index}") + + return cls( + artifact_id=artifact_id, + node_id=node_id, + artifact_type=artifact_type, + key=key, + content=content, + merge_strategy=merge_strategy, + ownership=ownership, + metadata=metadata, + created_at=created_at, + ) + + def to_dict(self) -> dict[str, Any]: + """Serialize artifact record into JSON-safe dict.""" + return { + "id": self.artifact_id, + "node_id": self.node_id, + "type": self.artifact_type.value, + "key": self.key, + "content": self.content, + "merge": self.merge_strategy.value, + "ownership": list(self.ownership), + "metadata": dict(self.metadata), + "created_at": self.created_at, + } + + +class ArtifactBus: + """In-memory artifact exchange bus with schema and conflict control.""" + + def __init__(self) -> None: + self._records_by_node: dict[str, list[ArtifactRecord]] = {} + self._records: list[ArtifactRecord] = [] + self._merged_values: dict[tuple[str, str], Any] = {} + self._last_writer: dict[tuple[str, str], str] = {} + + def publish_from_output( + self, + *, + node_id: str, + output: Any, + ownership: tuple[str, ...] = (), + ) -> list[ArtifactRecord]: + """Parse node output and publish artifacts to the bus.""" + payload = self._extract_payload( + node_id=node_id, + output=output, + ownership=ownership, + ) + return self.publish( + node_id=node_id, + payload=payload, + default_ownership=ownership, + ) + + def publish( + self, + *, + node_id: str, + payload: Any, + default_ownership: tuple[str, ...] = (), + ) -> list[ArtifactRecord]: + """Validate and merge a producer payload.""" + artifacts_payload = _extract_artifact_list(payload) + records = [ + ArtifactRecord.from_dict( + item, + node_id=node_id, + index=index, + default_ownership=default_ownership, + ) + for index, item in enumerate(artifacts_payload) + ] + for record in records: + self._merge_record(record) + self._records.append(record) + self._records_by_node.setdefault(record.node_id, []).append(record) + return records + + def build_dependency_view(self, node_ids: Iterable[str]) -> dict[str, dict[str, Any]]: + """Build consumer payload for dependency nodes.""" + view: dict[str, dict[str, Any]] = {} + for node_id in node_ids: + view[node_id] = self.build_node_bundle(node_id) + return view + + def build_node_bundle(self, node_id: str) -> dict[str, Any]: + """Build per-node artifact bundle for workflow summary/output.""" + records = list(self._records_by_node.get(node_id, [])) + merged: dict[str, Any] = {} + for record in records: + key_tuple = (record.artifact_type.value, record.key) + merged_key = f"{record.artifact_type.value}:{record.key}" + merged[merged_key] = self._merged_values.get(key_tuple, record.content) + return { + "artifacts": [record.to_dict() for record in records], + "merged": merged, + } + + def snapshot(self) -> dict[str, Any]: + """Return global artifact bus snapshot.""" + merged: dict[str, Any] = { + f"{artifact_type}:{key}": value + for (artifact_type, key), value in self._merged_values.items() + } + return { + "records": [record.to_dict() for record in self._records], + "merged": merged, + } + + def _merge_record(self, record: ArtifactRecord) -> None: + key = (record.artifact_type.value, record.key) + if key not in self._merged_values: + self._merged_values[key] = record.content + self._last_writer[key] = record.node_id + return + + strategy = record.merge_strategy + existing_value = self._merged_values[key] + existing_node = self._last_writer.get(key, "") + + if strategy == ArtifactMergeStrategy.REPLACE: + self._merged_values[key] = record.content + self._last_writer[key] = record.node_id + return + + if strategy == ArtifactMergeStrategy.APPEND: + if isinstance(existing_value, list): + merged_value = list(existing_value) + else: + merged_value = [existing_value] + merged_value.append(record.content) + self._merged_values[key] = merged_value + self._last_writer[key] = record.node_id + return + + raise ArtifactConflictError( + "artifact conflict for " + f"{record.artifact_type.value}:{record.key} " + f"(existing_node={existing_node}, incoming_node={record.node_id}, strategy=fail)" + ) + + def _extract_payload( + self, + *, + node_id: str, + output: Any, + ownership: tuple[str, ...], + ) -> Any: + if isinstance(output, dict) and "artifacts" in output: + return output + if isinstance(output, list): + return {"artifacts": output} + if isinstance(output, dict) and "type" in output and "key" in output: + return {"artifacts": [output]} + if isinstance(output, str): + stripped = output.strip() + if stripped.startswith("{") or stripped.startswith("["): + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict) and "artifacts" in decoded: + return decoded + if isinstance(decoded, list): + return {"artifacts": decoded} + if isinstance(decoded, dict) and "type" in decoded and "key" in decoded: + return {"artifacts": [decoded]} + except Exception: + pass + return { + "artifacts": [ + { + "type": ArtifactType.REPORT.value, + "key": f"{node_id}.report", + "content": "" if output is None else str(output), + "merge": ArtifactMergeStrategy.REPLACE.value, + "ownership": list(ownership), + } + ] + } + + +def _extract_artifact_list(payload: Any) -> list[dict[str, Any]]: + if isinstance(payload, dict): + artifacts = payload.get("artifacts") + if not isinstance(artifacts, list): + raise ArtifactSchemaError("artifact payload must contain artifacts as array") + return artifacts + if isinstance(payload, list): + return payload + raise ArtifactSchemaError("artifact payload must be an object or list") + + +def _coerce_string_tuple(raw: Any, *, field_name: str) -> tuple[str, ...]: + if raw is None: + return () + if isinstance(raw, str): + value = raw.strip() + return (value,) if value else () + if isinstance(raw, tuple): + values = [str(item).strip() for item in raw if str(item).strip()] + return tuple(values) + if isinstance(raw, list): + values = [str(item).strip() for item in raw if str(item).strip()] + return tuple(values) + raise ArtifactSchemaError(f"{field_name} must be a string or list of strings") + + +__all__ = [ + "ArtifactSchemaError", + "ArtifactConflictError", + "ArtifactType", + "ArtifactMergeStrategy", + "ArtifactRecord", + "ArtifactBus", +] diff --git a/claude_code/services/event_journal.py b/claude_code/services/event_journal.py new file mode 100644 index 0000000..70b8614 --- /dev/null +++ b/claude_code/services/event_journal.py @@ -0,0 +1,536 @@ +"""Runtime event journal for audit and replay. + +Stores structured events and provides filtered query/replay helpers. +""" + +from __future__ import annotations + +import json +import sqlite3 +import threading +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional +from uuid import uuid4 + + +EVENT_JOURNAL_SCHEMA_VERSION = 1 +EVENT_JOURNAL_SCHEMA_KEY = "event_journal_schema_version" + + +@dataclass(frozen=True, slots=True) +class EventJournalEntry: + """Single immutable event journal entry.""" + + event_id: str + version: int + event_type: str + sequence: int + timestamp: float + session_id: str | None = None + conversation_id: str | None = None + source: str = "runtime" + payload: dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "event_id": self.event_id, + "version": self.version, + "event_type": self.event_type, + "sequence": self.sequence, + "timestamp": self.timestamp, + "session_id": self.session_id, + "conversation_id": self.conversation_id, + "source": self.source, + "payload": self.payload, + "metadata": self.metadata, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "EventJournalEntry": + version = int(data.get("version", EVENT_JOURNAL_SCHEMA_VERSION)) + if version <= 0: + raise ValueError(f"invalid event journal version: {version}") + + event_type = str(data.get("event_type", "")).strip() + if not event_type: + raise ValueError("event_type is required") + + sequence = int(data.get("sequence", 0)) + if sequence <= 0: + raise ValueError("sequence must be > 0") + + timestamp = float(data.get("timestamp", 0.0)) + if timestamp <= 0: + raise ValueError("timestamp must be > 0") + + payload = data.get("payload", {}) + metadata = data.get("metadata", {}) + if not isinstance(payload, dict): + raise ValueError("payload must be an object") + if not isinstance(metadata, dict): + raise ValueError("metadata must be an object") + + return cls( + event_id=str(data.get("event_id") or uuid4().hex), + version=version, + event_type=event_type, + sequence=sequence, + timestamp=timestamp, + session_id=data.get("session_id"), + conversation_id=data.get("conversation_id"), + source=str(data.get("source", "runtime")), + payload=payload, + metadata=metadata, + ) + + +class EventJournal: + """File-backed event journal with query and replay capabilities.""" + + def __init__(self, storage_path: str | Path) -> None: + self._storage_path = Path(storage_path) + self._storage_path.parent.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + self._sequence = 1 + self._initialized = False + self._last_parse_errors = 0 + self._last_invalid_lines = 0 + + @property + def storage_path(self) -> Path: + return self._storage_path + + def append_event( + self, + *, + event_type: str, + payload: Optional[dict[str, Any]] = None, + session_id: Optional[str] = None, + conversation_id: Optional[str] = None, + source: str = "runtime", + metadata: Optional[dict[str, Any]] = None, + timestamp: Optional[float] = None, + ) -> EventJournalEntry: + """Append one event to journal.""" + normalized_type = str(event_type).strip() + if not normalized_type: + raise ValueError("event_type is required") + + with self._lock: + self._ensure_sequence_initialized() + entry = EventJournalEntry( + event_id=uuid4().hex, + version=EVENT_JOURNAL_SCHEMA_VERSION, + event_type=normalized_type, + sequence=self._sequence, + timestamp=float(timestamp or time.time()), + session_id=session_id, + conversation_id=conversation_id, + source=source, + payload=dict(payload or {}), + metadata=dict(metadata or {}), + ) + self._sequence += 1 + self._append_line(entry) + return entry + + def query_events( + self, + *, + session_id: Optional[str] = None, + event_types: Optional[list[str]] = None, + limit: Optional[int] = None, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + ) -> list[EventJournalEntry]: + """Query journal entries with basic filters.""" + entries = self._read_all_entries() + types_filter = {item.strip() for item in (event_types or []) if item and item.strip()} + + filtered: list[EventJournalEntry] = [] + for entry in entries: + if session_id and entry.session_id != session_id: + continue + if types_filter and entry.event_type not in types_filter: + continue + if since_sequence is not None and entry.sequence < since_sequence: + continue + if until_sequence is not None and entry.sequence > until_sequence: + continue + filtered.append(entry) + + filtered.sort(key=lambda item: (item.sequence, item.timestamp, item.event_id)) + if limit is not None and limit > 0: + filtered = filtered[-limit:] + return filtered + + def replay_events( + self, + *, + session_id: Optional[str] = None, + event_types: Optional[list[str]] = None, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + limit: Optional[int] = None, + ) -> list[EventJournalEntry]: + """Replay events in stable sequence order.""" + return self.query_events( + session_id=session_id, + event_types=event_types, + since_sequence=since_sequence, + until_sequence=until_sequence, + limit=limit, + ) + + def get_diagnostics(self) -> dict[str, Any]: + """Return lightweight diagnostics for parser recovery visibility.""" + return { + "storage_path": str(self._storage_path), + "parse_errors": self._last_parse_errors, + "invalid_lines": self._last_invalid_lines, + } + + def _append_line(self, entry: EventJournalEntry) -> None: + with open(self._storage_path, "a", encoding="utf-8") as handle: + handle.write(json.dumps(entry.to_dict(), ensure_ascii=False)) + handle.write("\n") + + def _ensure_sequence_initialized(self) -> None: + if self._initialized: + return + entries = self._read_all_entries() + if entries: + self._sequence = max(item.sequence for item in entries) + 1 + self._initialized = True + + def _read_all_entries(self) -> list[EventJournalEntry]: + if not self._storage_path.exists(): + self._last_parse_errors = 0 + self._last_invalid_lines = 0 + return [] + + entries: list[EventJournalEntry] = [] + parse_errors = 0 + invalid_lines = 0 + with open(self._storage_path, "r", encoding="utf-8") as handle: + for raw_line in handle: + line = raw_line.strip() + if not line: + continue + try: + raw_event = json.loads(line) + if not isinstance(raw_event, dict): + invalid_lines += 1 + continue + entries.append(EventJournalEntry.from_dict(raw_event)) + except Exception: + parse_errors += 1 + continue + + self._last_parse_errors = parse_errors + self._last_invalid_lines = invalid_lines + return entries + + +class SQLiteEventJournal: + """SQLite-backed event journal with query/replay capabilities.""" + + def __init__(self, db_path: str | Path) -> None: + self._db_path = Path(db_path) + self._db_path.parent.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + self._initialize_schema() + + @property + def storage_path(self) -> Path: + return self._db_path + + def append_event( + self, + *, + event_type: str, + payload: Optional[dict[str, Any]] = None, + session_id: Optional[str] = None, + conversation_id: Optional[str] = None, + source: str = "runtime", + metadata: Optional[dict[str, Any]] = None, + timestamp: Optional[float] = None, + ) -> EventJournalEntry: + normalized_type = str(event_type).strip() + if not normalized_type: + raise ValueError("event_type is required") + + event_ts = float(timestamp or time.time()) + payload_json = json.dumps(dict(payload or {}), ensure_ascii=False) + metadata_json = json.dumps(dict(metadata or {}), ensure_ascii=False) + + with self._lock: + with self._connect() as conn: + row = conn.execute( + "SELECT COALESCE(MAX(sequence), 0) + 1 FROM events" + ).fetchone() + sequence = int(row[0]) if row and row[0] is not None else 1 + event_id = uuid4().hex + conn.execute( + """ + INSERT INTO events ( + event_id, + version, + event_type, + sequence, + timestamp, + session_id, + conversation_id, + source, + payload_json, + metadata_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + event_id, + EVENT_JOURNAL_SCHEMA_VERSION, + normalized_type, + sequence, + event_ts, + session_id, + conversation_id, + source, + payload_json, + metadata_json, + ), + ) + + return EventJournalEntry( + event_id=event_id, + version=EVENT_JOURNAL_SCHEMA_VERSION, + event_type=normalized_type, + sequence=sequence, + timestamp=event_ts, + session_id=session_id, + conversation_id=conversation_id, + source=source, + payload=dict(payload or {}), + metadata=dict(metadata or {}), + ) + + def query_events( + self, + *, + session_id: Optional[str] = None, + event_types: Optional[list[str]] = None, + limit: Optional[int] = None, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + ) -> list[EventJournalEntry]: + where: list[str] = [] + params: list[Any] = [] + if session_id: + where.append("session_id = ?") + params.append(session_id) + normalized_types = [item.strip() for item in (event_types or []) if item and item.strip()] + if normalized_types: + placeholders = ",".join(["?"] * len(normalized_types)) + where.append(f"event_type IN ({placeholders})") + params.extend(normalized_types) + if since_sequence is not None: + where.append("sequence >= ?") + params.append(int(since_sequence)) + if until_sequence is not None: + where.append("sequence <= ?") + params.append(int(until_sequence)) + + sql = """ + SELECT + event_id, + version, + event_type, + sequence, + timestamp, + session_id, + conversation_id, + source, + payload_json, + metadata_json + FROM events + """ + if where: + sql += " WHERE " + " AND ".join(where) + sql += " ORDER BY sequence ASC, timestamp ASC, event_id ASC" + if limit is not None and limit > 0: + sql += " LIMIT ?" + params.append(int(limit)) + + with self._connect() as conn: + rows = conn.execute(sql, tuple(params)).fetchall() + + entries: list[EventJournalEntry] = [] + for row in rows: + payload = self._parse_json(row[8], default={}) + metadata = self._parse_json(row[9], default={}) + try: + entries.append( + EventJournalEntry( + event_id=str(row[0]), + version=int(row[1]), + event_type=str(row[2]), + sequence=int(row[3]), + timestamp=float(row[4]), + session_id=row[5], + conversation_id=row[6], + source=str(row[7]), + payload=payload if isinstance(payload, dict) else {}, + metadata=metadata if isinstance(metadata, dict) else {}, + ) + ) + except Exception: + continue + return entries + + def replay_events( + self, + *, + session_id: Optional[str] = None, + event_types: Optional[list[str]] = None, + since_sequence: Optional[int] = None, + until_sequence: Optional[int] = None, + limit: Optional[int] = None, + ) -> list[EventJournalEntry]: + return self.query_events( + session_id=session_id, + event_types=event_types, + since_sequence=since_sequence, + until_sequence=until_sequence, + limit=limit, + ) + + def get_diagnostics(self) -> dict[str, Any]: + with self._connect() as conn: + row = conn.execute("SELECT COUNT(*) FROM events").fetchone() + count = int(row[0]) if row and row[0] is not None else 0 + return { + "storage_path": str(self._db_path), + "backend": "sqlite", + "events_count": count, + "parse_errors": 0, + "invalid_lines": 0, + } + + def _initialize_schema(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS journal_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS events ( + event_id TEXT PRIMARY KEY, + version INTEGER NOT NULL, + event_type TEXT NOT NULL, + sequence INTEGER NOT NULL UNIQUE, + timestamp REAL NOT NULL, + session_id TEXT, + conversation_id TEXT, + source TEXT NOT NULL, + payload_json TEXT NOT NULL, + metadata_json TEXT NOT NULL + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_events_session_sequence ON events (session_id, sequence)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_events_type_sequence ON events (event_type, sequence)" + ) + version = self._read_schema_version(conn) + if version is None: + self._write_schema_version(conn, EVENT_JOURNAL_SCHEMA_VERSION) + elif version > EVENT_JOURNAL_SCHEMA_VERSION: + raise RuntimeError( + "Event journal SQLite schema is newer than this binary: " + f"{version} > {EVENT_JOURNAL_SCHEMA_VERSION}" + ) + elif version < EVENT_JOURNAL_SCHEMA_VERSION: + self._migrate_schema(conn, version, EVENT_JOURNAL_SCHEMA_VERSION) + self._write_schema_version(conn, EVENT_JOURNAL_SCHEMA_VERSION) + + def _connect(self) -> sqlite3.Connection: + return sqlite3.connect(self._db_path, timeout=5.0) + + @staticmethod + def _parse_json(raw_value: Any, *, default: Any) -> Any: + if not isinstance(raw_value, str): + return default + try: + return json.loads(raw_value) + except json.JSONDecodeError: + return default + + @staticmethod + def _read_schema_version(conn: sqlite3.Connection) -> int | None: + row = conn.execute( + "SELECT value FROM journal_metadata WHERE key = ?", + (EVENT_JOURNAL_SCHEMA_KEY,), + ).fetchone() + if row is None: + return None + try: + return int(row[0]) + except (TypeError, ValueError): + return None + + @staticmethod + def _write_schema_version(conn: sqlite3.Connection, version: int) -> None: + conn.execute( + """ + INSERT INTO journal_metadata (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """, + (EVENT_JOURNAL_SCHEMA_KEY, str(version)), + ) + + @staticmethod + def _migrate_schema(conn: sqlite3.Connection, from_version: int, to_version: int) -> None: + if from_version < 1: + raise RuntimeError(f"Unsupported event journal schema version: {from_version}") + if to_version != EVENT_JOURNAL_SCHEMA_VERSION: + raise RuntimeError(f"Unsupported target event journal schema version: {to_version}") + # Schema v1 is current. Future migrations will be appended here. + + +_event_journal: Optional[EventJournal | SQLiteEventJournal] = None + + +def get_event_journal(storage_path: Optional[str | Path] = None) -> EventJournal | SQLiteEventJournal: + """Get global event journal singleton.""" + global _event_journal + if _event_journal is None: + path = storage_path or (Path.cwd() / ".claude" / "event_journal.jsonl") + _event_journal = EventJournal(path) + return _event_journal + + +def set_event_journal(journal: EventJournal | SQLiteEventJournal) -> None: + """Override global event journal singleton.""" + global _event_journal + _event_journal = journal + + +__all__ = [ + "EVENT_JOURNAL_SCHEMA_VERSION", + "EVENT_JOURNAL_SCHEMA_KEY", + "EventJournalEntry", + "EventJournal", + "SQLiteEventJournal", + "get_event_journal", + "set_event_journal", +] diff --git a/claude_code/services/github_ci_workflow.py b/claude_code/services/github_ci_workflow.py new file mode 100644 index 0000000..686b693 --- /dev/null +++ b/claude_code/services/github_ci_workflow.py @@ -0,0 +1,613 @@ +"""GitHub/CI workflow service for issue->plan->code->test->review->PR automation.""" + +from __future__ import annotations + +import os +import shlex +import subprocess +import time +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Callable, Optional + + +class GitHubCIWorkflowError(RuntimeError): + """Raised when workflow execution fails with structured metadata.""" + + def __init__( + self, + message: str, + *, + code: str = "workflow_failed", + status_code: int = 500, + details: Any = None, + step: str | None = None, + ) -> None: + super().__init__(message) + self.message = message + self.code = code + self.status_code = status_code + self.details = details + self.step = step + + +class WorkflowStepStatus(Enum): + """Step state in workflow execution.""" + + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + SKIPPED = "skipped" + + +@dataclass(slots=True) +class WorkflowStepRecord: + """Runtime state for one workflow step.""" + + name: str + status: WorkflowStepStatus = WorkflowStepStatus.PENDING + message: str = "" + commands: list[str] = field(default_factory=list) + started_at: str | None = None + finished_at: str | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "name": self.name, + "status": self.status.value, + "message": self.message, + "commands": list(self.commands), + "started_at": self.started_at, + "finished_at": self.finished_at, + } + + +@dataclass(slots=True) +class _WorkflowState: + """Mutable workflow state accumulator.""" + + workflow_id: str + working_directory: str + headless_ci: bool + steps: list[WorkflowStepRecord] = field( + default_factory=lambda: [ + WorkflowStepRecord(name="issue"), + WorkflowStepRecord(name="plan"), + WorkflowStepRecord(name="code"), + WorkflowStepRecord(name="test"), + WorkflowStepRecord(name="review"), + WorkflowStepRecord(name="pr"), + ] + ) + branch_name: str | None = None + pull_request_url: str | None = None + started_at: str = field(default_factory=lambda: _utc_now_iso()) + finished_at: str | None = None + + def get_step(self, name: str) -> WorkflowStepRecord: + for step in self.steps: + if step.name == name: + return step + raise KeyError(f"unknown workflow step: {name}") + + def start(self, name: str) -> WorkflowStepRecord: + step = self.get_step(name) + step.status = WorkflowStepStatus.RUNNING + step.started_at = _utc_now_iso() + return step + + def complete(self, name: str, message: str = "") -> WorkflowStepRecord: + step = self.get_step(name) + step.status = WorkflowStepStatus.COMPLETED + step.message = message + step.finished_at = _utc_now_iso() + if step.started_at is None: + step.started_at = step.finished_at + return step + + def fail(self, name: str, message: str) -> WorkflowStepRecord: + step = self.get_step(name) + step.status = WorkflowStepStatus.FAILED + step.message = message + step.finished_at = _utc_now_iso() + if step.started_at is None: + step.started_at = step.finished_at + return step + + def skip_pending_after(self, name: str, message: str) -> None: + target_seen = False + for step in self.steps: + if step.name == name: + target_seen = True + continue + if not target_seen: + continue + if step.status != WorkflowStepStatus.PENDING: + continue + step.status = WorkflowStepStatus.SKIPPED + step.message = message + step.started_at = _utc_now_iso() + step.finished_at = step.started_at + + def to_dict(self, *, status: str) -> dict[str, Any]: + return { + "workflow_id": self.workflow_id, + "status": status, + "working_directory": self.working_directory, + "headless_ci": self.headless_ci, + "branch_name": self.branch_name, + "pull_request_url": self.pull_request_url, + "started_at": self.started_at, + "finished_at": self.finished_at, + "steps": [step.to_dict() for step in self.steps], + } + + +CommandRunner = Callable[[list[str], str], subprocess.CompletedProcess[str]] + + +class GitHubCIWorkflowService: + """Execute workflow card P2-04 with pluggable command runner.""" + + def __init__(self, *, command_runner: Optional[CommandRunner] = None) -> None: + self._command_runner = command_runner or _default_command_runner + + def run_workflow( + self, + *, + working_directory: str, + issue_reference: str | None = None, + plan_summary: str | None = None, + branch_name: str | None = None, + base_branch: str = "main", + commit_message: str | None = None, + pr_title: str | None = None, + pr_body: str | None = None, + ci_commands: list[str] | None = None, + headless_ci: bool = True, + allow_dirty_repo: bool = False, + push_remote: bool = True, + create_pull_request: bool = True, + ) -> dict[str, Any]: + ci_commands = [cmd.strip() for cmd in (ci_commands or []) if str(cmd).strip()] + workflow_id = f"ghci-{int(time.time() * 1000)}" + state = _WorkflowState( + workflow_id=workflow_id, + working_directory=working_directory, + headless_ci=headless_ci, + ) + + try: + self._run_issue_step(state, issue_reference) + self._run_plan_step(state, plan_summary) + + active_branch = self._run_code_step( + state, + issue_reference=issue_reference, + plan_summary=plan_summary, + branch_name=branch_name, + base_branch=base_branch, + commit_message=commit_message, + allow_dirty_repo=allow_dirty_repo, + ) + state.branch_name = active_branch + + self._run_test_step(state, ci_commands=ci_commands, headless_ci=headless_ci) + self._run_review_step(state, base_branch=base_branch) + self._run_pr_step( + state, + issue_reference=issue_reference, + plan_summary=plan_summary, + branch_name=active_branch, + base_branch=base_branch, + pr_title=pr_title, + pr_body=pr_body, + push_remote=push_remote, + create_pull_request=create_pull_request, + ) + except GitHubCIWorkflowError as exc: + failed_step = exc.step or "code" + state.fail(failed_step, exc.message) + state.skip_pending_after(failed_step, "skipped due to previous failure") + state.finished_at = _utc_now_iso() + details = dict(exc.details or {}) + details["workflow"] = state.to_dict(status="failed") + raise GitHubCIWorkflowError( + exc.message, + code=exc.code, + status_code=exc.status_code, + details=details, + step=failed_step, + ) from exc + + state.finished_at = _utc_now_iso() + return state.to_dict(status="completed") + + def _run_issue_step(self, state: _WorkflowState, issue_reference: str | None) -> None: + state.start("issue") + state.complete("issue", issue_reference or "no issue reference provided") + + def _run_plan_step(self, state: _WorkflowState, plan_summary: str | None) -> None: + state.start("plan") + state.complete("plan", plan_summary or "no plan summary provided") + + def _run_code_step( + self, + state: _WorkflowState, + *, + issue_reference: str | None, + plan_summary: str | None, + branch_name: str | None, + base_branch: str, + commit_message: str | None, + allow_dirty_repo: bool, + ) -> str: + step = state.start("code") + self._expect_success( + ["git", "rev-parse", "--is-inside-work-tree"], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + + status_result = self._expect_success( + ["git", "status", "--porcelain"], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + is_dirty = bool(status_result.stdout.strip()) + if is_dirty and not allow_dirty_repo: + raise GitHubCIWorkflowError( + "Repository working tree is not clean; set allow_dirty_repo=true to continue.", + code="repo_dirty", + status_code=409, + step=step.name, + details={"status_output": status_result.stdout}, + ) + + target_branch = branch_name or _build_branch_name(issue_reference) + self._expect_success( + ["git", "checkout", "-B", target_branch], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + self._expect_success( + ["git", "add", "-A"], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + + staged_result = self._expect_success( + ["git", "diff", "--cached", "--name-only"], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + staged_files = [line.strip() for line in staged_result.stdout.splitlines() if line.strip()] + if staged_files: + resolved_message = commit_message or _build_default_commit_message( + issue_reference=issue_reference, + plan_summary=plan_summary, + ) + self._expect_success( + ["git", "commit", "-m", resolved_message], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + summary = ( + f"checked out {target_branch} from {base_branch}; " + f"committed {len(staged_files)} staged file(s)" + ) + else: + summary = f"checked out {target_branch}; no staged changes to commit" + + state.complete(step.name, summary) + return target_branch + + def _run_test_step( + self, + state: _WorkflowState, + *, + ci_commands: list[str], + headless_ci: bool, + ) -> None: + step = state.start("test") + if not headless_ci: + state.complete(step.name, "headless CI disabled by request") + return + if not ci_commands: + state.complete(step.name, "headless CI enabled; no ci_commands configured") + return + + for command_line in ci_commands: + command = _split_command(command_line) + self._expect_success( + command, + working_directory=state.working_directory, + step=step.name, + record=step, + ) + state.complete(step.name, f"executed {len(ci_commands)} headless CI command(s)") + + def _run_review_step(self, state: _WorkflowState, *, base_branch: str) -> None: + step = state.start("review") + review_command = ["git", "diff", "--name-only", f"{base_branch}...HEAD"] + result = self._run_command(review_command, state.working_directory, record=step) + changed_files: list[str] + if result.returncode == 0: + changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()] + state.complete(step.name, f"review snapshot contains {len(changed_files)} changed file(s)") + return + + fallback_result = self._expect_success( + ["git", "diff", "--name-only"], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + changed_files = [line.strip() for line in fallback_result.stdout.splitlines() if line.strip()] + state.complete( + step.name, + ( + f"review fallback used (base branch unavailable: {base_branch}); " + f"changed file(s)={len(changed_files)}" + ), + ) + + def _run_pr_step( + self, + state: _WorkflowState, + *, + issue_reference: str | None, + plan_summary: str | None, + branch_name: str, + base_branch: str, + pr_title: str | None, + pr_body: str | None, + push_remote: bool, + create_pull_request: bool, + ) -> None: + step = state.start("pr") + if not create_pull_request: + state.complete(step.name, "pull request creation skipped by request") + return + + if push_remote: + self._expect_success( + ["git", "push", "-u", "origin", branch_name], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + + resolved_title = pr_title or _build_default_pr_title(issue_reference, plan_summary) + resolved_body = pr_body or _build_default_pr_body(issue_reference, plan_summary, headless_ci=state.headless_ci) + pr_result = self._expect_success( + [ + "gh", + "pr", + "create", + "--base", + base_branch, + "--head", + branch_name, + "--title", + resolved_title, + "--body", + resolved_body, + ], + working_directory=state.working_directory, + step=step.name, + record=step, + ) + + pr_url = _extract_first_url(pr_result.stdout) or _extract_first_url(pr_result.stderr) + state.pull_request_url = pr_url + if pr_url: + state.complete(step.name, f"pull request created: {pr_url}") + else: + state.complete(step.name, "pull request created (URL not detected in command output)") + + def _expect_success( + self, + command: list[str], + *, + working_directory: str, + step: str, + record: WorkflowStepRecord, + ) -> subprocess.CompletedProcess[str]: + result = self._run_command(command, working_directory, record=record) + if result.returncode == 0: + return result + raise _command_failure_to_error( + command=command, + result=result, + step=step, + ) + + def _run_command( + self, + command: list[str], + working_directory: str, + *, + record: WorkflowStepRecord, + ) -> subprocess.CompletedProcess[str]: + command_text = _join_command(command) + record.commands.append(command_text) + return self._command_runner(command, working_directory) + + +def _default_command_runner(command: list[str], working_directory: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + cwd=working_directory, + capture_output=True, + text=True, + ) + + +def _command_failure_to_error( + *, + command: list[str], + result: subprocess.CompletedProcess[str], + step: str, +) -> GitHubCIWorkflowError: + stderr_text = (result.stderr or "").strip() + stdout_text = (result.stdout or "").strip() + output_text = f"{stderr_text}\n{stdout_text}".strip().lower() + if _contains_any( + output_text, + [ + "permission denied", + "resource not accessible", + "forbidden", + "403", + "authentication failed", + "not authorized", + "requires authentication", + ], + ): + code = "permission_denied" + status_code = 403 + elif _contains_any( + output_text, + [ + "could not resolve host", + "network is unreachable", + "connection timed out", + "timed out", + "failed to connect", + "connection reset", + ], + ): + code = "network_failure" + status_code = 503 + elif _contains_any( + output_text, + [ + "not a git repository", + "unknown revision", + ], + ): + code = "repo_error" + status_code = 400 + elif _contains_any( + output_text, + [ + "not recognized as an internal or external command", + "command not found", + "no such file or directory", + ], + ): + code = "command_unavailable" + status_code = 503 + else: + code = "workflow_command_failed" + status_code = 500 + + message = ( + f"Command failed at step '{step}': {_join_command(command)} " + f"(exit={result.returncode})" + ) + details = { + "command": command, + "return_code": result.returncode, + "stderr": stderr_text, + "stdout": stdout_text, + } + return GitHubCIWorkflowError( + message, + code=code, + status_code=status_code, + details=details, + step=step, + ) + + +def _contains_any(text: str, tokens: list[str]) -> bool: + return any(token in text for token in tokens) + + +def _build_branch_name(issue_reference: str | None) -> str: + issue = (issue_reference or "").strip().replace("#", "") + if issue: + normalized = "".join(ch if ch.isalnum() or ch in "-_" else "-" for ch in issue).strip("-") + if normalized: + return f"codex/p2-04-{normalized}" + return f"codex/p2-04-{int(time.time())}" + + +def _build_default_commit_message(*, issue_reference: str | None, plan_summary: str | None) -> str: + if issue_reference: + return f"feat(workflow): progress {issue_reference}" + if plan_summary: + compact = " ".join(plan_summary.split()) + if compact: + return f"feat(workflow): {compact[:72]}" + return "feat(workflow): progress github ci workflow" + + +def _build_default_pr_title(issue_reference: str | None, plan_summary: str | None) -> str: + if issue_reference and plan_summary: + return f"{issue_reference}: {plan_summary[:80]}" + if issue_reference: + return f"{issue_reference}: workflow delivery" + if plan_summary: + return f"Workflow delivery: {plan_summary[:80]}" + return "Workflow delivery update" + + +def _build_default_pr_body(issue_reference: str | None, plan_summary: str | None, *, headless_ci: bool) -> str: + lines = [ + "Automated issue->plan->code->test->review->PR workflow execution.", + ] + if issue_reference: + lines.append(f"Issue: {issue_reference}") + if plan_summary: + lines.append(f"Plan: {plan_summary}") + lines.append(f"Headless CI mode: {'enabled' if headless_ci else 'disabled'}") + return "\n".join(lines) + + +def _split_command(command_line: str) -> list[str]: + parsed = shlex.split(command_line, posix=os.name != "nt") + if not parsed: + raise GitHubCIWorkflowError( + f"Invalid empty CI command: {command_line!r}", + code="validation_error", + status_code=400, + step="test", + ) + return parsed + + +def _join_command(command: list[str]) -> str: + return " ".join(command) + + +def _extract_first_url(text: str) -> str | None: + if not text: + return None + for token in text.replace("\r", " ").replace("\n", " ").split(): + normalized = token.strip() + if normalized.startswith("http://") or normalized.startswith("https://"): + return normalized + return None + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +__all__ = [ + "GitHubCIWorkflowError", + "WorkflowStepStatus", + "WorkflowStepRecord", + "GitHubCIWorkflowService", +] diff --git a/claude_code/services/history_manager.py b/claude_code/services/history_manager.py index bcd7cce..af3d6bc 100644 --- a/claude_code/services/history_manager.py +++ b/claude_code/services/history_manager.py @@ -256,6 +256,108 @@ def search(self, query: str, limit: int = 10) -> list[HistoryEntry]: break return results + + def archive_session_messages( + self, + session_id: str, + messages: list[dict[str, Any]], + *, + working_directory: str | None = None, + model: str | None = None, + replace_existing: bool = False, + ) -> int: + """Archive persisted session messages into history storage. + + Session is the source-of-truth for active conversations. This method + materializes a stable, searchable archive in history. + + Args: + session_id: Session identifier. + messages: Serialized session message list. + working_directory: Optional working directory metadata. + model: Optional model metadata. + replace_existing: If True, drop existing archive entries for session first. + + Returns: + Number of newly archived entries. + """ + if replace_existing: + self._entries = [ + entry + for entry in self._entries + if entry.metadata.get("session_id") != session_id + ] + + existing_message_ids = { + str(entry.metadata.get("message_id")) + for entry in self._entries + if entry.metadata.get("session_id") == session_id + and entry.metadata.get("message_id") is not None + } + + archived_count = 0 + now = time.time() + for index, message in enumerate(messages): + message_id = str(message.get("id") or f"message-{index}") + if message_id in existing_message_ids: + continue + + role = str(message.get("role", "assistant")) + content = message.get("content", "") + timestamp = float(message.get("timestamp") or now) + + metadata = dict(message.get("metadata") or {}) + metadata.update( + { + "session_id": session_id, + "message_id": message_id, + "archived_from": "session", + } + ) + if working_directory: + metadata["working_directory"] = working_directory + if model: + metadata["model"] = model + if message.get("tool_call_id"): + metadata["tool_call_id"] = message.get("tool_call_id") + if message.get("tool_name"): + metadata["tool_name"] = message.get("tool_name") + + self._entries.append( + HistoryEntry( + id=f"{session_id}:{message_id}", + role=role, + content=content, + timestamp=timestamp, + metadata=metadata, + ) + ) + existing_message_ids.add(message_id) + archived_count += 1 + + if len(self._entries) > self._max_entries: + self._entries = self._entries[-self._max_entries :] + + if archived_count > 0: + self.save() + + return archived_count + + def get_session_entries( + self, + session_id: str, + limit: Optional[int] = None, + ) -> list[HistoryEntry]: + """Get archived history entries for a specific session.""" + entries = [ + entry + for entry in self._entries + if entry.metadata.get("session_id") == session_id + ] + entries.sort(key=lambda item: item.timestamp) + if limit: + return entries[-limit:] + return entries def clear(self) -> None: """Clear all history and generate a new session ID.""" diff --git a/claude_code/services/hooks_manager.py b/claude_code/services/hooks_manager.py index f62a55f..f2ab2d5 100644 --- a/claude_code/services/hooks_manager.py +++ b/claude_code/services/hooks_manager.py @@ -15,10 +15,9 @@ import asyncio import json import shlex -import subprocess import time from pathlib import Path -from typing import Callable, Optional, Any +from typing import Optional, Any from dataclasses import dataclass, field from enum import Enum @@ -86,6 +85,16 @@ class HookResult: error: Optional[str] = None duration_ms: float = 0.0 + def to_dict(self) -> dict[str, Any]: + """Serialize hook execution result for audit/event payloads.""" + return { + "hook_name": self.hook_name, + "success": self.success, + "output": self.output, + "error": self.error, + "duration_ms": self.duration_ms, + } + class HooksManager: """Manages execution hooks. @@ -123,8 +132,25 @@ def _load_hooks(self) -> None: try: with open(self._config_path, "r", encoding="utf-8") as f: data = json.load(f) - - for hook_data in data.get("hooks", []): + + loaded_hooks = data.get("hooks", []) + if isinstance(loaded_hooks, dict): + # Backward compatibility with legacy {"hook_name": {...}} format. + loaded_hooks = [ + { + "name": hook_name, + **(hook_config if isinstance(hook_config, dict) else {}), + } + for hook_name, hook_config in loaded_hooks.items() + ] + + if not isinstance(loaded_hooks, list): + return + + self._hooks = [] + for hook_data in loaded_hooks: + if not isinstance(hook_data, dict): + continue try: hook = Hook( name=hook_data["name"], @@ -226,9 +252,29 @@ def get_hooks(self, event: Optional[HookEvent] = None) -> list[Hook]: Returns: List of enabled Hook objects. """ - if event: - return [h for h in self._hooks if h.event == event and h.enabled] - return [h for h in self._hooks if h.enabled] + return self.list_hooks(event=event, include_disabled=False) + + def list_hooks( + self, + event: Optional[HookEvent] = None, + *, + include_disabled: bool = False, + ) -> list[Hook]: + """List hooks with optional event/disabled filtering. + + Args: + event: Optional event to filter by. + include_disabled: Include disabled hooks when True. + + Returns: + Matching hook definitions. + """ + hooks = self._hooks + if event is not None: + hooks = [hook for hook in hooks if hook.event == event] + if include_disabled: + return list(hooks) + return [hook for hook in hooks if hook.enabled] async def trigger( self, diff --git a/claude_code/services/mcp/client.py b/claude_code/services/mcp/client.py index e9f6064..73b162f 100644 --- a/claude_code/services/mcp/client.py +++ b/claude_code/services/mcp/client.py @@ -101,7 +101,7 @@ def __init__(self, command: str, args: list[str], env: dict[str, str]): self._command = command self._args = args self._env = env - self._process: Optional[subprocess.Process] = None + self._process: Optional[asyncio.subprocess.Process] = None self._reader_task: Optional[asyncio.Task] = None self._pending_requests: dict[str, asyncio.Future] = {} self._request_id = 0 @@ -136,8 +136,8 @@ async def disconnect(self) -> None: async def send(self, message: dict) -> None: if not self._process or not self._process.stdin: raise ConnectionError("Not connected") - await self._process.stdin.write(json.dumps(message).encode() + b"\n") - await self._process.stdin.flush() + self._process.stdin.write(json.dumps(message).encode() + b"\n") + await self._process.stdin.drain() async def receive(self) -> dict: return await self._message_queue.get() @@ -176,8 +176,7 @@ async def _reader_loop(self) -> None: )) else: future.set_result(message.get("result")) - else: - await self._message_queue.put(message) + await self._message_queue.put(message) except json.JSONDecodeError: pass except asyncio.CancelledError: @@ -222,7 +221,7 @@ async def disconnect(self) -> None: await self._session.close() self._connected = False - async def send(self, message: dict) -> None: + async def send(self, message: dict) -> dict[str, Any]: if not self._session: raise ConnectionError("Not connected") @@ -275,26 +274,44 @@ def __init__(self, url: str, auth: Optional[MCPAuthToken] = None, self._url = url self._auth = auth self._ssl = ssl_context + self._aiohttp = None + self._session = None self._ws = None self._connected = False self._pending_requests: dict[str, asyncio.Future] = {} self._request_id = 0 + self._reader_task: Optional[asyncio.Task] = None + self._message_queue: asyncio.Queue = asyncio.Queue() async def connect(self) -> None: try: import aiohttp except ImportError: raise ImportError("aiohttp required for WebSocket transport: pip install aiohttp") - - session = aiohttp.ClientSession() - self._ws = await session.ws_connect(self._url, ssl=self._ssl) + self._aiohttp = aiohttp + self._session = aiohttp.ClientSession() + headers: dict[str, str] = {} + if self._auth: + headers["Authorization"] = f"Bearer {self._auth.access_token}" + self._ws = await self._session.ws_connect(self._url, ssl=self._ssl, headers=headers) self._connected = True - asyncio.create_task(self._reader_loop()) + self._reader_task = asyncio.create_task(self._reader_loop()) async def disconnect(self) -> None: + self._connected = False + if self._reader_task: + self._reader_task.cancel() + try: + await self._reader_task + except asyncio.CancelledError: + pass + self._reader_task = None if self._ws: await self._ws.close() - self._connected = False + self._ws = None + if self._session: + await self._session.close() + self._session = None async def send(self, message: dict) -> None: if not self._ws: @@ -302,9 +319,7 @@ async def send(self, message: dict) -> None: await self._ws.send_json(message) async def receive(self) -> dict: - if not self._ws: - raise ConnectionError("Not connected") - return await self._ws.receive_json() + return await self._message_queue.get() async def send_request(self, method: str, params: Optional[dict] = None) -> dict: request_id = str(self._request_id) @@ -323,9 +338,9 @@ async def _reader_loop(self) -> None: try: while self._connected: msg = await self._ws.receive() - if msg.type == aiohttp.WSMsgType.TEXT: + if msg.type == self._aiohttp.WSMsgType.TEXT: try: - data = msg.json() + data = json.loads(msg.data) if "id" in data: request_id = str(data["id"]) if request_id in self._pending_requests: @@ -336,9 +351,14 @@ async def _reader_loop(self) -> None: )) else: future.set_result(data.get("result")) + await self._message_queue.put(data) except json.JSONDecodeError: pass - elif msg.type == aiohttp.WSMsgType.ERROR: + elif msg.type in ( + self._aiohttp.WSMsgType.ERROR, + self._aiohttp.WSMsgType.CLOSED, + self._aiohttp.WSMsgType.CLOSE, + ): break except asyncio.CancelledError: pass @@ -398,11 +418,16 @@ def __init__(self): self._pending_requests: dict[str, asyncio.Future] = {} self._request_id = 0 self._notification_handlers: dict[str, Callable] = {} + self._sender: Optional[Callable[[dict[str, Any]], Awaitable[Any]]] = None def _next_request_id(self) -> str: """Generate the next request ID.""" self._request_id += 1 return str(self._request_id) + + def set_sender(self, sender: Callable[[dict[str, Any]], Awaitable[Any]]) -> None: + """Set async sender used for outbound JSON-RPC messages.""" + self._sender = sender async def send_request( self, @@ -421,12 +446,13 @@ async def send_request( } if params: message["params"] = params - - # In STDIO mode, we write to the process - # This is handled by the transport layer + + if self._sender is None: + raise RuntimeError("MCPProtocol sender not configured") + await self._sender(message) return await future - def send_notification(self, method: str, params: Optional[dict] = None) -> None: + async def send_notification(self, method: str, params: Optional[dict] = None) -> None: """Send a JSON-RPC notification (no response expected).""" message = { "jsonrpc": "2.0", @@ -434,6 +460,9 @@ def send_notification(self, method: str, params: Optional[dict] = None) -> None: } if params: message["params"] = params + if self._sender is None: + raise RuntimeError("MCPProtocol sender not configured") + await self._sender(message) def handle_response(self, response: dict) -> None: """Handle a JSON-RPC response.""" @@ -468,7 +497,8 @@ class MCPClient: def __init__(self, config: MCPConnectionConfig): self.config = config self.protocol = MCPProtocol() - self._process: Optional[subprocess.Process] = None + self._transport: Optional[MCPTransport] = None + self._receiver_task: Optional[asyncio.Task] = None self._state = MCPConnectionState.DISCONNECTED self._tools: list[MCPTool] = [] self._resources: list[MCPResource] = [] @@ -495,70 +525,105 @@ async def connect(self) -> None: return self._state = MCPConnectionState.CONNECTING - - if self.config.transport == MCPTransportType.STDIO: - await self._connect_stdio() - elif self.config.transport in (MCPTransportType.SSE, MCPTransportType.HTTP): - await self._connect_http() - else: - raise NotImplementedError(f"Transport {self.config.transport} not supported") - - async def _connect_stdio(self) -> None: - """Connect via STDIO transport.""" - env = {**os.environ, **self.config.env} - + try: - self._process = await asyncio.create_subprocess_exec( - self.config.command, - *self.config.args, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=env, - ) - - # Start reading responses - asyncio.create_task(self._read_stdout()) - - # Initialize the connection + self._transport = self._create_transport() + self.protocol.set_sender(self._send_with_transport) + await self._transport.connect() + self._receiver_task = asyncio.create_task(self._receive_loop()) await self._initialize() - self._state = MCPConnectionState.CONNECTED - except Exception as e: self._state = MCPConnectionState.ERROR + await self._cleanup_transport() raise ConnectionError(f"Failed to connect to MCP server: {e}") - - async def _connect_http(self) -> None: - """Connect via HTTP/SSE transport.""" - # HTTP/SSE transport implementation - # Would use aiohttp or similar - raise NotImplementedError("HTTP/SSE transport not yet implemented") - - async def _read_stdout(self) -> None: - """Read and process messages from stdout.""" - if not self._process or not self._process.stdout: + + def _create_transport(self) -> MCPTransport: + """Create transport instance from connection config.""" + auth = self._build_auth_token() + ssl_context = self._build_ssl_context() + + if self.config.transport == MCPTransportType.STDIO: + if not self.config.command: + raise ValueError("STDIO transport requires 'command'") + return MCPStdIOTransport( + command=self.config.command, + args=self.config.args, + env=self.config.env, + ) + + endpoint = self.config.url or self.config.command + if not endpoint: + raise ValueError("HTTP/SSE/WebSocket transport requires 'url' (or command fallback)") + + if self.config.transport in (MCPTransportType.SSE, MCPTransportType.HTTP): + return MCPHTTPTransport(endpoint, auth=auth, ssl_context=ssl_context) + if self.config.transport == MCPTransportType.WEBSOCKET: + return MCPWebSocketTransport(endpoint, auth=auth, ssl_context=ssl_context) + + raise NotImplementedError(f"Transport {self.config.transport} not supported") + + def _build_auth_token(self) -> Optional[MCPAuthToken]: + """Build auth token from connection config.""" + if self.config.auth_type in (MCPAuthType.API_KEY, MCPAuthType.BEARER): + if self.config.api_key: + return MCPAuthToken(access_token=self.config.api_key) + return None + + if self.config.auth_type == MCPAuthType.OAUTH and self.config.oauth_config: + token = self.config.oauth_config.access_token + if token: + return MCPAuthToken(access_token=token) + return None + + def _build_ssl_context(self) -> Optional[ssl.SSLContext]: + """Build SSL context according to config.ssl_verify.""" + if self.config.ssl_verify: + return None + insecure_context = ssl.create_default_context() + insecure_context.check_hostname = False + insecure_context.verify_mode = ssl.CERT_NONE + return insecure_context + + async def _send_with_transport(self, message: dict[str, Any]) -> None: + """Send JSON-RPC message through active transport.""" + if self._transport is None: + raise ConnectionError("MCP transport is not connected") + response = await self._transport.send(message) + if isinstance(response, dict) and "id" in response: + self.protocol.handle_response(response) + + async def _receive_loop(self) -> None: + """Dispatch inbound transport messages to MCPProtocol.""" + if self._transport is None: return - try: while True: - line = await self._process.stdout.readline() - if not line: - break - - try: - message = json.loads(line.decode('utf-8')) - - if "id" in message: - self.protocol.handle_response(message) - else: - self.protocol.handle_notification(message) - - except json.JSONDecodeError: - pass - + message = await self._transport.receive() + if not isinstance(message, dict): + continue + if "id" in message: + self.protocol.handle_response(message) + else: + self.protocol.handle_notification(message) except asyncio.CancelledError: pass + except Exception: + # Transport failures are surfaced through operation-level calls. + pass + + async def _cleanup_transport(self) -> None: + """Cleanup transport and receiver task.""" + if self._receiver_task: + self._receiver_task.cancel() + try: + await self._receiver_task + except asyncio.CancelledError: + pass + self._receiver_task = None + if self._transport: + await self._transport.disconnect() + self._transport = None async def _initialize(self) -> None: """Initialize the MCP connection.""" @@ -578,7 +643,7 @@ async def _initialize(self) -> None: ) # Store capabilities - capabilities = result.get("capabilities", {}) + _capabilities = result.get("capabilities", {}) if isinstance(result, dict) else {} # Fetch tools and resources await self._fetch_tools() @@ -655,14 +720,7 @@ async def read_resource(self, uri: str) -> dict[str, Any]: async def disconnect(self) -> None: """Disconnect from the MCP server.""" - if self._process: - self._process.terminate() - try: - await asyncio.wait_for(self._process.wait(), timeout=5) - except asyncio.TimeoutError: - self._process.kill() - self._process = None - + await self._cleanup_transport() self._state = MCPConnectionState.DISCONNECTED self._tools.clear() self._resources.clear() diff --git a/claude_code/services/memory_service.py b/claude_code/services/memory_service.py index 9047ad0..aad75bc 100644 --- a/claude_code/services/memory_service.py +++ b/claude_code/services/memory_service.py @@ -16,6 +16,16 @@ DEFAULT_STORAGE_DIR = Path.home() / ".claude" / "memory" +VALID_MEMORY_SCOPES: frozenset[str] = frozenset({"user", "project", "local"}) +DEFAULT_MEMORY_SCOPE = "project" + + +def _normalize_memory_scope(scope: str | None) -> str: + """Normalize and validate memory scope.""" + candidate = (scope or DEFAULT_MEMORY_SCOPE).strip().lower() + if candidate not in VALID_MEMORY_SCOPES: + raise ValueError(f"Invalid memory scope: {scope}") + return candidate @dataclass(slots=True) @@ -48,6 +58,9 @@ class SessionMemory: Provides persistent memory storage that survives across sessions. Useful for storing user preferences, project context, and learned information. + + This store is reserved for long-term structured knowledge and should not be + used as a duplicate sink for active session transcripts or history archives. """ def __init__(self, storage_dir: Path | str | None = None) -> None: @@ -75,6 +88,31 @@ def _get_file_path(self, namespace: str = "default") -> Path: safe_name = hashlib.md5(namespace.encode()).hexdigest()[:16] return self._storage_dir / f"{safe_name}.json" + @staticmethod + def _scope_namespace( + scope: str, + working_directory: Path | str | None = None, + ) -> str: + """Build deterministic namespace for a memory scope.""" + normalized_scope = _normalize_memory_scope(scope) + if normalized_scope == "user": + return "user" + + base_path = Path(working_directory).expanduser() if working_directory else Path.cwd() + resolved = str(base_path.resolve(strict=False)) + digest = hashlib.md5(resolved.encode("utf-8")).hexdigest()[:16] + return f"{normalized_scope}:{digest}" + + @classmethod + def _scoped_key( + cls, + scope: str, + key: str, + working_directory: Path | str | None = None, + ) -> str: + namespace = cls._scope_namespace(scope, working_directory=working_directory) + return f"{namespace}/{key}" + def _load(self) -> None: """Load memory from disk.""" try: @@ -311,6 +349,147 @@ async def import_data(self, data: dict[str, Any]) -> None: ) self._save() + async def set_scoped( + self, + scope: str, + key: str, + value: Any, + *, + working_directory: Path | str | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """Store value inside a scoped memory namespace.""" + normalized_scope = _normalize_memory_scope(scope) + namespaced_key = self._scoped_key( + normalized_scope, + key, + working_directory=working_directory, + ) + namespace = self._scope_namespace( + normalized_scope, + working_directory=working_directory, + ) + merged_tags = list(tags or []) + scope_tag = f"scope:{normalized_scope}" + if scope_tag not in merged_tags: + merged_tags.append(scope_tag) + merged_metadata = dict(metadata or {}) + merged_metadata.setdefault("scope", normalized_scope) + merged_metadata.setdefault("scope_namespace", namespace) + await self.set( + namespaced_key, + value, + tags=merged_tags, + metadata=merged_metadata, + ) + + async def get_scoped( + self, + scope: str, + key: str, + *, + working_directory: Path | str | None = None, + default: Any = None, + ) -> Any: + """Read value from a scoped memory namespace.""" + normalized_scope = _normalize_memory_scope(scope) + namespaced_key = self._scoped_key( + normalized_scope, + key, + working_directory=working_directory, + ) + return await self.get(namespaced_key, default) + + async def delete_scoped( + self, + scope: str, + key: str, + *, + working_directory: Path | str | None = None, + ) -> bool: + """Delete value from a scoped memory namespace.""" + normalized_scope = _normalize_memory_scope(scope) + namespaced_key = self._scoped_key( + normalized_scope, + key, + working_directory=working_directory, + ) + return await self.delete(namespaced_key) + + async def keys_scoped( + self, + scope: str, + *, + working_directory: Path | str | None = None, + ) -> list[str]: + """List keys inside a scoped memory namespace.""" + normalized_scope = _normalize_memory_scope(scope) + namespace = self._scope_namespace( + normalized_scope, + working_directory=working_directory, + ) + prefix = f"{namespace}/" + scoped_keys = await self.keys(pattern=f"{prefix}*") + return [key[len(prefix) :] for key in scoped_keys if key.startswith(prefix)] + + def snapshot_scoped( + self, + scope: str, + *, + working_directory: Path | str | None = None, + limit: int = 12, + ) -> dict[str, Any]: + """Build best-effort scoped snapshot for runtime context injection.""" + normalized_scope = _normalize_memory_scope(scope) + namespace = self._scope_namespace( + normalized_scope, + working_directory=working_directory, + ) + prefix = f"{namespace}/" + snapshot: dict[str, Any] = {} + for key, entry in self._memory.items(): + if not key.startswith(prefix): + continue + short_key = key[len(prefix) :] + snapshot[short_key] = entry.value + if len(snapshot) >= limit: + break + return snapshot + + def supported_scopes(self) -> tuple[str, ...]: + """Return supported memory scopes.""" + return tuple(sorted(VALID_MEMORY_SCOPES)) + + async def set_knowledge( + self, + key: str, + value: Any, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """Store long-term knowledge explicitly. + + Use this API when saving durable, structured memory. It marks entries + with a ``knowledge`` tag and metadata kind. + """ + merged_tags = list(tags or []) + if "knowledge" not in merged_tags: + merged_tags.append("knowledge") + + merged_metadata = dict(metadata or {}) + merged_metadata.setdefault("kind", "knowledge") + await self.set(key, value, tags=merged_tags, metadata=merged_metadata) + + async def list_knowledge_keys(self) -> list[str]: + """List keys marked as long-term knowledge.""" + async with self._lock: + return [ + key + for key, entry in self._memory.items() + if "knowledge" in entry.tags or entry.metadata.get("kind") == "knowledge" + ] + class MemoryNamespace: """A namespace within session memory for organizing related memories.""" @@ -409,4 +588,4 @@ def get_namespace(name: str) -> MemoryNamespace: Returns: A MemoryNamespace instance. """ - return MemoryNamespace(get_memory(), name) \ No newline at end of file + return MemoryNamespace(get_memory(), name) diff --git a/claude_code/services/multi_agent_supervisor.py b/claude_code/services/multi_agent_supervisor.py new file mode 100644 index 0000000..351615a --- /dev/null +++ b/claude_code/services/multi_agent_supervisor.py @@ -0,0 +1,624 @@ +"""Multi-agent supervisor runtime for DAG orchestration.""" + +from __future__ import annotations + +import asyncio +import json +import time +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Awaitable, Callable, Optional + +from claude_code.services.artifact_bus import ( + ArtifactBus, + ArtifactConflictError, + ArtifactSchemaError, +) + + +class SupervisorConfigError(ValueError): + """Raised when supervisor workflow configuration is invalid.""" + + +class NodeExecutionStatus(Enum): + """Execution status for one supervisor node.""" + + COMPLETED = "completed" + FAILED = "failed" + TIMEOUT = "timeout" + SKIPPED = "skipped" + + +@dataclass(frozen=True, slots=True) +class AgentNodeSpec: + """Specification for one agent node in a workflow DAG.""" + + node_id: str + description: str + prompt: str + subagent_type: str = "general-purpose" + model: Optional[str] = None + dependencies: tuple[str, ...] = () + ownership: tuple[str, ...] = () + timeout_seconds: Optional[float] = None + max_retries: int = 0 + + @classmethod + def from_dict(cls, payload: dict[str, Any], *, index: int) -> "AgentNodeSpec": + """Create node spec from workflow JSON payload.""" + if not isinstance(payload, dict): + raise SupervisorConfigError(f"workflow.nodes[{index}] must be an object") + + node_id = str(payload.get("id") or f"node-{index}") + prompt = str(payload.get("prompt") or "").strip() + if not prompt: + raise SupervisorConfigError(f"workflow.nodes[{index}] missing required prompt") + + description = str(payload.get("description") or node_id) + subagent_type = str(payload.get("subagent_type") or "general-purpose") + model_raw = payload.get("model") + model = str(model_raw) if isinstance(model_raw, str) and model_raw.strip() else None + + deps_raw = payload.get("depends_on", payload.get("dependencies", [])) + dependencies = _as_string_tuple(deps_raw, field_name=f"workflow.nodes[{index}].depends_on") + ownership = _as_string_tuple(payload.get("ownership", []), field_name=f"workflow.nodes[{index}].ownership") + + timeout_seconds = _parse_timeout( + payload.get("timeout_seconds"), + field_name=f"workflow.nodes[{index}].timeout_seconds", + ) + + max_retries = _parse_non_negative_int( + payload.get("max_retries", 0), + field_name=f"workflow.nodes[{index}].max_retries", + ) + + return cls( + node_id=node_id, + description=description, + prompt=prompt, + subagent_type=subagent_type, + model=model, + dependencies=dependencies, + ownership=ownership, + timeout_seconds=timeout_seconds, + max_retries=max_retries, + ) + + +@dataclass(frozen=True, slots=True) +class SupervisorBudget: + """Supervisor execution budget controls.""" + + max_parallelism: int = 2 + max_total_agents: Optional[int] = None + max_failures: Optional[int] = None + + @classmethod + def from_dict(cls, payload: Any) -> "SupervisorBudget": + """Build budget from optional JSON payload.""" + if payload is None: + return cls() + if not isinstance(payload, dict): + raise SupervisorConfigError("workflow.budget must be an object") + + max_parallelism = _parse_positive_int( + payload.get("max_parallelism", 2), + field_name="workflow.budget.max_parallelism", + ) + + max_total_agents = payload.get("max_total_agents") + if max_total_agents is not None: + max_total_agents = _parse_positive_int( + max_total_agents, + field_name="workflow.budget.max_total_agents", + ) + + max_failures = payload.get("max_failures") + if max_failures is not None: + max_failures = _parse_non_negative_int( + max_failures, + field_name="workflow.budget.max_failures", + ) + + return cls( + max_parallelism=max_parallelism, + max_total_agents=max_total_agents, + max_failures=max_failures, + ) + + +@dataclass(frozen=True, slots=True) +class NodeExecutionResult: + """Execution result for one node.""" + + node_id: str + description: str + status: NodeExecutionStatus + attempts: int + duration_ms: float + output: str = "" + error: Optional[str] = None + ownership: tuple[str, ...] = () + artifacts: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class SupervisorRunResult: + """Final workflow result.""" + + workflow_id: str + success: bool + duration_ms: float + results: tuple[NodeExecutionResult, ...] + artifacts_by_node: dict[str, dict[str, Any]] + + @property + def status(self) -> str: + """Return high-level workflow status.""" + return "completed" if self.success else "failed" + + +AgentNodeExecutor = Callable[ + [AgentNodeSpec, str, dict[str, dict[str, Any]]], + Awaitable[Any], +] + + +class MultiAgentSupervisor: + """DAG supervisor for spawning and aggregating sub-agent execution.""" + + def __init__( + self, + *, + executor: AgentNodeExecutor, + budget: Optional[SupervisorBudget] = None, + workflow_timeout_seconds: Optional[float] = None, + ) -> None: + self._executor = executor + self._budget = budget or SupervisorBudget() + self._workflow_timeout_seconds = workflow_timeout_seconds + + async def run( + self, + *, + workflow_id: str, + nodes: list[AgentNodeSpec], + ) -> SupervisorRunResult: + """Execute workflow DAG with parallel scheduling and retries.""" + if not nodes: + raise SupervisorConfigError("workflow.nodes must not be empty") + self._validate(nodes) + + start_time = time.perf_counter() + node_map = {node.node_id: node for node in nodes} + topo_order = _topological_sort(nodes) + dependents = _build_dependents(nodes) + remaining = { + node.node_id: set(node.dependencies) + for node in nodes + } + artifact_bus = ArtifactBus() + + ready: list[str] = [node_id for node_id in topo_order if not remaining[node_id]] + running: dict[str, asyncio.Task[NodeExecutionResult]] = {} + results: dict[str, NodeExecutionResult] = {} + artifacts_by_node: dict[str, dict[str, Any]] = {} + failures = 0 + + while ready or running: + workflow_timeout_triggered = False + if ( + self._workflow_timeout_seconds is not None + and (time.perf_counter() - start_time) >= self._workflow_timeout_seconds + ): + workflow_timeout_triggered = True + + while ready and len(running) < self._budget.max_parallelism and not workflow_timeout_triggered: + node_id = ready.pop(0) + node = node_map[node_id] + + if _has_non_success_dependency(node, results): + skip_result = _build_skipped_result( + node, + error="skipped due to failed dependency", + ) + results[node_id] = skip_result + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + for child_id in dependents[node_id]: + remaining[child_id].discard(node_id) + if not remaining[child_id]: + ready.append(child_id) + continue + + if self._is_budget_exhausted(failures): + skip_result = _build_skipped_result( + node, + error="skipped due to budget.max_failures", + ) + results[node_id] = skip_result + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + for child_id in dependents[node_id]: + remaining[child_id].discard(node_id) + if not remaining[child_id]: + ready.append(child_id) + continue + + dependency_artifacts = artifact_bus.build_dependency_view(node.dependencies) + running[node_id] = asyncio.create_task( + self._execute_node( + node, + dependency_artifacts, + artifact_bus=artifact_bus, + ) + ) + + if workflow_timeout_triggered: + for node_id, task in list(running.items()): + task.cancel() + results[node_id] = _build_timeout_result(node_map[node_id], "workflow timeout") + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + running.clear() + break + + if not running: + continue + + wait_timeout = None + if self._workflow_timeout_seconds is not None: + elapsed = time.perf_counter() - start_time + wait_timeout = max(self._workflow_timeout_seconds - elapsed, 0.0) + + done, _ = await asyncio.wait( + running.values(), + timeout=wait_timeout, + return_when=asyncio.FIRST_COMPLETED, + ) + + if not done: + # Workflow timeout while waiting. + for node_id, task in list(running.items()): + task.cancel() + results[node_id] = _build_timeout_result(node_map[node_id], "workflow timeout") + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + running.clear() + break + + finished_ids = [node_id for node_id, task in running.items() if task in done] + for node_id in finished_ids: + task = running.pop(node_id) + try: + node_result = task.result() + except asyncio.CancelledError: + node_result = _build_timeout_result(node_map[node_id], "node cancelled") + except Exception as exc: + node_result = _build_failed_result( + node_map[node_id], + error=f"node execution error: {exc}", + attempts=1, + ) + + results[node_id] = node_result + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + if node_result.status in (NodeExecutionStatus.FAILED, NodeExecutionStatus.TIMEOUT): + failures += 1 + + for child_id in dependents[node_id]: + remaining[child_id].discard(node_id) + if not remaining[child_id]: + ready.append(child_id) + + for node_id in topo_order: + if node_id in results: + continue + fallback = _build_skipped_result( + node_map[node_id], + error="skipped due to unresolved scheduling state", + ) + results[node_id] = fallback + artifacts_by_node[node_id] = artifact_bus.build_node_bundle(node_id) + + artifacts_by_node["__bus__"] = artifact_bus.snapshot() + + ordered_results = tuple(results[node_id] for node_id in topo_order) + success = all(item.status == NodeExecutionStatus.COMPLETED for item in ordered_results) + duration_ms = (time.perf_counter() - start_time) * 1000.0 + return SupervisorRunResult( + workflow_id=workflow_id, + success=success, + duration_ms=duration_ms, + results=ordered_results, + artifacts_by_node=artifacts_by_node, + ) + + def _validate(self, nodes: list[AgentNodeSpec]) -> None: + """Validate node graph and budget limits.""" + if self._budget.max_total_agents is not None and len(nodes) > self._budget.max_total_agents: + raise SupervisorConfigError( + f"workflow node count {len(nodes)} exceeds budget.max_total_agents={self._budget.max_total_agents}" + ) + + node_ids = [node.node_id for node in nodes] + seen: set[str] = set() + duplicates: set[str] = set() + for node_id in node_ids: + if node_id in seen: + duplicates.add(node_id) + seen.add(node_id) + if duplicates: + duplicate_list = ", ".join(sorted(duplicates)) + raise SupervisorConfigError(f"duplicate workflow node ids: {duplicate_list}") + + node_set = set(node_ids) + for node in nodes: + missing = [dep for dep in node.dependencies if dep not in node_set] + if missing: + missing_list = ", ".join(missing) + raise SupervisorConfigError( + f"workflow node '{node.node_id}' depends on unknown nodes: {missing_list}" + ) + + _topological_sort(nodes) + + def _is_budget_exhausted(self, failures: int) -> bool: + if self._budget.max_failures is None: + return False + return failures > self._budget.max_failures + + async def _execute_node( + self, + node: AgentNodeSpec, + dependency_artifacts: dict[str, dict[str, Any]], + *, + artifact_bus: ArtifactBus, + ) -> NodeExecutionResult: + """Execute one node with retry and timeout policy.""" + attempts = 0 + start_time = time.perf_counter() + last_error: Optional[str] = None + last_status = NodeExecutionStatus.FAILED + + while attempts <= node.max_retries: + attempts += 1 + prompt = self._build_node_prompt(node, dependency_artifacts) + try: + if node.timeout_seconds is not None: + output = await asyncio.wait_for( + self._executor(node, prompt, dependency_artifacts), + timeout=node.timeout_seconds, + ) + else: + output = await self._executor(node, prompt, dependency_artifacts) + + artifact_records = artifact_bus.publish_from_output( + node_id=node.node_id, + output=output, + ownership=node.ownership, + ) + output_text = str(output or "") + duration_ms = (time.perf_counter() - start_time) * 1000.0 + artifacts = { + "output": output_text, + "status": NodeExecutionStatus.COMPLETED.value, + "owner": list(node.ownership), + "artifact_count": len(artifact_records), + "artifacts": [record.to_dict() for record in artifact_records], + } + return NodeExecutionResult( + node_id=node.node_id, + description=node.description, + status=NodeExecutionStatus.COMPLETED, + attempts=attempts, + duration_ms=duration_ms, + output=output_text, + ownership=node.ownership, + artifacts=artifacts, + ) + except asyncio.TimeoutError: + last_status = NodeExecutionStatus.TIMEOUT + last_error = f"node timeout after {node.timeout_seconds}s" + except (ArtifactSchemaError, ArtifactConflictError) as exc: + last_status = NodeExecutionStatus.FAILED + last_error = f"artifact bus error: {exc}" + except Exception as exc: + last_status = NodeExecutionStatus.FAILED + last_error = str(exc) or exc.__class__.__name__ + + duration_ms = (time.perf_counter() - start_time) * 1000.0 + artifacts = { + "output": "", + "status": last_status.value, + "error": last_error or "unknown node failure", + "owner": list(node.ownership), + } + return NodeExecutionResult( + node_id=node.node_id, + description=node.description, + status=last_status, + attempts=attempts, + duration_ms=duration_ms, + output="", + error=last_error or "unknown node failure", + ownership=node.ownership, + artifacts=artifacts, + ) + + def _build_node_prompt( + self, + node: AgentNodeSpec, + dependency_artifacts: dict[str, dict[str, Any]], + ) -> str: + """Build node prompt with ownership and inherited artifacts.""" + parts = [node.prompt] + if node.ownership: + ownership_line = ", ".join(node.ownership) + parts.append( + "Ownership scope for this node: " + f"{ownership_line}. Do not revert edits made by other workers." + ) + if dependency_artifacts: + payload = json.dumps(dependency_artifacts, ensure_ascii=True, indent=2, sort_keys=True) + parts.append(f"Artifacts from dependencies:\n{payload}") + return "\n\n".join(parts) + + +def _has_non_success_dependency( + node: AgentNodeSpec, + results: dict[str, NodeExecutionResult], +) -> bool: + for dep in node.dependencies: + dep_result = results.get(dep) + if dep_result is None: + return False + if dep_result.status != NodeExecutionStatus.COMPLETED: + return True + return False + + +def _build_dependents(nodes: list[AgentNodeSpec]) -> dict[str, list[str]]: + dependents: dict[str, list[str]] = {node.node_id: [] for node in nodes} + for node in nodes: + for dep in node.dependencies: + dependents[dep].append(node.node_id) + return dependents + + +def _topological_sort(nodes: list[AgentNodeSpec]) -> list[str]: + """Return topological order or raise if cycle exists.""" + node_ids = [node.node_id for node in nodes] + indegree = {node.node_id: len(node.dependencies) for node in nodes} + dependents = _build_dependents(nodes) + + ready = [node_id for node_id in node_ids if indegree[node_id] == 0] + ordered: list[str] = [] + while ready: + node_id = ready.pop(0) + ordered.append(node_id) + for child_id in dependents[node_id]: + indegree[child_id] -= 1 + if indegree[child_id] == 0: + ready.append(child_id) + + if len(ordered) != len(nodes): + unresolved = sorted({node.node_id for node in nodes} - set(ordered)) + raise SupervisorConfigError( + "workflow graph contains a cycle: " + ", ".join(unresolved) + ) + return ordered + + +def _build_skipped_result(node: AgentNodeSpec, *, error: str) -> NodeExecutionResult: + return NodeExecutionResult( + node_id=node.node_id, + description=node.description, + status=NodeExecutionStatus.SKIPPED, + attempts=0, + duration_ms=0.0, + output="", + error=error, + ownership=node.ownership, + artifacts={ + "output": "", + "status": NodeExecutionStatus.SKIPPED.value, + "error": error, + "owner": list(node.ownership), + }, + ) + + +def _build_timeout_result(node: AgentNodeSpec, error: str) -> NodeExecutionResult: + return NodeExecutionResult( + node_id=node.node_id, + description=node.description, + status=NodeExecutionStatus.TIMEOUT, + attempts=1, + duration_ms=0.0, + output="", + error=error, + ownership=node.ownership, + artifacts={ + "output": "", + "status": NodeExecutionStatus.TIMEOUT.value, + "error": error, + "owner": list(node.ownership), + }, + ) + + +def _build_failed_result(node: AgentNodeSpec, *, error: str, attempts: int) -> NodeExecutionResult: + return NodeExecutionResult( + node_id=node.node_id, + description=node.description, + status=NodeExecutionStatus.FAILED, + attempts=attempts, + duration_ms=0.0, + output="", + error=error, + ownership=node.ownership, + artifacts={ + "output": "", + "status": NodeExecutionStatus.FAILED.value, + "error": error, + "owner": list(node.ownership), + }, + ) + + +def _as_string_tuple(raw: Any, *, field_name: str) -> tuple[str, ...]: + if raw is None: + return () + if isinstance(raw, str): + value = raw.strip() + return (value,) if value else () + if not isinstance(raw, list): + raise SupervisorConfigError(f"{field_name} must be a string or list of strings") + + values: list[str] = [] + for item in raw: + text = str(item).strip() + if text: + values.append(text) + return tuple(values) + + +def _parse_positive_int(value: Any, *, field_name: str) -> int: + try: + parsed = int(value) + except Exception as exc: # pragma: no cover - defensive parse guard + raise SupervisorConfigError(f"{field_name} must be an integer") from exc + if parsed <= 0: + raise SupervisorConfigError(f"{field_name} must be > 0") + return parsed + + +def _parse_non_negative_int(value: Any, *, field_name: str) -> int: + try: + parsed = int(value) + except Exception as exc: # pragma: no cover - defensive parse guard + raise SupervisorConfigError(f"{field_name} must be an integer") from exc + if parsed < 0: + raise SupervisorConfigError(f"{field_name} must be >= 0") + return parsed + + +def _parse_timeout(value: Any, *, field_name: str) -> Optional[float]: + if value is None: + return None + try: + parsed = float(value) + except Exception as exc: # pragma: no cover - defensive parse guard + raise SupervisorConfigError(f"{field_name} must be a number") from exc + if parsed <= 0: + raise SupervisorConfigError(f"{field_name} must be > 0") + return parsed + + +__all__ = [ + "SupervisorConfigError", + "NodeExecutionStatus", + "AgentNodeSpec", + "SupervisorBudget", + "NodeExecutionResult", + "SupervisorRunResult", + "MultiAgentSupervisor", +] diff --git a/claude_code/services/org_policy_audit.py b/claude_code/services/org_policy_audit.py new file mode 100644 index 0000000..afe3028 --- /dev/null +++ b/claude_code/services/org_policy_audit.py @@ -0,0 +1,686 @@ +"""Organization policy + audit service for workflow card P2-05.""" + +from __future__ import annotations + +import fnmatch +import hashlib +import json +import time +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Callable, Optional + + +class OrgPolicyAuditError(RuntimeError): + """Raised when org policy/audit workflow fails with structured metadata.""" + + def __init__( + self, + message: str, + *, + code: str = "org_policy_audit_error", + status_code: int = 500, + details: Any = None, + ) -> None: + super().__init__(message) + self.message = message + self.code = code + self.status_code = status_code + self.details = details + + +class PolicyEffect(Enum): + """Supported policy effects.""" + + ALLOW = "allow" + DENY = "deny" + REQUIRE_APPROVAL = "require_approval" + + +class ApprovalStatus(Enum): + """Lifecycle for approval queue items.""" + + PENDING = "pending" + APPROVED = "approved" + REJECTED = "rejected" + CANCELLED = "cancelled" + + +@dataclass(frozen=True, slots=True) +class OrgPolicyRule: + """One org-level policy rule.""" + + rule_id: str + effect: PolicyEffect + tool_pattern: str = "*" + operation_pattern: str = "*" + priority: int = 100 + enabled: bool = True + reason: str = "" + metadata: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls, payload: dict[str, Any], *, index: int) -> "OrgPolicyRule": + """Build validated rule from request payload.""" + if not isinstance(payload, dict): + raise OrgPolicyAuditError( + f"policies[{index}] must be an object", + code="validation_error", + status_code=400, + ) + + effect_raw = str(payload.get("effect") or "").strip().lower() + try: + effect = PolicyEffect(effect_raw) + except ValueError as exc: + supported = ", ".join(item.value for item in PolicyEffect) + raise OrgPolicyAuditError( + f"policies[{index}].effect must be one of: {supported}", + code="validation_error", + status_code=400, + ) from exc + + priority_raw = payload.get("priority", 100) + try: + priority = int(priority_raw) + except Exception as exc: + raise OrgPolicyAuditError( + f"policies[{index}].priority must be an integer", + code="validation_error", + status_code=400, + ) from exc + + metadata_raw = payload.get("metadata", {}) + if metadata_raw is None: + metadata = {} + elif isinstance(metadata_raw, dict): + metadata = dict(metadata_raw) + else: + raise OrgPolicyAuditError( + f"policies[{index}].metadata must be an object", + code="validation_error", + status_code=400, + ) + + rule_id = str(payload.get("id") or f"policy-{index}") + return cls( + rule_id=rule_id, + effect=effect, + tool_pattern=str(payload.get("tool_pattern") or payload.get("tool") or "*"), + operation_pattern=str(payload.get("operation_pattern") or payload.get("operation") or "*"), + priority=priority, + enabled=bool(payload.get("enabled", True)), + reason=str(payload.get("reason") or ""), + metadata=metadata, + ) + + def matches(self, *, tool_name: str, operation: str) -> bool: + """Return whether this rule applies to the request.""" + if not self.enabled: + return False + return fnmatch.fnmatch(tool_name, self.tool_pattern) and fnmatch.fnmatch( + operation, + self.operation_pattern, + ) + + def to_dict(self) -> dict[str, Any]: + """Serialize rule for API response.""" + return { + "id": self.rule_id, + "effect": self.effect.value, + "tool_pattern": self.tool_pattern, + "operation_pattern": self.operation_pattern, + "priority": self.priority, + "enabled": self.enabled, + "reason": self.reason, + "metadata": dict(self.metadata), + } + + +@dataclass(slots=True) +class ApprovalRequest: + """Approval queue item.""" + + approval_id: str + rule_id: str + tool_name: str + operation: str + payload_hash: str + payload_redacted: Any + status: ApprovalStatus = ApprovalStatus.PENDING + created_at: float = field(default_factory=time.time) + updated_at: float = field(default_factory=time.time) + decided_by: str | None = None + reason: str | None = None + + def to_dict(self) -> dict[str, Any]: + """Serialize approval request.""" + return { + "approval_id": self.approval_id, + "rule_id": self.rule_id, + "tool_name": self.tool_name, + "operation": self.operation, + "payload_hash": self.payload_hash, + "payload_redacted": self.payload_redacted, + "status": self.status.value, + "created_at": self.created_at, + "updated_at": self.updated_at, + "decided_by": self.decided_by, + "reason": self.reason, + } + + +@dataclass(frozen=True, slots=True) +class AuditEvent: + """Policy audit record.""" + + event_id: str + event_type: str + created_at: float + tool_name: str | None + operation: str | None + decision: str | None + rule_id: str | None + approval_id: str | None + details: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + """Serialize audit event.""" + return { + "event_id": self.event_id, + "event_type": self.event_type, + "created_at": self.created_at, + "tool_name": self.tool_name, + "operation": self.operation, + "decision": self.decision, + "rule_id": self.rule_id, + "approval_id": self.approval_id, + "details": dict(self.details), + } + + +_SENSITIVE_TOKENS: tuple[str, ...] = ( + "token", + "secret", + "password", + "passwd", + "authorization", + "cookie", + "api_key", + "apikey", + "access_key", + "private_key", +) + +_SENSITIVE_PREFIXES: tuple[str, ...] = ( + "sk-", + "ghp_", + "github_pat_", + "xoxb-", + "xoxp-", +) + + +class OrgPolicyAuditService: + """P2-05 runtime service: policy matcher + approval queue + audit reporting.""" + + def __init__( + self, + *, + append_event: Optional[Callable[..., Any]] = None, + ) -> None: + self._rules: list[OrgPolicyRule] = [] + self._approvals: dict[str, ApprovalRequest] = {} + self._approvals_by_fingerprint: dict[str, str] = {} + self._audit_events: list[AuditEvent] = [] + self._append_event = append_event + self._approval_counter = 0 + self._audit_counter = 0 + + def configure_policies( + self, + *, + policies: list[dict[str, Any]], + replace: bool = True, + ) -> dict[str, Any]: + """Register policy set for org-level execution control.""" + parsed = [ + OrgPolicyRule.from_dict(item, index=index) + for index, item in enumerate(policies) + ] + + if replace: + self._rules = parsed + else: + self._rules.extend(parsed) + + self._write_audit( + event_type="org_policy.rules.configured", + details={ + "replace": replace, + "rule_count": len(self._rules), + }, + ) + return { + "rule_count": len(self._rules), + "rules": [rule.to_dict() for rule in self._rules], + } + + def evaluate( + self, + *, + tool_name: str, + operation: str = "execute", + payload: Any = None, + actor: str | None = None, + context: Optional[dict[str, Any]] = None, + create_approval: bool = True, + ) -> dict[str, Any]: + """Evaluate one execution request against org policy.""" + if not tool_name.strip(): + raise OrgPolicyAuditError( + "tool_name is required", + code="validation_error", + status_code=400, + ) + + clean_tool_name = tool_name.strip() + clean_operation = operation.strip() or "execute" + payload_redacted = self.redact_secrets(payload) + payload_hash = _stable_hash(payload) + + matched = [ + rule + for rule in self._rules + if rule.matches(tool_name=clean_tool_name, operation=clean_operation) + ] + matched.sort(key=lambda item: (item.priority, item.rule_id)) + + rule, conflict = self._select_effective_rule(matched) + if conflict is not None: + self._write_audit( + event_type="org_policy.conflict.detected", + tool_name=clean_tool_name, + operation=clean_operation, + decision="blocked", + rule_id=None, + details=conflict, + ) + raise OrgPolicyAuditError( + "conflicting policies detected at same priority", + code="policy_conflict", + status_code=409, + details=conflict, + ) + + approval = None + decision = "allowed" + allowed = True + + if rule.effect == PolicyEffect.DENY: + decision = "denied" + allowed = False + elif rule.effect == PolicyEffect.REQUIRE_APPROVAL: + fingerprint = self._approval_fingerprint( + rule_id=rule.rule_id, + tool_name=clean_tool_name, + operation=clean_operation, + payload_hash=payload_hash, + ) + approval = self._get_approval_by_fingerprint(fingerprint) + if approval is None and create_approval: + approval = self._create_approval( + rule_id=rule.rule_id, + tool_name=clean_tool_name, + operation=clean_operation, + payload_hash=payload_hash, + payload_redacted=payload_redacted, + fingerprint=fingerprint, + ) + decision = "approval_pending" + allowed = False + elif approval is None: + decision = "approval_required" + allowed = False + elif approval.status == ApprovalStatus.APPROVED: + decision = "allowed_by_approval" + allowed = True + elif approval.status in (ApprovalStatus.REJECTED, ApprovalStatus.CANCELLED): + decision = "denied_by_approval" + allowed = False + else: + decision = "approval_pending" + allowed = False + + details = { + "actor": actor, + "context": dict(context or {}), + "payload_redacted": payload_redacted, + "matched_rule_count": len(matched), + } + audit = self._write_audit( + event_type="org_policy.evaluated", + tool_name=clean_tool_name, + operation=clean_operation, + decision=decision, + rule_id=rule.rule_id, + approval_id=approval.approval_id if approval else None, + details=details, + ) + + result = { + "allowed": allowed, + "decision": decision, + "tool_name": clean_tool_name, + "operation": clean_operation, + "rule": rule.to_dict(), + "payload_redacted": payload_redacted, + "audit_event_id": audit.event_id, + } + if approval is not None: + result["approval"] = approval.to_dict() + return result + + def decide_approval( + self, + *, + approval_id: str, + decision: str, + decided_by: str | None = None, + reason: str | None = None, + ) -> dict[str, Any]: + """Apply transition to approval queue item.""" + approval = self._approvals.get(approval_id) + if approval is None: + raise OrgPolicyAuditError( + f"approval not found: {approval_id}", + code="approval_not_found", + status_code=404, + ) + + decision_normalized = decision.strip().lower() + if decision_normalized not in {"approve", "reject", "cancel"}: + raise OrgPolicyAuditError( + f"unsupported approval decision: {decision}", + code="validation_error", + status_code=400, + ) + + if approval.status != ApprovalStatus.PENDING: + raise OrgPolicyAuditError( + f"approval transition is not allowed from {approval.status.value}", + code="invalid_approval_transition", + status_code=409, + details={ + "approval_id": approval.approval_id, + "status": approval.status.value, + }, + ) + + if decision_normalized == "approve": + new_status = ApprovalStatus.APPROVED + elif decision_normalized == "reject": + new_status = ApprovalStatus.REJECTED + else: + new_status = ApprovalStatus.CANCELLED + + approval.status = new_status + approval.decided_by = decided_by + approval.reason = reason + approval.updated_at = time.time() + + self._write_audit( + event_type="org_policy.approval.decided", + tool_name=approval.tool_name, + operation=approval.operation, + decision=new_status.value, + rule_id=approval.rule_id, + approval_id=approval.approval_id, + details={ + "decided_by": decided_by, + "reason": reason, + }, + ) + return approval.to_dict() + + def list_approvals( + self, + *, + status: str | None = None, + limit: int = 100, + ) -> list[dict[str, Any]]: + """List approval queue with optional status filter.""" + approvals = list(self._approvals.values()) + if status: + normalized = status.strip().lower() + approvals = [item for item in approvals if item.status.value == normalized] + approvals.sort(key=lambda item: item.created_at, reverse=True) + return [item.to_dict() for item in approvals[:limit]] + + def list_audit_events( + self, + *, + event_type: str | None = None, + tool_name: str | None = None, + limit: int = 200, + ) -> list[dict[str, Any]]: + """List audit events with basic filters.""" + events = list(self._audit_events) + if event_type: + normalized = event_type.strip() + events = [item for item in events if item.event_type == normalized] + if tool_name: + normalized_tool = tool_name.strip() + events = [item for item in events if item.tool_name == normalized_tool] + events.sort(key=lambda item: item.created_at, reverse=True) + return [item.to_dict() for item in events[:limit]] + + def build_report(self, *, limit: int = 200) -> dict[str, Any]: + """Build compact audit summary for org reporting.""" + latest = self.list_audit_events(limit=limit) + decision_count: dict[str, int] = {} + for item in latest: + decision = str(item.get("decision") or "none") + decision_count[decision] = decision_count.get(decision, 0) + 1 + pending = len([item for item in self._approvals.values() if item.status == ApprovalStatus.PENDING]) + return { + "summary": { + "rule_count": len(self._rules), + "approval_total": len(self._approvals), + "approval_pending": pending, + "audit_total": len(self._audit_events), + }, + "decision_counts": decision_count, + "events": latest, + } + + def redact_secrets(self, value: Any) -> Any: + """Redact sensitive fields recursively.""" + return _redact_value(value) + + def _select_effective_rule( + self, + matched_rules: list[OrgPolicyRule], + ) -> tuple[OrgPolicyRule, dict[str, Any] | None]: + if not matched_rules: + return ( + OrgPolicyRule( + rule_id="__default_allow__", + effect=PolicyEffect.ALLOW, + tool_pattern="*", + operation_pattern="*", + priority=99999, + enabled=True, + reason="no policy matched", + ), + None, + ) + + top_priority = matched_rules[0].priority + top_rules = [item for item in matched_rules if item.priority == top_priority] + effects = sorted({item.effect.value for item in top_rules}) + if len(effects) <= 1: + return top_rules[0], None + + return ( + top_rules[0], + { + "priority": top_priority, + "rules": [item.to_dict() for item in top_rules], + "effects": effects, + }, + ) + + def _create_approval( + self, + *, + rule_id: str, + tool_name: str, + operation: str, + payload_hash: str, + payload_redacted: Any, + fingerprint: str, + ) -> ApprovalRequest: + self._approval_counter += 1 + approval_id = f"apr-{int(time.time() * 1000)}-{self._approval_counter}" + approval = ApprovalRequest( + approval_id=approval_id, + rule_id=rule_id, + tool_name=tool_name, + operation=operation, + payload_hash=payload_hash, + payload_redacted=payload_redacted, + ) + self._approvals[approval_id] = approval + self._approvals_by_fingerprint[fingerprint] = approval_id + + self._write_audit( + event_type="org_policy.approval.created", + tool_name=tool_name, + operation=operation, + decision=ApprovalStatus.PENDING.value, + rule_id=rule_id, + approval_id=approval_id, + details={"payload_redacted": payload_redacted}, + ) + return approval + + def _approval_fingerprint( + self, + *, + rule_id: str, + tool_name: str, + operation: str, + payload_hash: str, + ) -> str: + return "|".join([rule_id, tool_name, operation, payload_hash]) + + def _get_approval_by_fingerprint(self, fingerprint: str) -> ApprovalRequest | None: + approval_id = self._approvals_by_fingerprint.get(fingerprint) + if approval_id is None: + return None + return self._approvals.get(approval_id) + + def _write_audit( + self, + *, + event_type: str, + tool_name: str | None = None, + operation: str | None = None, + decision: str | None = None, + rule_id: str | None = None, + approval_id: str | None = None, + details: Optional[dict[str, Any]] = None, + ) -> AuditEvent: + self._audit_counter += 1 + event = AuditEvent( + event_id=f"audit-{int(time.time() * 1000)}-{self._audit_counter}", + event_type=event_type, + created_at=time.time(), + tool_name=tool_name, + operation=operation, + decision=decision, + rule_id=rule_id, + approval_id=approval_id, + details=dict(details or {}), + ) + self._audit_events.append(event) + append_event = self._append_event + if callable(append_event): + try: + append_event( + event_type=event.event_type, + payload={ + "tool_name": event.tool_name, + "operation": event.operation, + "decision": event.decision, + "rule_id": event.rule_id, + "approval_id": event.approval_id, + "details": event.details, + }, + source="org_policy_audit_service", + ) + except Exception: + pass + return event + + +def _stable_hash(payload: Any) -> str: + try: + encoded = json.dumps(payload, sort_keys=True, ensure_ascii=True, separators=(",", ":")) + except TypeError: + encoded = str(payload) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest() + + +def _redact_value(value: Any) -> Any: + if isinstance(value, dict): + redacted: dict[str, Any] = {} + for raw_key, raw_item in value.items(): + key = str(raw_key) + if _is_sensitive_key(key): + redacted[key] = "***REDACTED***" + continue + redacted[key] = _redact_value(raw_item) + return redacted + + if isinstance(value, list): + return [_redact_value(item) for item in value] + + if isinstance(value, tuple): + return [_redact_value(item) for item in value] + + if isinstance(value, str) and _looks_like_secret(value): + return "***REDACTED***" + + return value + + +def _is_sensitive_key(key: str) -> bool: + lowered = key.strip().lower() + if not lowered: + return False + return any(token in lowered for token in _SENSITIVE_TOKENS) + + +def _looks_like_secret(value: str) -> bool: + text = value.strip() + if not text: + return False + lowered = text.lower() + if any(lowered.startswith(prefix) for prefix in _SENSITIVE_PREFIXES): + return True + if len(text) >= 40 and any(ch.isdigit() for ch in text) and any(ch.isalpha() for ch in text): + return True + return False + + +__all__ = [ + "OrgPolicyAuditError", + "PolicyEffect", + "ApprovalStatus", + "OrgPolicyRule", + "ApprovalRequest", + "AuditEvent", + "OrgPolicyAuditService", +] diff --git a/claude_code/services/review_service.py b/claude_code/services/review_service.py new file mode 100644 index 0000000..95bd732 --- /dev/null +++ b/claude_code/services/review_service.py @@ -0,0 +1,308 @@ +"""Review service for command-level diff and file risk inspection.""" + +from __future__ import annotations + +import re +import subprocess +from dataclasses import dataclass, field +from pathlib import Path + + +_HUNK_PATTERN = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") +_SHELL_TRUE_PATTERN = re.compile(r"subprocess\.(?:run|Popen|call|check_call|check_output)\(.*shell\s*=\s*True") +_TODO_PATTERN = re.compile(r"#\s*(?:TODO|FIXME)\b", re.IGNORECASE) +_HARD_CODED_SECRET_PATTERN = re.compile( + r"\b(?:api_key|token|password|secret)\b\s*=\s*['\"][^'\"]+['\"]", + re.IGNORECASE, +) +_BARE_EXCEPT_PATTERN = re.compile(r"^\s*except\s*:\s*$") +_BROAD_EXCEPT_PATTERN = re.compile(r"^\s*except\s+Exception\b") +_DEBUG_PRINT_PATTERN = re.compile(r"\bprint\(") + + +@dataclass(frozen=True, slots=True) +class ReviewFinding: + """A structured finding produced by static review rules.""" + + file_path: str + line: int + severity: str + issue: str + recommendation: str + + +@dataclass(frozen=True, slots=True) +class ReviewResult: + """Review result for a specific scope.""" + + scope: str + files_reviewed: list[str] + findings: list[ReviewFinding] + warnings: list[str] = field(default_factory=list) + + +@dataclass(frozen=True, slots=True) +class _ChangedFile: + path: str + full_scan: bool = False + + +class ReviewServiceError(RuntimeError): + """Raised when review cannot be executed for the requested scope.""" + + +class ReviewService: + """Review service with lightweight risk heuristics.""" + + def review_git_changes(self, working_directory: str) -> ReviewResult: + """Review current git working tree changes.""" + changed_files = self._collect_changed_files(working_directory) + if not changed_files: + return ReviewResult(scope="git_diff", files_reviewed=[], findings=[], warnings=[]) + + findings: list[ReviewFinding] = [] + warnings: list[str] = [] + files_reviewed: list[str] = [] + + for changed in changed_files: + file_path = Path(working_directory) / changed.path + if not file_path.exists(): + warnings.append(f"Skipped missing file: {changed.path}") + continue + + line_filter = None + if not changed.full_scan: + line_filter = self._collect_changed_line_numbers(working_directory, changed.path) + if not line_filter: + continue + + files_reviewed.append(changed.path) + findings.extend(self._analyze_file(file_path, changed.path, line_filter)) + + return ReviewResult( + scope="git_diff", + files_reviewed=files_reviewed, + findings=self._sort_findings(findings), + warnings=warnings, + ) + + def review_files(self, working_directory: str, file_paths: list[str]) -> ReviewResult: + """Review specific files provided by user input.""" + findings: list[ReviewFinding] = [] + warnings: list[str] = [] + files_reviewed: list[str] = [] + + for file_path_text in file_paths: + normalized = file_path_text.strip() + if not normalized: + continue + absolute_path = Path(working_directory) / normalized + if not absolute_path.exists(): + raise ReviewServiceError(f"File not found: {normalized}") + if absolute_path.is_dir(): + raise ReviewServiceError(f"Expected file but got directory: {normalized}") + + files_reviewed.append(normalized) + findings.extend(self._analyze_file(absolute_path, normalized, None)) + + return ReviewResult( + scope="files", + files_reviewed=files_reviewed, + findings=self._sort_findings(findings), + warnings=warnings, + ) + + def _collect_changed_files(self, working_directory: str) -> list[_ChangedFile]: + """Collect changed files from git status porcelain output.""" + result = self._run_git( + ["git", "status", "--porcelain=v1", "-uall"], + working_directory, + ) + if result.returncode != 0: + error = result.stderr.strip() or "Unknown git status error" + raise ReviewServiceError(f"Git review unavailable: {error}") + + changed_files: list[_ChangedFile] = [] + seen: set[str] = set() + for raw_line in result.stdout.splitlines(): + line = raw_line.rstrip() + if len(line) < 4: + continue + status = line[:2] + path_part = self._normalize_path_token(line[3:]) + if " -> " in path_part: + path_part = path_part.split(" -> ", 1)[1].strip() + + if not path_part or path_part in seen: + continue + + # Deletions cannot be scanned from workspace files. + if "D" in status: + continue + + changed_files.append(_ChangedFile(path=path_part, full_scan=status == "??")) + seen.add(path_part) + + return changed_files + + def _collect_changed_line_numbers(self, working_directory: str, file_path: str) -> set[int]: + """Collect added/modified line numbers from staged and unstaged diffs.""" + line_numbers: set[int] = set() + for cmd in ( + ["git", "diff", "-U0", "--", file_path], + ["git", "diff", "--cached", "-U0", "--", file_path], + ): + result = self._run_git(cmd, working_directory) + if result.returncode != 0: + continue + for line in result.stdout.splitlines(): + match = _HUNK_PATTERN.match(line) + if not match: + continue + start = int(match.group(1)) + count_group = match.group(2) + count = int(count_group) if count_group is not None else 1 + for line_number in range(start, start + count): + line_numbers.add(line_number) + return line_numbers + + def _analyze_file( + self, + absolute_path: Path, + display_path: str, + line_filter: set[int] | None, + ) -> list[ReviewFinding]: + """Analyze a file and return findings.""" + try: + text = absolute_path.read_text(encoding="utf-8") + except UnicodeDecodeError: + text = absolute_path.read_text(encoding="utf-8", errors="ignore") + except Exception as exc: + return [ + ReviewFinding( + file_path=display_path, + line=1, + severity="medium", + issue=f"Unable to read file for review: {exc}", + recommendation="Check file permissions and encoding.", + ) + ] + + findings: list[ReviewFinding] = [] + for index, line in enumerate(text.splitlines(), start=1): + if line_filter is not None and index not in line_filter: + continue + findings.extend(self._analyze_line(display_path, index, line)) + return findings + + def _analyze_line(self, file_path: str, line_number: int, line: str) -> list[ReviewFinding]: + """Apply heuristic checks to a single line.""" + findings: list[ReviewFinding] = [] + stripped = line.strip() + + if _SHELL_TRUE_PATTERN.search(line): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="high", + issue="subprocess call uses shell=True, which can lead to command injection.", + recommendation="Use argument lists and keep shell=False unless strictly required.", + ) + ) + + if _HARD_CODED_SECRET_PATTERN.search(line): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="high", + issue="Potential hard-coded secret detected.", + recommendation="Load secrets from environment/config vault instead of source code.", + ) + ) + + if _BARE_EXCEPT_PATTERN.match(stripped): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="high", + issue="Bare except swallows all exceptions.", + recommendation="Catch specific exception types and log failure context.", + ) + ) + elif _BROAD_EXCEPT_PATTERN.match(stripped): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="medium", + issue="Broad `except Exception` may hide actionable failures.", + recommendation="Handle expected exceptions explicitly and fail closed for unknown errors.", + ) + ) + + if _TODO_PATTERN.search(line): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="low", + issue="TODO/FIXME marker in reviewed code path.", + recommendation="Resolve or track it with a linked task before merge.", + ) + ) + + if _DEBUG_PRINT_PATTERN.search(line) and not stripped.startswith("#"): + findings.append( + ReviewFinding( + file_path=file_path, + line=line_number, + severity="low", + issue="Debug print statement detected.", + recommendation="Use structured logging or remove debug output before shipping.", + ) + ) + + return findings + + @staticmethod + def _sort_findings(findings: list[ReviewFinding]) -> list[ReviewFinding]: + """Sort findings by severity and location.""" + severity_order = {"high": 0, "medium": 1, "low": 2} + return sorted( + findings, + key=lambda item: ( + severity_order.get(item.severity, 99), + item.file_path, + item.line, + item.issue, + ), + ) + + @staticmethod + def _normalize_path_token(path_token: str) -> str: + """Normalize git porcelain path token.""" + candidate = path_token.strip() + if len(candidate) >= 2 and candidate.startswith('"') and candidate.endswith('"'): + return candidate[1:-1].encode("utf-8").decode("unicode_escape") + return candidate + + @staticmethod + def _run_git(cmd: list[str], working_directory: str) -> subprocess.CompletedProcess[str]: + """Run git command with shared subprocess defaults.""" + return subprocess.run( + cmd, + cwd=working_directory, + capture_output=True, + text=True, + ) + + +__all__ = [ + "ReviewFinding", + "ReviewResult", + "ReviewService", + "ReviewServiceError", +] diff --git a/claude_code/services/shutdown.py b/claude_code/services/shutdown.py index a225792..c214903 100644 --- a/claude_code/services/shutdown.py +++ b/claude_code/services/shutdown.py @@ -111,48 +111,73 @@ async def shutdown(self, reason: str = "unknown") -> None: logger.info(f"Shutting down gracefully: {reason}") self._phase = ShutdownPhase.DRAINING - - # Call shutdown callback - if self.config.on_shutdown: - if asyncio.iscoroutinefunction(self.config.on_shutdown): - await self.config.on_shutdown() - else: - self.config.on_shutdown() - - # Wait for running tasks with timeout - if self._running_tasks: - try: - await asyncio.wait_for( - asyncio.gather(*self._running_tasks, return_exceptions=True), - timeout=self.config.timeout, - ) - except asyncio.TimeoutError: - logger.warning("Some tasks did not complete in time") - - self._phase = ShutdownPhase.STOPPING - - # Run cleanup callbacks - for name, cleanup in self._cleanups.items(): - try: - if asyncio.iscoroutinefunction(cleanup): - await cleanup() + if self.config.force_after > 0 and self._force_task is None: + self._force_task = asyncio.create_task(self._force_shutdown_after_timeout()) + + try: + # Call shutdown callback + if self.config.on_shutdown: + if asyncio.iscoroutinefunction(self.config.on_shutdown): + await self.config.on_shutdown() + else: + self.config.on_shutdown() + + # Wait for running tasks with timeout + if self._running_tasks: + try: + await asyncio.wait_for( + asyncio.gather(*self._running_tasks, return_exceptions=True), + timeout=self.config.timeout, + ) + except asyncio.TimeoutError: + logger.warning("Some tasks did not complete in time") + + self._phase = ShutdownPhase.STOPPING + + # Run cleanup callbacks + for name, cleanup in self._cleanups.items(): + try: + if asyncio.iscoroutinefunction(cleanup): + await cleanup() + else: + cleanup() + logger.debug(f"Cleaned up: {name}") + except Exception as e: + logger.error(f"Error during cleanup {name}: {e}") + + # Call final cleanup callback + if self.config.on_cleanup: + if asyncio.iscoroutinefunction(self.config.on_cleanup): + await self.config.on_cleanup() else: - cleanup() - logger.debug(f"Cleaned up: {name}") - except Exception as e: - logger.error(f"Error during cleanup {name}: {e}") - - # Call final cleanup callback - if self.config.on_cleanup: - if asyncio.iscoroutinefunction(self.config.on_cleanup): - await self.config.on_cleanup() - else: - self.config.on_cleanup() + self.config.on_cleanup() + finally: + if self._force_task is not None: + self._force_task.cancel() + try: + await self._force_task + except asyncio.CancelledError: + pass + self._force_task = None self._phase = ShutdownPhase.COMPLETED self._shutdown_event.set() logger.info("Shutdown complete") + async def _force_shutdown_after_timeout(self) -> None: + """Force-cancel running tasks if graceful shutdown exceeds force_after.""" + await asyncio.sleep(self.config.force_after) + if self._phase == ShutdownPhase.COMPLETED: + return + + logger.error( + "Force shutdown deadline reached after %.1fs; cancelling %d running task(s)", + self.config.force_after, + len(self._running_tasks), + ) + for task in list(self._running_tasks): + task.cancel() + async def wait(self) -> None: """Wait for shutdown to complete.""" await self._shutdown_event.wait() diff --git a/claude_code/tasks/__init__.py b/claude_code/tasks/__init__.py index beb928d..b71697f 100644 --- a/claude_code/tasks/__init__.py +++ b/claude_code/tasks/__init__.py @@ -39,8 +39,10 @@ RuntimeTaskRepositoryConfig, RuntimeTaskRepository, FileRuntimeTaskRepository, + SQLiteRuntimeTaskRepository, create_file_task_repository, create_file_runtime_task_repository, + create_sqlite_runtime_task_repository, ) from claude_code.tasks.queue import ( QueueItem, @@ -54,6 +56,7 @@ create_task_queue, create_runtime_repository, create_task_manager, + create_task_manager_with_event_journal, ) __all__ = [ @@ -87,8 +90,10 @@ "RuntimeTaskRepositoryConfig", "RuntimeTaskRepository", "FileRuntimeTaskRepository", + "SQLiteRuntimeTaskRepository", "create_file_task_repository", "create_file_runtime_task_repository", + "create_sqlite_runtime_task_repository", # Queue "QueueItem", @@ -102,6 +107,7 @@ "create_task_queue", "create_runtime_repository", "create_task_manager", + "create_task_manager_with_event_journal", ] diff --git a/claude_code/tasks/factory.py b/claude_code/tasks/factory.py index d75290b..6a7a0d2 100644 --- a/claude_code/tasks/factory.py +++ b/claude_code/tasks/factory.py @@ -6,14 +6,16 @@ from pathlib import Path from typing import Literal, Optional +from claude_code.services.event_journal import EventJournal from claude_code.tasks.queue import InMemoryTaskQueue, TaskQueue from claude_code.tasks.repository import ( RuntimeTaskRepository, create_file_runtime_task_repository, + create_sqlite_runtime_task_repository, ) QueueBackend = Literal["memory", "redis"] -RuntimeBackend = Literal["file", "none"] +RuntimeBackend = Literal["sqlite", "file", "none"] @dataclass(frozen=True, slots=True) @@ -22,7 +24,7 @@ class TaskBackendConfig: working_directory: str | Path queue_backend: QueueBackend = "memory" - runtime_backend: RuntimeBackend = "file" + runtime_backend: RuntimeBackend = "sqlite" def create_task_queue(backend: QueueBackend = "memory") -> TaskQueue: @@ -39,11 +41,13 @@ def create_task_queue(backend: QueueBackend = "memory") -> TaskQueue: def create_runtime_repository( working_directory: str | Path, - backend: RuntimeBackend = "file", + backend: RuntimeBackend = "sqlite", ) -> Optional[RuntimeTaskRepository]: """Create runtime repository by backend name.""" if backend == "none": return None + if backend == "sqlite": + return create_sqlite_runtime_task_repository(working_directory) if backend == "file": return create_file_runtime_task_repository(working_directory) raise ValueError(f"Unknown runtime backend: {backend}") @@ -61,6 +65,25 @@ def create_task_manager(config: TaskBackendConfig): return TaskManager(task_queue=queue, runtime_repository=runtime_repository) +def create_task_manager_with_event_journal( + config: TaskBackendConfig, + event_journal: EventJournal, +): + """Create TaskManager with selected backends and event journal.""" + from claude_code.tasks.manager import TaskManager + + queue = create_task_queue(config.queue_backend) + runtime_repository = create_runtime_repository( + config.working_directory, + config.runtime_backend, + ) + return TaskManager( + task_queue=queue, + runtime_repository=runtime_repository, + event_journal=event_journal, + ) + + __all__ = [ "QueueBackend", "RuntimeBackend", @@ -68,4 +91,5 @@ def create_task_manager(config: TaskBackendConfig): "create_task_queue", "create_runtime_repository", "create_task_manager", + "create_task_manager_with_event_journal", ] diff --git a/claude_code/tasks/manager.py b/claude_code/tasks/manager.py index 0a9d1df..fc29954 100644 --- a/claude_code/tasks/manager.py +++ b/claude_code/tasks/manager.py @@ -10,6 +10,7 @@ from dataclasses import dataclass, field from datetime import datetime +from claude_code.services.event_journal import EventJournal from claude_code.tasks.types import ( Task, TaskType, @@ -77,6 +78,7 @@ def __init__( task_queue: Optional[TaskQueue] = None, runtime_repository: Optional[RuntimeTaskRepository] = None, max_concurrency: Optional[int] = None, + event_journal: Optional[EventJournal] = None, ): if max_concurrency is not None and max_concurrency <= 0: raise ValueError("max_concurrency must be > 0 when provided") @@ -92,6 +94,7 @@ def __init__( self._attempt_counts: dict[str, int] = {} self._task_queue: TaskQueue = task_queue or InMemoryTaskQueue() self._runtime_repository = runtime_repository + self._event_journal = event_journal self._max_concurrency = max_concurrency self._queue_lag_samples: list[float] = [] self._execution_samples: list[float] = [] @@ -167,6 +170,7 @@ async def create_agent_task( tools: Optional[list] = None, background: bool = True, idempotency_key: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, ) -> AgentTask: """Create an agent task.""" if idempotency_key: @@ -187,6 +191,8 @@ async def create_agent_task( tools=tools, is_backgrounded=background, ) + if metadata: + task.metadata.update(dict(metadata)) if idempotency_key: task.metadata["idempotency_key"] = idempotency_key self._idempotency_index[idempotency_key] = task.id @@ -441,11 +447,45 @@ def register_handler(self, handler: Callable[[TaskEvent], None]) -> None: def _emit_event(self, event: TaskEvent) -> None: """Emit a task event.""" + self._record_journal_event(event) for handler in self._event_handlers: try: handler(event) except Exception: pass + + def _record_journal_event(self, event: TaskEvent) -> None: + """Persist normalized task lifecycle event into runtime event journal.""" + if self._event_journal is None: + return + + task = self._tasks.get(event.task_id) + task_type: str | None = None + task_status: str | None = None + task_metadata: dict[str, Any] = {} + if task is not None: + task_type = task.type.value if hasattr(task.type, "value") else str(task.type) + task_status = task.status.value if hasattr(task.status, "value") else str(task.status) + task_metadata = dict(task.metadata) + + session_id = task_metadata.get("session_id") + conversation_id = task_metadata.get("conversation_id") + self._event_journal.append_event( + event_type=f"task.{event.event_type}", + payload={ + "task_id": event.task_id, + "task_type": task_type, + "task_status": task_status, + "event_data": dict(event.data), + }, + session_id=session_id if isinstance(session_id, str) else None, + conversation_id=conversation_id if isinstance(conversation_id, str) else None, + source="task_manager", + metadata={ + "task_event_type": event.event_type, + }, + timestamp=event.timestamp.timestamp(), + ) async def wait_for_task( self, diff --git a/claude_code/tasks/repository.py b/claude_code/tasks/repository.py index c047d10..2c2b32d 100644 --- a/claude_code/tasks/repository.py +++ b/claude_code/tasks/repository.py @@ -3,6 +3,8 @@ from __future__ import annotations import json +import sqlite3 +import threading from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import datetime @@ -14,6 +16,7 @@ from claude_code.utils.task_store import TaskFileLock, load_tasks, save_tasks RUNTIME_TASKS_SCHEMA_VERSION = 1 +RUNTIME_TASKS_SCHEMA_KEY = "runtime_tasks_schema_version" @dataclass(frozen=True, slots=True) @@ -172,40 +175,164 @@ def _write_payload(self, payload: dict[str, Any]) -> None: ) def _task_to_record(self, task: Task) -> dict[str, Any]: - result_payload = None - if task.result is not None: - result_payload = { - "code": task.result.code, - "stdout": task.result.stdout, - "stderr": task.result.stderr, - "error": task.result.error, - } - - record: dict[str, Any] = { - "id": task.id, - "type": task.type.value, - "status": task.status.value, - "description": task.description, - "created_at": _dt_to_iso(task.created_at), - "started_at": _dt_to_iso(task.started_at), - "completed_at": _dt_to_iso(task.completed_at), - "error": task.error, - "is_backgrounded": task.is_backgrounded, - "parent_id": task.parent_id, - "tags": list(task.tags), - "metadata": dict(task.metadata), - "result": result_payload, - } + return _task_to_record(task) - if isinstance(task, BashTask): - record["command"] = task.command - record["cwd"] = task.cwd - record["timeout"] = task.timeout - elif isinstance(task, AgentTask): - record["prompt"] = task.prompt - record["model"] = task.model - return record +class SQLiteRuntimeTaskRepository(RuntimeTaskRepository): + """SQLite-backed runtime task repository.""" + + def __init__(self, db_path: str | Path) -> None: + self._db_path = Path(db_path) + self._db_path.parent.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + self._initialize_schema() + + @property + def db_path(self) -> Path: + return self._db_path + + def upsert_task(self, task: Task) -> None: + record = _task_to_record(task) + payload = json.dumps(record, ensure_ascii=False) + created_at = str(record.get("created_at") or "") + updated_at = datetime.now().isoformat() + with self._lock: + with self._connect() as conn: + conn.execute( + """ + INSERT INTO runtime_tasks ( + id, record_json, created_at, updated_at + ) VALUES (?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + record_json = excluded.record_json, + created_at = excluded.created_at, + updated_at = excluded.updated_at + """, + (task.id, payload, created_at, updated_at), + ) + + def delete_task(self, task_id: str) -> None: + with self._lock: + with self._connect() as conn: + conn.execute( + "DELETE FROM runtime_tasks WHERE id = ?", + (task_id,), + ) + + def get_task_record(self, task_id: str) -> dict[str, Any] | None: + with self._connect() as conn: + row = conn.execute( + "SELECT record_json FROM runtime_tasks WHERE id = ?", + (task_id,), + ).fetchone() + if row is None: + return None + return self._decode_record(row[0]) + + def list_task_records(self) -> list[dict[str, Any]]: + with self._connect() as conn: + rows = conn.execute( + """ + SELECT record_json + FROM runtime_tasks + ORDER BY created_at ASC, updated_at ASC, id ASC + """ + ).fetchall() + + records: list[dict[str, Any]] = [] + for row in rows: + record = self._decode_record(row[0]) + if record is not None: + records.append(record) + return records + + def _initialize_schema(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS runtime_tasks_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS runtime_tasks ( + id TEXT PRIMARY KEY, + record_json TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE INDEX IF NOT EXISTS idx_runtime_tasks_created_at + ON runtime_tasks (created_at, updated_at) + """ + ) + + version = self._read_schema_version(conn) + if version is None: + self._write_schema_version(conn, RUNTIME_TASKS_SCHEMA_VERSION) + elif version > RUNTIME_TASKS_SCHEMA_VERSION: + raise RuntimeError( + "Runtime task SQLite schema is newer than this binary: " + f"{version} > {RUNTIME_TASKS_SCHEMA_VERSION}" + ) + elif version < RUNTIME_TASKS_SCHEMA_VERSION: + self._migrate_schema(conn, version, RUNTIME_TASKS_SCHEMA_VERSION) + self._write_schema_version(conn, RUNTIME_TASKS_SCHEMA_VERSION) + + def _connect(self) -> sqlite3.Connection: + return sqlite3.connect(self._db_path, timeout=5.0) + + @staticmethod + def _decode_record(raw_payload: Any) -> dict[str, Any] | None: + if not isinstance(raw_payload, str) or not raw_payload: + return None + try: + parsed = json.loads(raw_payload) + except json.JSONDecodeError: + return None + if not isinstance(parsed, dict): + return None + return parsed + + @staticmethod + def _read_schema_version(conn: sqlite3.Connection) -> int | None: + row = conn.execute( + "SELECT value FROM runtime_tasks_metadata WHERE key = ?", + (RUNTIME_TASKS_SCHEMA_KEY,), + ).fetchone() + if row is None: + return None + try: + return int(row[0]) + except (TypeError, ValueError): + return None + + @staticmethod + def _write_schema_version(conn: sqlite3.Connection, version: int) -> None: + conn.execute( + """ + INSERT INTO runtime_tasks_metadata (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """, + (RUNTIME_TASKS_SCHEMA_KEY, str(version)), + ) + + @staticmethod + def _migrate_schema(conn: sqlite3.Connection, from_version: int, to_version: int) -> None: + if from_version < 1: + raise RuntimeError( + f"Unsupported runtime task SQLite schema version: {from_version}" + ) + if to_version != RUNTIME_TASKS_SCHEMA_VERSION: + raise RuntimeError(f"Unsupported target runtime task schema version: {to_version}") + # Schema v1 is current. Future migrations will be appended here. def _dt_to_iso(value: datetime | None) -> str | None: @@ -234,13 +361,68 @@ def create_file_runtime_task_repository( ) +def create_sqlite_runtime_task_repository( + working_directory: str | Path, + sqlite_db_path: str | Path | None = None, +) -> SQLiteRuntimeTaskRepository: + """Create default SQLite-backed runtime task repository.""" + workdir = Path(working_directory) + db_path = ( + Path(sqlite_db_path) + if sqlite_db_path is not None + else workdir / ".claude" / "runtime_state.db" + ) + db_path.parent.mkdir(parents=True, exist_ok=True) + return SQLiteRuntimeTaskRepository(db_path) + + +def _task_to_record(task: Task) -> dict[str, Any]: + result_payload = None + if task.result is not None: + result_payload = { + "code": task.result.code, + "stdout": task.result.stdout, + "stderr": task.result.stderr, + "error": task.result.error, + } + + record: dict[str, Any] = { + "id": task.id, + "type": task.type.value, + "status": task.status.value, + "description": task.description, + "created_at": _dt_to_iso(task.created_at), + "started_at": _dt_to_iso(task.started_at), + "completed_at": _dt_to_iso(task.completed_at), + "error": task.error, + "is_backgrounded": task.is_backgrounded, + "parent_id": task.parent_id, + "tags": list(task.tags), + "metadata": dict(task.metadata), + "result": result_payload, + } + + if isinstance(task, BashTask): + record["command"] = task.command + record["cwd"] = task.cwd + record["timeout"] = task.timeout + elif isinstance(task, AgentTask): + record["prompt"] = task.prompt + record["model"] = task.model + + return record + + __all__ = [ "TaskRepositoryConfig", "TaskRepository", "RuntimeTaskRepositoryConfig", "RuntimeTaskRepository", "FileRuntimeTaskRepository", + "SQLiteRuntimeTaskRepository", "RUNTIME_TASKS_SCHEMA_VERSION", + "RUNTIME_TASKS_SCHEMA_KEY", "create_file_task_repository", "create_file_runtime_task_repository", + "create_sqlite_runtime_task_repository", ] diff --git a/claude_code/tools/agent/__init__.py b/claude_code/tools/agent/__init__.py index 3df6def..53c0d70 100644 --- a/claude_code/tools/agent/__init__.py +++ b/claude_code/tools/agent/__init__.py @@ -12,6 +12,7 @@ import asyncio import os +import json from typing import Any, Optional from uuid import uuid4 from dataclasses import dataclass, field @@ -22,23 +23,64 @@ create_builtin_agents, setup_builtin_agents, ) +from claude_code.agents.loader import load_agents_from_directory +from claude_code.services.multi_agent_supervisor import ( + AgentNodeSpec, + MultiAgentSupervisor, + SupervisorBudget, + SupervisorConfigError, +) -# Module-level reusable default API config (avoid repeated object creation) -_DEFAULT_API_CONFIG = None +_MODEL_ALIASES: dict[str, str] = { + "haiku": "claude-haiku-20240307", + "sonnet": "claude-sonnet-4-20250514", + "opus": "claude-opus-4-20250514", + "sonnet-4": "claude-sonnet-4-20250514", + "opus-4": "claude-opus-4-20250514", +} -def _get_default_api_config(): - """Get or create a cached default API config for agent spawns. - - Reuses the same config object across agent calls to avoid - repeated object creation. The Anthropic SDK handles actual - connection pooling internally. - """ - global _DEFAULT_API_CONFIG - if _DEFAULT_API_CONFIG is None: - from claude_code.api.client import APIClientConfig - _DEFAULT_API_CONFIG = APIClientConfig() - return _DEFAULT_API_CONFIG +def _resolve_model_name(model: str) -> str: + """Resolve a short model alias to a full runtime model id.""" + if not model: + return _MODEL_ALIASES["sonnet"] + if model in _MODEL_ALIASES: + return _MODEL_ALIASES[model] + if model.startswith("claude-"): + return model + return model + + +def _resolve_runtime_provider(model_provider: Optional[str]) -> str: + """Normalize runtime provider string for sub-agent API-client config.""" + candidate = (model_provider or "").strip().lower() + if candidate in {"anthropic", "openai", "ollama", "vllm", "deepseek", "bedrock", "vertex"}: + return candidate + return "anthropic" + + +def _resolve_runtime_model(*, model: str, fallback_model: Optional[str]) -> str: + """Resolve sub-agent model with fallback to parent runtime model when needed.""" + resolved = _resolve_model_name(model) + if resolved.startswith("claude-") and fallback_model and not str(fallback_model).startswith("claude-"): + return str(fallback_model) + return resolved + + +def _build_subagent_system_prompt( + agent_prompt: str, + task_prompt: str, + working_directory: str, +) -> str: + """Build consistent system prompt for sync/background sub-agents.""" + return f"""{agent_prompt} + +You are executing as a sub-agent of Claude Code. Your task is: +{task_prompt} + +Working directory: {working_directory} + +Report your findings and results clearly.""" def _build_agent_registry() -> dict[str, AgentDefinition]: @@ -68,6 +110,11 @@ class AgentTool(Tool): name: agent description: Spawn a sub-agent to perform a task """ + + def __init__(self) -> None: + super().__init__() + self._custom_agents: dict[str, AgentDefinition] = {} + self._custom_agents_loaded_from: Optional[str] = None @property def name(self) -> str: @@ -99,8 +146,10 @@ def input_schema(self) -> dict[str, Any]: }, "subagent_type": { "type": "string", - "description": "The type of specialized agent to use", - "enum": list(BUILTIN_AGENTS.keys()), + "description": ( + "The type of specialized agent to use. " + "Supports built-in and .claude/agents custom agent names." + ), }, "model": { "type": "string", @@ -124,6 +173,24 @@ def input_schema(self) -> dict[str, Any]: "type": "string", "description": "Working directory for the agent" }, + "orchestrate": { + "type": "boolean", + "description": "Run a multi-agent DAG workflow instead of a single sub-agent", + }, + "workflow": { + "type": "object", + "description": "Workflow DAG payload when orchestrate=true", + "properties": { + "id": {"type": "string"}, + "nodes": { + "type": "array", + "items": {"type": "object"}, + }, + "budget": {"type": "object"}, + "timeout_seconds": {"type": "number"}, + }, + "required": ["nodes"], + }, }, "required": ["description", "prompt"] } @@ -133,6 +200,14 @@ def is_read_only(self) -> bool: def is_concurrency_safe(self) -> bool: return True + + def _ensure_custom_agents_loaded(self, cwd: str) -> None: + """Lazy-load custom agents for current workspace.""" + normalized = os.path.abspath(cwd) + if self._custom_agents_loaded_from == normalized: + return + self._custom_agents = load_agents_from_directory(normalized) + self._custom_agents_loaded_from = normalized async def execute( self, @@ -158,6 +233,8 @@ async def execute( name = input_data.get("name") isolation = input_data.get("isolation") cwd = input_data.get("cwd") + orchestrate = bool(input_data.get("orchestrate", False)) + workflow = input_data.get("workflow") if not description: return ToolResult( @@ -170,18 +247,30 @@ async def execute( content="Error: prompt is required", is_error=True ) - + + self._ensure_custom_agents_loaded(context.working_directory) + agent_id = str(uuid4())[:8] agent_name = name or f"agent-{agent_id}" - - agent_def = BUILTIN_AGENTS.get(subagent_type) + + agent_def = self._custom_agents.get(subagent_type) or BUILTIN_AGENTS.get(subagent_type) if not agent_def: + available = sorted(set(self._custom_agents.keys()) | set(BUILTIN_AGENTS.keys())) return ToolResult( - content=f"Unknown agent type: {subagent_type}. Available: {', '.join(BUILTIN_AGENTS.keys())}", + content=f"Unknown agent type: {subagent_type}. Available: {', '.join(available)}", is_error=True ) effective_cwd = cwd or context.working_directory + + if orchestrate: + return await self._run_supervisor_workflow( + description=description, + prompt=prompt, + workflow=workflow, + cwd=effective_cwd, + context=context, + ) effective_model = model or agent_def.model @@ -209,6 +298,135 @@ async def execute( context=context, on_progress=on_progress, ) + + async def _run_supervisor_workflow( + self, + *, + description: str, + prompt: str, + workflow: Any, + cwd: str, + context: ToolContext, + ) -> ToolResult: + """Run P2-01 DAG supervisor workflow.""" + try: + workflow_payload = workflow if isinstance(workflow, dict) else {} + nodes_payload = workflow_payload.get("nodes") + if nodes_payload is None: + raise SupervisorConfigError("workflow.nodes is required when orchestrate=true") + if not isinstance(nodes_payload, list): + raise SupervisorConfigError("workflow.nodes must be an array") + + workflow_id = str(workflow_payload.get("id") or f"wf-{uuid4().hex[:8]}") + budget = SupervisorBudget.from_dict(workflow_payload.get("budget")) + timeout_raw = workflow_payload.get("timeout_seconds") + workflow_timeout_seconds = None + if timeout_raw is not None: + try: + workflow_timeout_seconds = float(timeout_raw) + except Exception as exc: + raise SupervisorConfigError("workflow.timeout_seconds must be a number") from exc + if workflow_timeout_seconds <= 0: + raise SupervisorConfigError("workflow.timeout_seconds must be > 0") + + nodes = [ + AgentNodeSpec.from_dict(item, index=index) + for index, item in enumerate(nodes_payload) + ] + + async def _node_executor( + node: AgentNodeSpec, + resolved_prompt: str, + dependency_artifacts: dict[str, dict[str, Any]], + ) -> str: + agent_def = BUILTIN_AGENTS.get(node.subagent_type) + if agent_def is None: + raise ValueError( + f"Unknown subagent_type '{node.subagent_type}' for node '{node.node_id}'" + ) + resolved_model = node.model or agent_def.model + result = await self._run_sync_agent( + agent_name=f"{workflow_id}-{node.node_id}", + description=node.description, + prompt=resolved_prompt, + agent_def=agent_def, + model=resolved_model, + isolation=agent_def.isolation, + cwd=cwd, + context=context, + on_progress=None, + ) + if result.is_error: + raise RuntimeError(str(result.content)) + return str(result.content) + + supervisor = MultiAgentSupervisor( + executor=_node_executor, + budget=budget, + workflow_timeout_seconds=workflow_timeout_seconds, + ) + run_result = await supervisor.run( + workflow_id=workflow_id, + nodes=nodes, + ) + summary = self._format_supervisor_summary( + workflow_id=workflow_id, + description=description, + root_prompt=prompt, + run_result=run_result, + ) + return ToolResult(content=summary) + except SupervisorConfigError as exc: + return ToolResult(content=f"Workflow config error: {exc}", is_error=True) + + def _format_supervisor_summary( + self, + *, + workflow_id: str, + description: str, + root_prompt: str, + run_result: Any, + ) -> str: + """Render workflow execution summary.""" + lines = [ + f"[supervisor:{workflow_id}] {description}", + f"root_prompt: {root_prompt}", + f"status: {run_result.status}", + f"duration_ms: {run_result.duration_ms:.2f}", + "nodes:", + ] + for item in run_result.results: + line = ( + f"- {item.node_id} [{item.status.value}] attempts={item.attempts} " + f"duration_ms={item.duration_ms:.2f}" + ) + if item.ownership: + line += f" ownership={','.join(item.ownership)}" + if item.error: + line += f" error={item.error}" + lines.append(line) + artifacts = json.dumps(run_result.artifacts_by_node, ensure_ascii=True, indent=2, sort_keys=True) + lines.append("artifacts:") + lines.append(artifacts) + return "\n".join(lines) + + @staticmethod + def _extract_text_result(event: Any) -> str: + """Normalize assistant/text stream events to plain text chunks.""" + from claude_code.engine.query import Message + + if isinstance(event, Message) and event.role == "assistant": + if isinstance(event.content, str): + return event.content + if isinstance(event.content, list): + chunks: list[str] = [] + for block in event.content: + if isinstance(block, dict) and block.get("type") == "text": + chunks.append(str(block.get("text", ""))) + return "".join(chunks) + if isinstance(event, dict) and event.get("type") == "text": + return str(event.get("content", "")) + return "" async def _run_sync_agent( self, @@ -223,8 +441,7 @@ async def _run_sync_agent( on_progress: Optional[ToolCallback], ) -> ToolResult: """Run agent synchronously.""" - from claude_code.engine.query import QueryEngine, Message - from claude_code.api.client import APIClient, APIClientConfig + from claude_code.engine.query import QueryConfig, QueryEngine worktree_path = None if isolation == "worktree": @@ -233,36 +450,46 @@ async def _run_sync_agent( cwd = worktree_path try: - api_config = _get_default_api_config() - api_client = APIClient(api_config) - - system_prompt = f"""{agent_def.prompt} + parent_provider = _resolve_runtime_provider(context.api_provider) + parent_model = str(context.model) if context.model else None + system_prompt = _build_subagent_system_prompt( + agent_prompt=agent_def.prompt, + task_prompt=prompt, + working_directory=cwd, + ) + resolved_model = _resolve_runtime_model(model=model, fallback_model=parent_model) -You are executing as a sub-agent of Claude Code. Your task is: -{prompt} + from claude_code.main import Config, setup_api_client -Working directory: {cwd} + api_client = setup_api_client( + Config( + api_provider=parent_provider, + model=resolved_model, + ) + ) -Report your findings and results clearly.""" - + query_config = QueryConfig( + model=resolved_model, + system_prompt=system_prompt, + permission_mode=context.permission_mode, + always_allow=list(context.always_allow), + always_deny=list(context.always_deny), + working_directory=cwd, + memory_scope=agent_def.memory or context.memory_scope or "project", + ) engine = QueryEngine( api_client=api_client, - config=None, - tool_registry=None, + config=query_config, + tool_registry=context.tool_registry, ) - - engine.config.system_prompt = system_prompt - if model: - engine.config.model = f"claude-{model}-4-20250514" - + if context.task_manager is not None: + engine.task_manager = context.task_manager + result_parts = [] async for event in engine.query(prompt): - if isinstance(event, Message) and event.role == "assistant": - if isinstance(event.content, str): - result_parts.append(event.content) - elif isinstance(event, dict): - if event.get("type") == "text": - result_parts.append(event.get("content", "")) + text_chunk = self._extract_text_result(event) + if text_chunk: + result_parts.append(text_chunk) result = "\n".join(result_parts) if result_parts else "Agent completed with no output" @@ -291,41 +518,84 @@ async def _run_async_agent( ) -> ToolResult: """Run agent asynchronously in background.""" from claude_code.tasks.manager import TaskManager - from claude_code.tasks.types import AgentTask - - task_manager = TaskManager.get_instance() - + from claude_code.engine.query import QueryConfig, QueryEngine + from claude_code.tasks.types import TaskResult + + task_manager = context.task_manager if isinstance(context.task_manager, TaskManager) else TaskManager.get_instance() + execution_cwd = cwd + worktree_path = None + if isolation == "worktree": + worktree_path = await self._create_worktree(agent_name, cwd) + if worktree_path: + execution_cwd = worktree_path + + parent_provider = _resolve_runtime_provider(context.api_provider) + parent_model = str(context.model) if context.model else None + resolved_model = _resolve_runtime_model(model=model, fallback_model=parent_model) + system_prompt = _build_subagent_system_prompt( + agent_prompt=agent_def.prompt, + task_prompt=prompt, + working_directory=execution_cwd, + ) + + task_metadata = { + "session_id": context.session_id or "", + "conversation_id": context.conversation_id or "", + "working_directory": execution_cwd, + "permission_mode": context.permission_mode, + "memory_scope": agent_def.memory or context.memory_scope or "project", + "provider": parent_provider, + "model": resolved_model, + "source_tool": "agent", + } task = await task_manager.create_agent_task( prompt=prompt, - model=model, + model=resolved_model, background=True, + metadata=task_metadata, ) async def _execute_agent(task): - from claude_code.engine.query import QueryEngine, Message - from claude_code.api.client import APIClient - - api_config = _get_default_api_config() - api_client = APIClient(api_config) - engine = QueryEngine(api_client=api_client) - engine.config.system_prompt = agent_def.prompt - if model: - engine.config.model = f"claude-{model}-4-20250514" - - result_parts = [] - async for event in engine.query(prompt): - if isinstance(event, Message) and event.role == "assistant": - if isinstance(event.content, str): - result_parts.append(event.content) - elif isinstance(event, dict): - if event.get("type") == "text": - result_parts.append(event.get("content", "")) - - from claude_code.tasks.types import TaskResult - return TaskResult( - code=0, - stdout="\n".join(result_parts) if result_parts else "Agent completed with no output", - ) + try: + from claude_code.main import Config, setup_api_client + + api_client = setup_api_client( + Config( + api_provider=parent_provider, + model=resolved_model, + ) + ) + query_config = QueryConfig( + model=resolved_model, + system_prompt=system_prompt, + permission_mode=context.permission_mode, + always_allow=list(context.always_allow), + always_deny=list(context.always_deny), + working_directory=execution_cwd, + memory_scope=agent_def.memory or context.memory_scope or "project", + session_id=context.session_id, + ) + engine = QueryEngine( + api_client=api_client, + config=query_config, + tool_registry=context.tool_registry, + ) + if context.task_manager is not None: + engine.task_manager = context.task_manager + + result_parts = [] + async for event in engine.query(prompt): + text_chunk = self._extract_text_result(event) + if text_chunk: + result_parts.append(text_chunk) + + return TaskResult( + code=0, + stdout="\n".join(result_parts) if result_parts else "Agent completed with no output", + ) + finally: + if worktree_path: + await self._cleanup_worktree(worktree_path) await task_manager.start_task(task.id, executor=_execute_agent) diff --git a/claude_code/tools/base.py b/claude_code/tools/base.py index 70ebd4b..4e3d346 100644 --- a/claude_code/tools/base.py +++ b/claude_code/tools/base.py @@ -87,6 +87,11 @@ class ToolContext: always_deny: list[str] = field(default_factory=list) model: Optional[str] = None session_id: Optional[str] = None + memory_scope: Optional[str] = None + conversation_id: Optional[str] = None + api_provider: Optional[str] = None + task_manager: Any = None + tool_registry: Optional["ToolRegistry"] = None _allow_set: Optional[frozenset[str]] = field(default=None, repr=False) _deny_set: Optional[frozenset[str]] = field(default=None, repr=False) diff --git a/claude_code/tools/cron/__init__.py b/claude_code/tools/cron/__init__.py index a367a7b..5fc52e2 100644 --- a/claude_code/tools/cron/__init__.py +++ b/claude_code/tools/cron/__init__.py @@ -10,16 +10,21 @@ from __future__ import annotations -import os -import json -import subprocess from typing import Any, Optional from datetime import datetime from pathlib import Path from uuid import uuid4 from claude_code.tools.base import Tool, ToolContext, ToolResult, ToolCallback -from claude_code.tools.cron.create import CronCreateTool +from claude_code.tools.cron.create import ( + CronCreateTool, + _SESSION_ONLY_TASKS, + _load_persisted_tasks, + _save_persisted_tasks, +) + + +_SCHEDULE_FILE_NAME = "scheduled_tasks.json" class ScheduleCronTool(Tool): @@ -102,7 +107,7 @@ async def execute( command = input_data.get("command", "") schedule = input_data.get("schedule", "") name = input_data.get("name", str(uuid4())[:8]) - enabled = input_data.get("enabled", True) + enabled = bool(input_data.get("enabled", True)) if not command or not schedule: return ToolResult( @@ -110,23 +115,20 @@ async def execute( is_error=True ) - schedule_file = Path(context.working_directory) / ".claude" / "schedules.json" - schedule_file.parent.mkdir(exist_ok=True) - - schedules = {} - if schedule_file.exists(): - with open(schedule_file) as f: - schedules = json.load(f) - + schedule_file = Path(context.working_directory) / ".claude" / _SCHEDULE_FILE_NAME + schedules = _load_persisted_tasks(schedule_file) schedules[name] = { - "command": command, - "schedule": schedule, - "enabled": enabled, + "cron": schedule, + "prompt": command, + "recurring": True, + "durable": True, + "status": "active" if enabled else "disabled", "created_at": datetime.now().isoformat(), + "last_run": None, + "source": "schedule_cron", + "name": name, } - - with open(schedule_file, "w") as f: - json.dump(schedules, f, indent=2) + _save_persisted_tasks(schedule_file, schedules) return ToolResult(content=f"""# Schedule Created @@ -134,6 +136,7 @@ async def execute( **Command:** {command} **Schedule:** {schedule} **Enabled:** {enabled} +**Stored In:** .claude/{_SCHEDULE_FILE_NAME} Use /schedule list to see all schedules.""") @@ -192,24 +195,33 @@ async def execute( Returns: ToolResult with list of schedules. """ - schedule_file = Path(context.working_directory) / ".claude" / "schedules.json" - - if not schedule_file.exists(): - return ToolResult(content="No schedules configured") - - with open(schedule_file) as f: - schedules = json.load(f) - - if not schedules: + schedule_file = Path(context.working_directory) / ".claude" / _SCHEDULE_FILE_NAME + persisted = _load_persisted_tasks(schedule_file) + if not persisted and not _SESSION_ONLY_TASKS: return ToolResult(content="No schedules configured") lines = ["# Scheduled Tasks\n"] - - for name, config in schedules.items(): + + for name, config in persisted.items(): + cron = str(config.get("cron", config.get("schedule", ""))) + prompt = str(config.get("prompt", config.get("command", ""))) + enabled = str(config.get("status", "active")).lower() == "active" + lines.append(f"\n## {name}") + lines.append(f"Prompt/Command: {prompt}") + lines.append(f"Cron: {cron}") + lines.append(f"Enabled: {enabled}") + lines.append("Durable: True") + lines.append(f"Created: {config.get('created_at', 'unknown')}") + + for name, config in _SESSION_ONLY_TASKS.items(): + cron = str(config.get("cron", "")) + prompt = str(config.get("prompt", "")) + enabled = str(config.get("status", "active")).lower() == "active" lines.append(f"\n## {name}") - lines.append(f"Command: {config['command']}") - lines.append(f"Schedule: {config['schedule']}") - lines.append(f"Enabled: {config['enabled']}") + lines.append(f"Prompt/Command: {prompt}") + lines.append(f"Cron: {cron}") + lines.append(f"Enabled: {enabled}") + lines.append("Durable: False (session-only)") lines.append(f"Created: {config.get('created_at', 'unknown')}") return ToolResult(content="\n".join(lines)) @@ -282,22 +294,21 @@ async def execute( if not name: return ToolResult(content="Error: name is required", is_error=True) - schedule_file = Path(context.working_directory) / ".claude" / "schedules.json" - - if not schedule_file.exists(): - return ToolResult(content=f"Schedule not found: {name}", is_error=True) - - with open(schedule_file) as f: - schedules = json.load(f) - - if name not in schedules: + schedule_file = Path(context.working_directory) / ".claude" / _SCHEDULE_FILE_NAME + schedules = _load_persisted_tasks(schedule_file) + + deleted = False + if name in schedules: + del schedules[name] + _save_persisted_tasks(schedule_file, schedules) + deleted = True + if name in _SESSION_ONLY_TASKS: + del _SESSION_ONLY_TASKS[name] + deleted = True + + if not deleted: return ToolResult(content=f"Schedule not found: {name}", is_error=True) - - del schedules[name] - - with open(schedule_file, "w") as f: - json.dump(schedules, f, indent=2) - + return ToolResult(content=f"Deleted schedule: {name}") diff --git a/claude_code/tools/registry.py b/claude_code/tools/registry.py index a03d464..6904a5c 100644 --- a/claude_code/tools/registry.py +++ b/claude_code/tools/registry.py @@ -1,24 +1,37 @@ """Tools module - Registry with default tool registration.""" -from typing import Optional, Any, Callable -from claude_code.tools.base import Tool, ToolDefinition, ToolContext, ToolResult, ToolInput, ToolProgress +from __future__ import annotations + +import importlib +from dataclasses import dataclass +from typing import Any, Callable, Optional + +from claude_code.tools.base import ( + Tool, + ToolCallback, + ToolContext, + ToolDefinition, + ToolInput, + ToolProgress, + ToolResult, +) class ToolRegistry: """Registry for all available tools. - + Manages tool registration, retrieval, and aliasing. Supports lazy loading - tools are only instantiated when accessed. - + Attributes: _tools: Dictionary mapping tool names to Tool instances (or factories) _aliases: Dictionary mapping aliases to tool names _lazy_factories: Dictionary mapping tool names to factory functions """ - + def __init__(self, lazy: bool = True) -> None: """Initialize tool registry. - + Args: lazy: If True, tools are loaded lazily on first access. """ @@ -26,113 +39,102 @@ def __init__(self, lazy: bool = True) -> None: self._aliases: dict[str, str] = {} self._lazy_factories: dict[str, Callable[[], Tool]] = {} self._lazy = lazy - + def register(self, tool: Tool) -> None: """Register a tool. - + Args: tool: Tool instance to register """ self._tools[tool.name] = tool - - # Register aliases + definition = tool.get_definition() for alias in definition.aliases: self._aliases[alias] = tool.name - - def register_lazy(self, name: str, factory: Callable[[], Tool], aliases: list[str] = None) -> None: + + def register_lazy(self, name: str, factory: Callable[[], Tool], aliases: Optional[list[str]] = None) -> None: """Register a tool factory for lazy loading. - + Args: name: Tool name factory: Factory function that creates the tool instance aliases: Optional list of aliases for the tool """ self._lazy_factories[name] = factory - self._tools[name] = None # Placeholder until first access - + self._tools[name] = None + if aliases: for alias in aliases: self._aliases[alias] = name - + def _resolve_tool(self, name: str) -> Optional[Tool]: """Resolve a lazy-loaded tool if needed. - + Args: name: Tool name to resolve - + Returns: Tool instance or None if not found """ if name not in self._tools: - # Check if we have a lazy factory for this - if name in self._lazy_factories: - self._tools[name] = self._lazy_factories[name]() - del self._lazy_factories[name] # No longer needed - else: - return None - - # Check if it's a lazy placeholder + return None + tool = self._tools[name] - if tool is None and name in self._lazy_factories: - tool = self._lazy_factories[name]() - self._tools[name] = tool - del self._lazy_factories[name] - + if tool is not None: + return tool + + factory = self._lazy_factories.pop(name, None) + if factory is None: + return None + + tool = factory() + self._tools[name] = tool return tool - + def get(self, name: str) -> Optional[Tool]: """Get a tool by name or alias. - + Optimized lookup order: 1. Direct name in _tools (O(1) dict lookup) 2. Alias resolution (O(1) dict lookup) - 3. Lazy factory resolution (O(1) dict lookup) - + Args: name: Tool name or alias - + Returns: Tool instance or None if not found """ - # Direct lookup — most common case - tool = self._tools.get(name) + tool = self._resolve_tool(name) if tool is not None: return tool - - # Check if it's a lazy placeholder that needs resolution - if name in self._lazy_factories: - resolved = self._lazy_factories.pop(name) - tool = resolved() - self._tools[name] = tool - return tool - - # Check aliases + real_name = self._aliases.get(name) if real_name is not None: - # Alias resolution — recurse with real name - return self.get(real_name) - + return self._resolve_tool(real_name) + return None - + def preload(self) -> None: """Preload all lazy tools (for eager initialization when needed).""" for name in list(self._lazy_factories.keys()): - self._resolve_tool(name) - + try: + self._resolve_tool(name) + except Exception: + # Keep startup resilient when optional/experimental tools cannot load. + continue + def list_all(self) -> list[Tool]: """List all registered tools. - + Returns: List of all Tool instances """ - # Resolve all lazy tools first self.preload() - return list(self._tools.values()) - + return [tool for tool in self._tools.values() if tool is not None] + def get_definitions(self) -> list[dict[str, Any]]: """Get all tool definitions for API. - + Optimized to avoid preloading all tools. Only resolves tools that have already been accessed. """ @@ -141,185 +143,149 @@ def get_definitions(self) -> list[dict[str, Any]]: for tool in self._tools.values() if tool is not None ] - + def get_names(self) -> list[str]: """Get all tool names. - + Returns: List of tool names """ - return list(self._tools.keys()) + list(self._lazy_factories.keys()) + return sorted(set(self._tools.keys()) | set(self._lazy_factories.keys())) + + +@dataclass(frozen=True, slots=True) +class _LazyToolSpec: + """Static lazy-registration specification for a tool.""" + + name: str + module: str + class_name: str + aliases: tuple[str, ...] = () + + +def _create_tool_instance(module_name: str, class_name: str, tool_name: str) -> Tool: + """Import a tool class and create an instance on-demand.""" + module = importlib.import_module(module_name) + try: + tool_class = getattr(module, class_name) + except AttributeError as exc: + raise ImportError( + f"Tool '{tool_name}' could not be loaded: missing {class_name} in {module_name}" + ) from exc + + tool = tool_class() + if not isinstance(tool, Tool): + raise TypeError( + f"Tool '{tool_name}' loaded from {module_name}.{class_name} is not a Tool instance" + ) + return tool + + +def _register_lazy_spec(registry: ToolRegistry, spec: _LazyToolSpec) -> None: + """Register one lazy tool spec into registry.""" + + def _factory(module_name: str = spec.module, class_name: str = spec.class_name, tool_name: str = spec.name) -> Tool: + return _create_tool_instance(module_name, class_name, tool_name) + + registry.register_lazy(spec.name, _factory, list(spec.aliases)) def create_default_registry() -> ToolRegistry: - """Create the default tool registry with all built-in tools. - - Uses lazy loading - tools are only instantiated when first accessed. - This significantly improves startup time by deferring object creation. - - Returns: - Configured ToolRegistry with all tools registered (lazy) + """Create the default tool registry with lazy module imports. + + Compared with the previous implementation, this function no longer imports + all tool modules up front. Each module is imported only when the tool is + first resolved via ``get()`` / ``preload()``. """ - # Import tools here to avoid circular imports - from claude_code.tools.builtin import ( - BashTool, ReadTool, WriteTool, EditTool, - GlobTool, GrepTool, NotebookEditTool, - ) - from claude_code.tools.utility import ( - TodoWriteTool, WebSearchTool, WebFetchTool, SendMessageTool, - SnipTool, BriefTool, SendUserFileTool, SuggestBackgroundPRTool, - OverflowTestTool, SyntheticOutputTool, - ) - from claude_code.tools.system import ( - SleepTool, PowerShellTool, MonitorTool, ConfigTool, - ) - from claude_code.tools.workflow import ( - VerifyTool, EnterPlanModeTool, ExitPlanModeTool, WorkflowTool, - TaskCreateTool, TaskGetTool, TaskUpdateTool, - REPLTool, ReviewArtifactTool, - ) - from claude_code.tools.agent import AgentTool - from claude_code.tools.mcp import ( - MCPTool, ListMcpResourcesTool, ReadMcpResourceTool, - McpAuthTool, ListMcpToolsTool, ListMcpPromptsTool, - ) - from claude_code.tools.skills import ( - SkillTool, ListSkillsTool, DiscoverSkillsTool, - ) - from claude_code.tools.control import ( - TaskStopTool, TaskOutputTool, TaskControlListTool, - ) - from claude_code.tools.lsp import LSPTool - from claude_code.tools.browser import BrowserTool - from claude_code.tools.analysis.tool import AnalyzeTool - from claude_code.tools.ask_question import AskUserQuestionTool - from claude_code.tools.worktree import ( - EnterWorktreeTool, ExitWorktreeTool, ListWorktreesTool, - ) - from claude_code.tools.cron import ( - ScheduleCronTool, CronListTool, CronDeleteTool, CronCreateTool, - ) - from claude_code.tools.team import ( - TeamCreateTool, TeamDeleteTool, TeamAddMemberTool, TeamListTool, - ) - from claude_code.tools.terminal import TerminalCaptureTool - from claude_code.tools.search import ToolSearchTool, RemoteTriggerTool - - # Workflow TaskListTool (also registered by control module under same key) - from claude_code.tools.workflow import TaskListTool as WorkflowTaskListTool - + registry = ToolRegistry(lazy=True) - - # Builtin tools - registry.register_lazy("bash", lambda: BashTool(), ["shell", "sh"]) - registry.register_lazy("read", lambda: ReadTool(), ["file_read", "read_file"]) - registry.register_lazy("write", lambda: WriteTool(), ["file_write", "write_file"]) - registry.register_lazy("edit", lambda: EditTool(), ["file_edit"]) - registry.register_lazy("glob", lambda: GlobTool(), ["glob_files"]) - registry.register_lazy("grep", lambda: GrepTool(), ["search", "find"]) - registry.register_lazy("notebook_edit", lambda: NotebookEditTool()) - - # Utility tools - registry.register_lazy("todo_write", lambda: TodoWriteTool(), ["todo", "task"]) - registry.register_lazy("web_search", lambda: WebSearchTool(), ["search_web"]) - registry.register_lazy("web_fetch", lambda: WebFetchTool(), ["fetch_url", "http_get"]) - registry.register_lazy("send_message", lambda: SendMessageTool()) - registry.register_lazy("snip", lambda: SnipTool()) - registry.register_lazy("brief", lambda: BriefTool()) - registry.register_lazy("send_user_file", lambda: SendUserFileTool()) - registry.register_lazy("suggest_background_pr", lambda: SuggestBackgroundPRTool()) - registry.register_lazy("overflow_test", lambda: OverflowTestTool()) - registry.register_lazy("synthetic_output", lambda: SyntheticOutputTool()) - - # System tools - registry.register_lazy("sleep", lambda: SleepTool()) - registry.register_lazy("powershell", lambda: PowerShellTool(), ["ps1"]) - registry.register_lazy("monitor", lambda: MonitorTool()) - registry.register_lazy("config", lambda: ConfigTool()) - - # Workflow tools - registry.register_lazy("verify", lambda: VerifyTool()) - registry.register_lazy("enter_plan_mode", lambda: EnterPlanModeTool()) - registry.register_lazy("exit_plan_mode", lambda: ExitPlanModeTool()) - registry.register_lazy("workflow", lambda: WorkflowTool()) - registry.register_lazy("task_create", lambda: TaskCreateTool()) - registry.register_lazy("task_get", lambda: TaskGetTool()) - registry.register_lazy("task_update", lambda: TaskUpdateTool()) - registry.register_lazy("task_list", lambda: WorkflowTaskListTool()) - registry.register_lazy("task_control_list", lambda: TaskControlListTool()) - registry.register_lazy("repl", lambda: REPLTool()) - registry.register_lazy("review_artifact", lambda: ReviewArtifactTool()) - - # Agent tools - registry.register_lazy("agent", lambda: AgentTool()) - - # MCP tools - registry.register_lazy("mcp", lambda: MCPTool(), ["mcp_tool"]) - registry.register_lazy("list_mcp_resources", lambda: ListMcpResourcesTool()) - registry.register_lazy("read_mcp_resource", lambda: ReadMcpResourceTool()) - registry.register_lazy("mcp_auth", lambda: McpAuthTool()) - registry.register_lazy("list_mcp_tools", lambda: ListMcpToolsTool()) - registry.register_lazy("list_mcp_prompts", lambda: ListMcpPromptsTool()) - - # Skills tools - registry.register_lazy("skill", lambda: SkillTool()) - registry.register_lazy("list_skills", lambda: ListSkillsTool()) - registry.register_lazy("discover_skills", lambda: DiscoverSkillsTool()) - - # Control tools - registry.register_lazy("task_stop", lambda: TaskStopTool()) - registry.register_lazy("task_output", lambda: TaskOutputTool()) - - # LSP tools (may fail if dependencies not available) - try: - registry.register_lazy("lsp", lambda: LSPTool()) - except Exception: - pass - - # Browser tools (if playwright available) - try: - registry.register_lazy("browser", lambda: BrowserTool()) - except Exception: - pass - - # Analysis tools - registry.register_lazy("analyze", lambda: AnalyzeTool()) - - # Ask question tools - registry.register_lazy("ask_question", lambda: AskUserQuestionTool()) - - # Worktree tools - registry.register_lazy("enter_worktree", lambda: EnterWorktreeTool()) - registry.register_lazy("exit_worktree", lambda: ExitWorktreeTool()) - registry.register_lazy("list_worktrees", lambda: ListWorktreesTool()) - - # Cron tools - registry.register_lazy("schedule_cron", lambda: ScheduleCronTool()) - registry.register_lazy("cron_create", lambda: CronCreateTool()) - registry.register_lazy("cron_list", lambda: CronListTool()) - registry.register_lazy("cron_delete", lambda: CronDeleteTool()) - - # Team tools - registry.register_lazy("team_create", lambda: TeamCreateTool()) - registry.register_lazy("team_delete", lambda: TeamDeleteTool()) - registry.register_lazy("team_add_member", lambda: TeamAddMemberTool()) - registry.register_lazy("team_list", lambda: TeamListTool()) - - # Terminal tools (may fail if dependencies not available) - try: - registry.register_lazy("terminal_capture", lambda: TerminalCaptureTool()) - except Exception: - pass - - # Search tools - registry.register_lazy("tool_search", lambda: ToolSearchTool()) - registry.register_lazy("remote_trigger", lambda: RemoteTriggerTool()) - + + specs: tuple[_LazyToolSpec, ...] = ( + # Builtin tools + _LazyToolSpec("bash", "claude_code.tools.builtin.bash", "BashTool", ("shell", "sh")), + _LazyToolSpec("read", "claude_code.tools.builtin.read", "ReadTool", ("file_read", "read_file")), + _LazyToolSpec("write", "claude_code.tools.builtin.write", "WriteTool", ("file_write", "write_file")), + _LazyToolSpec("edit", "claude_code.tools.builtin.edit", "EditTool", ("file_edit",)), + _LazyToolSpec("glob", "claude_code.tools.builtin.glob", "GlobTool", ("glob_files",)), + _LazyToolSpec("grep", "claude_code.tools.builtin.grep", "GrepTool", ("search", "find")), + _LazyToolSpec("notebook_edit", "claude_code.tools.builtin.notebook_edit", "NotebookEditTool"), + # Utility tools + _LazyToolSpec("todo_write", "claude_code.tools.utility.todo", "TodoWriteTool", ("todo", "task")), + _LazyToolSpec("web_search", "claude_code.tools.utility.web_search", "WebSearchTool", ("search_web",)), + _LazyToolSpec("web_fetch", "claude_code.tools.utility.web_fetch", "WebFetchTool", ("fetch_url", "http_get")), + _LazyToolSpec("send_message", "claude_code.tools.utility.send_message", "SendMessageTool"), + _LazyToolSpec("snip", "claude_code.tools.utility.snip", "SnipTool"), + _LazyToolSpec("brief", "claude_code.tools.utility.brief", "BriefTool"), + _LazyToolSpec("send_user_file", "claude_code.tools.utility.send_user_file", "SendUserFileTool"), + _LazyToolSpec("suggest_background_pr", "claude_code.tools.utility.suggest_background_pr", "SuggestBackgroundPRTool"), + _LazyToolSpec("overflow_test", "claude_code.tools.utility.overflow_test", "OverflowTestTool"), + _LazyToolSpec("synthetic_output", "claude_code.tools.utility.synthetic_output", "SyntheticOutputTool"), + # System tools + _LazyToolSpec("sleep", "claude_code.tools.system.sleep", "SleepTool"), + _LazyToolSpec("powershell", "claude_code.tools.system.powershell", "PowerShellTool", ("ps1",)), + _LazyToolSpec("monitor", "claude_code.tools.system.monitor", "MonitorTool"), + _LazyToolSpec("config", "claude_code.tools.system.config", "ConfigTool"), + # Workflow tools + _LazyToolSpec("verify", "claude_code.tools.workflow.verify", "VerifyTool"), + _LazyToolSpec("enter_plan_mode", "claude_code.tools.workflow.plan_mode", "EnterPlanModeTool"), + _LazyToolSpec("exit_plan_mode", "claude_code.tools.workflow.plan_mode", "ExitPlanModeTool"), + _LazyToolSpec("workflow", "claude_code.tools.workflow.plan_mode", "WorkflowTool"), + _LazyToolSpec("task_create", "claude_code.tools.workflow.task_create", "TaskCreateTool"), + _LazyToolSpec("task_get", "claude_code.tools.workflow.task_get", "TaskGetTool"), + _LazyToolSpec("task_update", "claude_code.tools.workflow.task_update", "TaskUpdateTool"), + _LazyToolSpec("task_list", "claude_code.tools.workflow.task_list", "TaskListTool"), + _LazyToolSpec("task_control_list", "claude_code.tools.control", "TaskControlListTool"), + _LazyToolSpec("repl", "claude_code.tools.workflow.repl_tool", "REPLTool"), + _LazyToolSpec("review_artifact", "claude_code.tools.workflow.review_artifact", "ReviewArtifactTool"), + # Agent tools + _LazyToolSpec("agent", "claude_code.tools.agent", "AgentTool"), + # MCP tools + _LazyToolSpec("mcp", "claude_code.tools.mcp", "MCPTool", ("mcp_tool",)), + _LazyToolSpec("list_mcp_resources", "claude_code.tools.mcp", "ListMcpResourcesTool"), + _LazyToolSpec("read_mcp_resource", "claude_code.tools.mcp", "ReadMcpResourceTool"), + _LazyToolSpec("mcp_auth", "claude_code.tools.mcp", "McpAuthTool"), + _LazyToolSpec("list_mcp_tools", "claude_code.tools.mcp", "ListMcpToolsTool"), + _LazyToolSpec("list_mcp_prompts", "claude_code.tools.mcp", "ListMcpPromptsTool"), + # Skills tools + _LazyToolSpec("skill", "claude_code.tools.skills", "SkillTool"), + _LazyToolSpec("list_skills", "claude_code.tools.skills", "ListSkillsTool"), + _LazyToolSpec("discover_skills", "claude_code.tools.skills", "DiscoverSkillsTool"), + # Control tools + _LazyToolSpec("task_stop", "claude_code.tools.control", "TaskStopTool"), + _LazyToolSpec("task_output", "claude_code.tools.control", "TaskOutputTool"), + # Analysis/LSP tools + _LazyToolSpec("lsp", "claude_code.tools.analysis.lsp", "LSPTool"), + _LazyToolSpec("browser", "claude_code.tools.browser", "BrowserTool"), + _LazyToolSpec("analyze", "claude_code.tools.analysis.tool", "AnalyzeTool"), + _LazyToolSpec("ask_question", "claude_code.tools.ask_question", "AskUserQuestionTool"), + # Worktree tools + _LazyToolSpec("enter_worktree", "claude_code.tools.worktree", "EnterWorktreeTool"), + _LazyToolSpec("exit_worktree", "claude_code.tools.worktree", "ExitWorktreeTool"), + _LazyToolSpec("list_worktrees", "claude_code.tools.worktree", "ListWorktreesTool"), + # Cron tools + _LazyToolSpec("schedule_cron", "claude_code.tools.cron", "ScheduleCronTool"), + _LazyToolSpec("cron_create", "claude_code.tools.cron.create", "CronCreateTool"), + _LazyToolSpec("cron_list", "claude_code.tools.cron", "CronListTool"), + _LazyToolSpec("cron_delete", "claude_code.tools.cron", "CronDeleteTool"), + # Team tools + _LazyToolSpec("team_create", "claude_code.tools.team", "TeamCreateTool"), + _LazyToolSpec("team_delete", "claude_code.tools.team", "TeamDeleteTool"), + _LazyToolSpec("team_add_member", "claude_code.tools.team", "TeamAddMemberTool"), + _LazyToolSpec("team_list", "claude_code.tools.team", "TeamListTool"), + # Terminal/Search tools + _LazyToolSpec("terminal_capture", "claude_code.tools.terminal", "TerminalCaptureTool"), + _LazyToolSpec("tool_search", "claude_code.tools.search", "ToolSearchTool"), + _LazyToolSpec("remote_trigger", "claude_code.tools.search", "RemoteTriggerTool"), + ) + + for spec in specs: + _register_lazy_spec(registry, spec) + # Note: Internal/feature-gated tools (tungsten, web_browser, push_notification, # subscribe_pr, ctx_inspect, list_peers, verify_plan_execution) are not registered # because they always return "not available" errors. They can be re-enabled # when their features are implemented. - + return registry @@ -333,4 +299,4 @@ def create_default_registry() -> ToolRegistry: "ToolCallback", "ToolInput", "ToolProgress", -] \ No newline at end of file +] diff --git a/claude_code/utils/__init__.py b/claude_code/utils/__init__.py index 0e3b118..cb58995 100644 --- a/claude_code/utils/__init__.py +++ b/claude_code/utils/__init__.py @@ -4,6 +4,8 @@ Common utility functions for file operations, formatting, git, shell, etc. """ +import warnings + from claude_code.utils.errors import ( ClaudeCodeError, AbortError, @@ -126,7 +128,9 @@ unflatten_json, ) -from claude_code.utils.features import FeatureDiscovery, FeatureInfo +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from claude_code.utils.features import FeatureDiscovery, FeatureInfo __all__ = [ # Errors diff --git a/docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md b/docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md index 66bdc42..de1d31e 100644 --- a/docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md +++ b/docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md @@ -354,3 +354,90 @@ CLI/REPL ### 10.6 当前剩余问题(按严重性) - 已完成本轮目标内问题清零;当前未发现阻塞级残留项。 + +### 10.7 增量迭代(2026-04-17,第4轮) + +- [x] 修复默认工具注册表构建断点:`create_default_registry()` 不再在构建阶段一次性导入全部工具模块。 +- [x] 修复 `lsp` 工具导入路径错误:由错误的 `claude_code.tools.lsp` 改为实际实现 `claude_code.tools.analysis.lsp`。 +- [x] 将默认工具注册改为“模块路径 + 类名”懒加载规范,避免单个工具模块异常阻断整个注册表。 +- [x] 提升 registry 预加载健壮性:`preload()` 对加载失败工具做容错,确保主流程可继续。 + +对应文件: + +- `claude_code/tools/registry.py` +- `tests/test_tools_registry_runtime.py` + +回归验证: + +- `D:\code_environment\anaconda_all_css\py311\python.exe -m pytest -q -c pytest.ini tests/test_tools_registry_runtime.py tests/test_query_engine_runtime.py` +- 结果:`12 passed` + +新增测试覆盖: + +- `tests/test_tools_registry_runtime.py` + - 覆盖:默认注册表构建阶段不触发模块导入(验证真正的模块级懒加载)。 + - 覆盖:默认注册表包含关键工具名(含 `lsp` / `cron_create`)。 + - 覆盖:`lsp` 工具可从默认注册表正常解析。 + +本轮解决的问题: + +- 解决了默认注册表在运行时可能因 `LSPTool` 导入路径错误直接抛 `ImportError`、导致主链路不可用的问题。 +- 解决了“lazy 仅延迟实例化、不延迟模块导入”的实现偏差,降低初始化阶段耦合与失败面。 + +### 10.8 增量迭代(2026-04-17,第5轮) + +- [x] 继续闭环 `P1-5`:`ClaudeMdLoader` 的目录遍历顺序修正为“从根目录到当前目录”。 +- [x] 移除 `load_content()` 的宽泛异常吞没:由 `except Exception` 收敛为 `except (OSError, UnicodeError)`,并输出可观测 warning 日志。 +- [x] 增加路径展示一致性:新增 `_display_path()`,优先以工作目录相对路径展示,跨层级时回退 `os.path.relpath`。 +- [x] 测试临时目录治理:将 `tests/.tmp_context/` 纳入 `.gitignore`,避免运行时临时工件污染工作区。 + +对应文件: + +- `claude_code/engine/context.py` +- `tests/test_context_builder_runtime.py` +- `.gitignore` + +回归验证: + +- `D:\code_environment\anaconda_all_css\py311\python.exe -m pytest -q -c pytest.ini tests/test_context_builder_runtime.py tests/test_query_engine_runtime.py` +- 结果:`12 passed` + +新增测试覆盖: + +- `tests/test_context_builder_runtime.py` + - 覆盖:`find_claude_md_files()` 按 root -> current 顺序返回。 + - 覆盖:`load_content()` 组合内容顺序与目录层级一致。 + - 覆盖:单个 `CLAUDE.md` 读取失败时不影响其他文件加载。 + +本轮解决的问题: + +- 解决了 `CLAUDE.md` 多层级加载顺序与文档声明不一致的问题(此前为 current -> root)。 +- 解决了 `load_content()` 对所有异常静默吞没的问题,避免真实 IO/编码故障无迹可查。 + +### 10.9 后续迭代待办(建议顺序) + +- [ ] **P1-4:AgentTool 后台路径上下文继承一致性** + - 目标:`run_in_background=True` 与同步路径在 `working_directory`、`tool_registry`、`permission_mode`、`provider/model` 上行为一致。 + - 验收:新增回归测试覆盖同步/后台两条路径,确保同一输入下工具执行目录与权限判定一致。 + +- [ ] **P1-2:配置单一事实源(Config-Driven Runtime)** + - 目标:`create_engine()` / `setup_api_client()` 统一从 `Config` 读取,明确优先级(CLI > 本地配置 > 全局配置 > 环境变量 > 默认值)。 + - 验收:`/model`、配置文件和环境变量对运行时生效行为一致,新增端到端配置优先级测试。 + +- [ ] **P0-2:MCP 主链路契约化验证(connect -> initialize -> list -> call -> read)** + - 目标:补齐 MCP 客户端主链路契约测试,覆盖请求-响应闭环、通知分发、断连与超时清理。 + - 验收:新增 `tests/test_services_mcp_contract.py`(或等价文件),通过后可稳定验证三类 transport 的最小可用语义。 + +- [ ] **P2-6:QueryEngine 公共接口收敛与类型治理** + - 目标:减少公共接口层 `Any`,收敛未完工/占位式接口,降低调用方歧义。 + - 验收:补齐关键接口类型标注,新增静态检查与运行时回归(至少覆盖 query loop / tool execution / response normalization)。 + +- [ ] **发布链路稳定化(CI Smoke)** + - 目标:把“可运行”升级为“可分发”,在 CI 中固化基础发布验证。 + - 验收:至少包含 `python -m build`、wheel 安装校验、`claude --version` / 单条 smoke 命令可运行。 + +#### 10.9.1 每轮迭代统一回归基线(建议) + +- `D:\code_environment\anaconda_all_css\py311\python.exe -m pytest -q -c pytest.ini` +- 若本轮涉及 MCP:追加 `tests/test_services_mcp_runtime.py` + 契约测试文件 +- 若本轮涉及启动/注册:追加 `tests/test_tools_registry_runtime.py` + `tests/test_query_engine_runtime.py` diff --git a/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md b/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md new file mode 100644 index 0000000..b9a7166 --- /dev/null +++ b/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md @@ -0,0 +1,383 @@ +# Claude Code Python 超越 Claude Code 产品战略与落地路线图(2026-04-29) + +## 1. 文档定位 + +- 文档性质:产品战略与落地路线图 +- 适用范围:`claude-code-python` 仓库当前代码基线 +- 结论时间:`2026-04-29` +- 与现有文档关系: + - 不替代 `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` + - 不替代 `docs/current/middleware/MIDDLEWARE_EVOLUTION_*` + - 本文档负责回答两个问题: + 1. 这份代码是否具备替代 Claude Code 的底盘? + 2. 若目标不是“跟随”,而是“超越”,接下来应如何迭代? + +## 2. 一句话结论 + +当前项目不是废骨,而是一个已经具备 `QueryEngine + Tool Runtime + Task Runtime + MCP` 主干的 Python Agent 内核;但它仍处于“工程底盘可用、产品闭环不足、平台潜力很高”的阶段。 + +如果继续按“补齐 Claude Code 的 CLI 功能点”推进,项目会长期陷入 parity 追赶;若改道为“面向团队/组织的自主开发执行平台(Agent Operating System for Code Work)”,则有机会形成超越 Claude Code 的差异化产品。 + +## 3. 本次核验范围与证据 + +### 3.1 代码与测试快照 + +- `claude_code/` 约 `208` 个 Python 文件 +- `claude_code/` 约 `42,430` 行 Python 代码 +- 默认工具注册量约 `62` +- 命令体系约 `24` + +### 3.2 本地核验命令 + +以下命令在本地使用 `D:\code_environment\anaconda_all_css\py311\python.exe` 复核: + +```powershell +& 'D:\code_environment\anaconda_all_css\py311\python.exe' -m claude_code.main --help +& 'D:\code_environment\anaconda_all_css\py311\python.exe' -m pytest -q -c pytest.ini +``` + +### 3.3 本地核验结果 + +- CLI 帮助输出正常 +- 测试结果:`104 passed, 2 warnings` +- 系统级 `python` 指向 WindowsApps stub,而非稳定解释器 +- 结论:代码主干可运行,但安装/分发链仍不稳 + +## 4. 当前系统定性 + +### 4.1 值得保留的核心底盘 + +以下模块具备继续投资价值,且应作为后续平台化演进的核心: + +- `claude_code/engine/query.py` + - 模型对话、tool orchestration、permission context 的主链 +- `claude_code/tools/registry.py` + - 默认工具注册与 lazy load 机制 +- `claude_code/api/client.py` + - provider 适配入口 +- `claude_code/tasks/manager.py` + - 后台任务、并发执行、重试、状态管理 +- `claude_code/mcp/server.py` + - MCP server 暴露层 +- `claude_code/services/mcp/client.py` + - MCP client / transport 主干 + +这说明项目不是单纯的 CLI 壳,而是已经具备“模型 -> 工具 -> 任务 -> MCP -> 多执行模式”的平台基因。 + +### 4.2 当前阶段的真实问题 + +项目的核心问题不是“完全没功能”,而是“功能名义存在,但产品闭环不完整”。 + +典型表现如下: + +- 会话恢复逻辑存在于 `QueryEngine.resume_session()`,但 `/resume` 命令没有真正接入恢复链路 + - 真实恢复逻辑:`claude_code/engine/query.py` + - 命令层仅返回提示文案:`claude_code/commands/compact/__init__.py` +- `/memory` 更接近 `CLAUDE*.md` 文件管理,而非主动记忆系统 + - `claude_code/commands/status/__init__.py` +- `review` 当前只是 prompt 引导,不执行真实代码审查 + - `claude_code/commands/review/__init__.py` +- feature flag 运行时存在设计错误 + - `FeatureConfig` 为 frozen dataclass,但 `enable()` / `disable()` 试图原地赋值 + - 对应文件:`claude_code/features.py` +- `auth/logout` 命令依赖未导入的 `ConfigManager` + - 对应文件:`claude_code/commands/auth/__init__.py` +- 任务后端已抽象,但 Redis backend 仍未接线 + - 对应文件:`claude_code/tasks/factory.py` +- `team` / `worktree` / `hooks` 仍以 `.claude/*.json` 与 subprocess 级能力为主 + - 对应文件: + - `claude_code/tools/team/__init__.py` + - `claude_code/tools/worktree/__init__.py` + - `claude_code/commands/hooks/__init__.py` + +### 4.3 一个更关键的结构性判断 + +项目内部已经出现多个“定义完整、消费不足”的子系统: + +- `HistoryManager` +- `SessionMemory` +- `Application` +- `HooksManager` +- deprecated `plugins` +- `remote transport` + +这意味着当前仓库的主矛盾是: + +`子系统数量增长 > 主运行时闭环能力增长` + +如果不先收敛主链路,再继续加功能,复杂度会持续上升,但产品可用性不会同比增长。 + +## 5. 对标 Claude Code 的能力矩阵 + +下表中的“Claude Code 公共能力面”基于公开官方文档,用于界定对标边界,而非逐项抄袭目标。 + +| 能力域 | 当前仓库状态 | 对标 Claude Code | 判断 | +|---|---|---|---| +| CLI 基础交互 | 已具备 REPL / pipe / single query / MCP serve | 基础能力成熟 | 可对标 | +| Tool orchestration | 已具备主链 | 官方能力成熟 | 可对标 | +| Provider 接入 | 多 provider 已接入 | 官方更稳定 | 可对标但需稳固 | +| Session continuity | 有 session store,但命令层与运行时未闭环 | 官方具备 `resume/continue` 工作流 | 明显落后 | +| Memory | 有 `CLAUDE.md` 与 memory service 雏形,但未形成 active memory | 官方有 memory 体系 | 明显落后 | +| Hooks | 有 hooks 配置与 manager,但未进入统一治理流 | 官方 hooks 更产品化 | 落后 | +| Subagents | 已有内置 agent 和 background task 雏形 | 官方 subagents 更完整 | 中度落后 | +| Custom agents | 暂未完成 `.claude/agents` 级加载闭环 | 官方已支持 | 落后 | +| Worktree isolation | 有第一代工具实现 | 官方流程更成熟 | 中度落后 | +| IDE integration | 当前基本缺位 | 官方已有 IDE 集成 | 明显落后 | +| GitHub / CI integration | 当前基本缺位 | 官方已有 GitHub Actions 路径 | 明显落后 | +| Org policy / 审计 / 治理 | 当前基本缺位 | 官方已具备较强治理面 | 明显落后 | +| MCP server/client | 当前是强项之一 | 官方也支持 | 可形成竞争力 | +| 多 Agent orchestration | 有雏形,无 supervisor/control plane | 官方有子代理与工作流能力 | 可超车机会点 | + +## 6. 战略方向判断 + +## 6.1 不建议的路线:功能点追平式模仿 + +不建议将目标定义为: + +“在 Python 中做一个功能点尽量像 Claude Code 的终端工具。” + +原因: + +1. 官方能力会持续演化,纯追平路线天然慢一拍。 +2. 终端交互层并不是官方唯一战场,IDE、desktop、CI、org policy 才是更深壁垒。 +3. 你当前仓库最强的不是 terminal UX,而是 runtime、MCP、task、orchestration 潜力。 + +## 6.2 建议的路线:Agent Operating System for Code Work + +建议将产品重新定义为: + +`面向团队与组织的代码工作 Agent 执行平台` + +一句话描述: + +> 让模型不只“会写代码”,而是能在受控环境中计划、执行、协作、审计、恢复、交付。 + +这一路线下,CLI 只是一个入口,而不是产品本体。 + +## 6.3 超越 Claude Code 的差异化抓手 + +建议把以下四个方向作为真正的超车点: + +1. `Daemon / API First` + - CLI、IDE、desktop、web、CI 统一接同一执行后端 +2. `Event Journal + Replay` + - 每一次 prompt、tool call、permission decision、artifact 生成都可追溯、可回放、可恢复 +3. `Supervisor-grade Multi-Agent Orchestration` + - 不是简单“再开一个 agent”,而是可控的 agent graph、预算、依赖、合流、失败恢复 +4. `Org Policy + Audit + Secret Safety` + - 在团队/组织环境中可落地,这是官方之外仍有巨大空间的战场 + +## 7. 模块层面的投资决策 + +### 7.1 保留并继续投资 + +- `claude_code/engine/query.py` +- `claude_code/tools/registry.py` +- `claude_code/api/client.py` +- `claude_code/tasks/manager.py` +- `claude_code/tasks/repository.py` +- `claude_code/mcp/server.py` +- `claude_code/services/mcp/client.py` +- `claude_code/context/builder.py` + +### 7.2 需要重构并接入主链 + +- `session / history / memory` +- `permission UX` +- `hooks` +- `agent registry` +- `config / auth` +- `analytics / telemetry` + +### 7.3 需要谨慎重写或降级处理 + +- `commands/review` +- `commands/auth` +- `tools/team` +- `tools/worktree` +- `features` +- deprecated `plugins` +- `remote.py` + +## 8. 90 天落地路线图 + +### Phase 0:止血与收敛(0-30 天) + +目标: + +把“看起来像产品”的部分变成“真的能稳定跑”的部分。 + +关键动作: + +1. 建立唯一主运行时入口 + - `main -> Application -> QueryEngine -> Session -> TaskManager -> Hooks -> Memory` +2. 修复假闭环功能 + - `/resume` + - `/review` + - `/auth` + - `features` +3. 清理孤岛子系统 + - 统一 history/session/memory 的职责边界 +4. 补关键 E2E + - CLI smoke + - session resume + - auth/config + - hooks execution + - team/worktree +5. 做本地分发稳定化 + - 至少解决 Windows 解释器探测与启动器问题 + +验收标准: + +- `python -m claude_code.main --help` / REPL / pipe / single query / MCP serve 稳定可跑 +- `/resume` 真正恢复历史消息 +- `/review` 能对 git diff 或指定文件执行真实审查 +- `auth` 命令链无运行时异常 +- 针对 P0/P1 功能新增契约测试 + +### Phase 1:平台化(30-60 天) + +目标: + +把单进程 CLI 演进成可被多个前端复用的执行后端。 + +关键动作: + +1. 引入 daemon / API 层 + - CLI 变成 thin client +2. 建立 event journal + - prompt + - message + - tool call + - permission decision + - task state transition + - artifact / patch / diff +3. 落地 SQLite 持久层 + - 先于 Redis,优先解决单机场景的恢复、检索、审计 +4. 让 `SessionMemory` 成为 active memory + - 与 session/context builder/runtime 真正联动 +5. 将 hooks 接入统一 runtime + - pre-tool / post-tool / on-error / on-exit + +验收标准: + +- CLI 与 API 指向同一执行核心 +- 任一会话支持恢复、回放、检索 +- 任务状态与执行事件可落盘 +- memory 与 hooks 真正影响运行时行为 + +### Phase 2:超车能力(60-90 天) + +目标: + +建立官方终端工具之外的产品护城河。 + +关键动作: + +1. 多 Agent supervisor + - agent budget + - dependency DAG + - artifact passing + - conflict resolution + - merge policy +2. IDE 集成 + - 先 VS Code + - 后 JetBrains +3. GitHub / CI 工作流 + - issue -> plan -> code -> test -> review -> PR +4. 组织级治理 + - policy + - approval queue + - audit log + - secret redaction + - execution reports + +验收标准: + +- 可在 IDE 中复用 daemon 能力 +- 可对 PR / issue / branch 执行 agent 工作流 +- 运行记录具备组织级审计价值 + +## 9. 当前最值得先砍的 backlog + +以下条目建议作为最近一轮实施入口: + +1. `resume` 主链闭环 +2. `auth` 命令修复与配置统一 +3. feature flag 设计修复 +4. `review` 从 prompt placeholder 升级为真实审查命令 +5. session/history/memory 统一模型 +6. task event journal 雏形 +7. hooks 接入 `QueryEngine` / `TaskManager` +8. Windows 启动器与解释器探测 + +## 10. 建议的衡量指标 + +后续迭代不应只看“测试通过”,建议引入以下指标: + +### 10.1 产品指标 + +- CLI 启动成功率 +- 会话恢复成功率 +- tool call 成功率 +- background task 完成率 +- permission deny / allow / ask 分布 + +### 10.2 工程指标 + +- P95 / P99 首次响应耗时 +- P95 / P99 tool execution latency +- event journal 写入成功率 +- SQLite / runtime repository 恢复成功率 +- regression E2E 通过率 + +### 10.3 平台指标 + +- agent fan-out 数量 +- artifact 交付成功率 +- PR 生成成功率 +- 审计记录覆盖率 + +## 11. 外部公开基线 + +以下公开文档可作为产品能力面的参考基线: + +- Overview: +- CLI reference: +- Settings: +- Memory: +- Subagents: +- Hooks: +- IDE integrations: +- GitHub Actions: + +说明: + +- 以上链接仅用于界定公开产品能力边界 +- 本项目的目标不应是逐项复制,而应是基于现有 Python runtime 优势重新定战略 + +## 12. 后续配套文档建议 + +为便于下一阶段落地,建议基于本文继续拆出以下文档: + +1. `docs/current/architecture/EXECUTION_PLATFORM_TARGET_ARCHITECTURE_*.md` + - 目标架构图、模块边界、数据流 +2. `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` + - 已落地:执行计划、阶段顺序、任务卡单、测试深度要求 +3. `docs/current/architecture/AGENT_CONTROL_PLANE_DESIGN_*.md` + - supervisor、多 agent、budget、artifact flow +4. `docs/current/architecture/RUNTIME_EVENT_JOURNAL_DESIGN_*.md` + - 事件模型、存储模型、回放模型 + +## 13. 最终判定 + +当前项目已经证明: + +- 它不只是一个命令行聊天壳 +- 它已经拥有可演化为平台的底盘 +- 它最强的机会不在“做一个更像 Claude Code 的 CLI” +- 它最强的机会在“做一个比 Claude Code 更可治理、更可恢复、更可协作的执行平台” + +因此,后续一切实施都应围绕这一判断展开: + +> 先收敛主运行时,再平台化,再做多入口与组织级治理,最后以多 Agent 执行能力完成超车。 diff --git a/docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md b/docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md new file mode 100644 index 0000000..6d245c9 --- /dev/null +++ b/docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md @@ -0,0 +1,169 @@ +# Claude Code Python 鎵ц璁″垝涓庝换鍔″崱鍗曪紙2026-04-29锛?## 1. 鏂囨。瀹氫綅- 鏂囨。鎬ц川锛氭墽琛岃鍒?+ 浠诲姟鍗″崟- 涓婃父鏂囨。锛歔CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md](D:/Download/gaming/new_program/claude-code-python/docs/current/architecture/CLAUDE_CODE_SUPERSEDE_STRATEGY_2026-04-29.md)- 浣跨敤鏂瑰紡锛? - 浜у搧銆佹灦鏋勩€佺爺鍙戙€佹祴璇曢兘浠ユ湰鏂囦欢浣滀负杩戞湡瀹炴柦娓呭崟 - 姣忓紶浠诲姟鍗″繀椤昏兘鐙珛寤哄垎鏀€佺嫭绔嬪紑鍙戙€佺嫭绔嬮獙璇併€佺嫭绔嬪悎骞? - 鑻ヤ竴寮犲崱鏃犳硶鍦ㄤ竴涓槑纭竟鐣屽唴瀹屾垚锛屽垯缁х画鍚戜笅鎷?## 2. 褰撳墠鎷嗚В鍘熷垯### 2.1 鎷嗗崱鍘熷垯姣忓紶鍗″繀椤诲悓鏃跺洖绛?5 涓棶棰橈細1. 瑙e喅浠€涔堥棶棰橈紵2. 瑕佸仛浠€涔堢爺鍙戝姛鑳斤紵3. 瑕佸仛鍒颁粈涔堢▼搴︽墠绠楀畬鎴愶紵4. 娴嬭瘯浠g爜瑕佽ˉ鍒颁粈涔堢▼搴︼紵5. 鏈変粈涔堝墠缃緷璧栧拰鏄庣‘闈炵洰鏍囷紵### 2.2 瀹屾垚瀹氫箟锛圖efinition of Done锛?姣忓紶鍗¢粯璁ゅ畬鎴愭爣鍑嗭細- 浠g爜涓婚摼鍙繍琛?- 鏂板鎴栦慨鏀规祴璇曢€氳繃- 瀵瑰簲鏂囨。鍚屾鏇存柊- 涓嶅紩鍏ユ柊鐨勬湭瑙i噴鑴忕姸鎬?- 鑷冲皯瀹屾垚涓€杞湰鍦版墜宸ラ獙鏀?### 2.3 娴嬭瘯鍒嗗眰鏍囧噯涓洪伩鍏嶁€滃啓浜嗙偣娴嬭瘯鈥濅絾娌℃湁楠屾敹浠峰€硷紝缁熶竴閲囩敤浠ヤ笅娣卞害鍒嗗眰锛?| 灞傜骇 | 鍚嶇О | 鐩殑 | 鏈€浣庤姹?||---|---|---|---|| T1 | Unit | 绾嚱鏁般€佺姸鎬佹満銆佽緭鍏ヨ緭鍑鸿竟鐣?| 姝e悜 + 璐熷悜 + 杈圭晫 || T2 | Component | 鍗曟ā鍧楃紪鎺掋€佸甫 fake/mocks 鐨勮涓洪獙璇?| 鑷冲皯瑕嗙洊 1 鏉′富閾?+ 1 鏉″紓甯搁摼 || T3 | Integration | 璺ㄦā鍧楄仈鍔紝楠岃瘉鐪熷疄渚濊禆杈圭晫 | 鑷冲皯瑕嗙洊 1 鏉$鍒扮涓氬姟閾?|| T4 | Regression | 澶嶇幇鍘嗗彶缂洪櫡锛岄槻姝㈠洖褰?| 姣忎釜宸茬煡 bug 鑷冲皯 1 鏉″洖褰掔敤渚?|| T5 | Manual Smoke | 浜哄伐蹇€熼獙鏀?| 鍛戒护绾?鍏ュ彛绾у疄闄呰繍琛?|### 2.4 娴嬭瘯浜х墿瑕佹眰姣忓紶浠g爜鍗¤嚦灏戞弧瓒筹細- 鏂板鎴栦慨鏀?`T1/T2` 娴嬭瘯- 鑻ュ彉鏇村奖鍝?CLI銆乻ession銆乼ask銆丮CP銆乭ooks銆乤gent 涓婚摼锛屽垯蹇呴』琛?`T3`- 鑻ヤ换鍔″崱婧愪簬宸茬煡缂洪櫡锛屽垯蹇呴』琛?`T4`- 杈撳嚭 1 浠芥渶灏忔墜宸ラ獙鏀跺懡浠ゆ竻鍗?## 3. 瀹炴柦闃舵鎬昏## 3.1 Phase 0锛氭琛€涓庢敹鏁?鐩爣锛?- 淇帀鈥滅湅璧锋潵鏈夈€佸疄闄呮病鎵撻€氣€濈殑鍋囬棴鐜?- 寤虹珛鍞竴涓昏繍琛屾椂鍏ュ彛- 寤虹珛绋冲畾鐨勬湰鍦板紑鍙?娴嬭瘯鏂藉伐闈?## 3.2 Phase 1锛氬钩鍙板寲鐩爣锛?- 浠庡崟杩涚▼ CLI 婕旇繘涓哄彲澶嶇敤鐨勬墽琛屽悗绔?- 琛ラ綈 event journal銆乻tate backend銆乤ctive memory## 3.3 Phase 2锛氳秴杞?鐩爣锛?- 寤虹珛澶?Agent 鎺у埗闈€両DE 瀹㈡埛绔€丆I 宸ヤ綔娴併€佺粍缁囩骇娌荤悊鑳藉姏## 4. 渚濊禆椤哄簭鍥?```textP0-01 Runtime Backbone -> P0-02 Session Resume -> P0-03 Auth/Config -> P0-04 Feature Flags -> P0-05 Review Command -> P0-06 Session/History/Memory Boundaries -> P0-07 Hooks Runtime -> P0-08 Windows Bootstrap -> P0-09 Temp Artifact Hygiene -> P0-10 Phase0 Regression GateP1-01 Daemon/API Control Plane -> P1-02 Event Journal -> P1-03 SQLite State Backend -> P1-04 Active Memory Runtime -> P1-05 Hook Policy/Audit Convergence -> P1-06 CLI Thin Client MigrationP2-01 Agent Supervisor -> P2-02 Artifact Bus -> P2-03 IDE Integration -> P2-04 GitHub/CI Workflow -> P2-05 Org Policy & Audit -> P2-06 Linux Unified Verification Gate -> P2-07 Agent Runtime Parity -> P2-08 Custom Agents Directory Loader -> P2-09 Linux Unified Execution Gate -> P2-10 JetBrains IDE Integration -> P2-11 Linux Sharded Execution Gate -> P2-12 Linux Shard Aggregation Gate -> P2-13 Linux Final Report Publish Gate -> P2-14 Linux Unified Pipeline Orchestration Gate -> P2-15 Linux Shard Plan Generation Gate -> P2-16 Linux CI Matrix Export Gate -> P2-17 Linux CI Workflow Plan Gate -> P2-18 Linux CI Workflow YAML Render Gate -> P2-19 Linux CI Workflow Drift Sync Gate -> P2-20 Linux CI Workflow Command Guard Gate -> P2-21 Linux CI Workflow Governance Convergence Gate -> P2-22 Linux CI Workflow Governance Publish Gate -> P2-23 Linux CI Workflow Execution Decision Gate -> P2-24 Linux CI Workflow Dispatch Readiness Gate -> P2-25 Linux CI Workflow Dispatch Execution Gate -> P2-26 Linux CI Workflow Full Pipeline Gate -> P2-27 Linux CI Workflow Dispatch Traceability Gate -> P2-28 Linux CI Workflow Dispatch Completion Gate -> P2-29 Linux CI Workflow Terminal Publish Gate -> P2-30 Linux CI Workflow Release Handoff Gate -> P2-31 Linux CI Workflow Release Trigger Gate -> P2-32 Linux CI Workflow Release Traceability Gate -> P2-33 Linux CI Workflow Release Completion Gate -> P2-34 Linux CI Workflow Release Terminal Publish Gate -> P2-35 Linux CI Workflow Release Finalization Gate -> P2-36 Linux CI Workflow Release Closure Publish Gate -> P2-37 Linux CI Workflow Release Archive Publish Gate -> P2-38 Linux CI Workflow Release Verdict Publish Gate -> P2-39 Linux CI Workflow Release Incident Dispatch Gate -> P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate -> P2-41 Linux CI Workflow Release Delivery Closure Gate -> P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate -> P2-43 Linux CI Workflow Release Delivery Final Verdict Gate -> P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate -> P2-45 Linux CI Workflow Release Follow-Up Closure Gate -> P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate -> P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate -> P2-48 Linux CI Workflow Release Final Outcome Gate -> P2-49 Linux CI Workflow Release Final Terminal Publish Gate -> P2-50 Linux CI Workflow Release Final Handoff Gate -> P2-51 Linux CI Workflow Release Final Closure Gate -> P2-52 Linux CI Workflow Release Final Closure Publish Gate -> P2-53 Linux CI Workflow Release Final Archive Gate -> P2-54 Linux CI Workflow Release Final Verdict Gate -> P2-55 Linux CI Workflow Release Final Verdict Publish Gate -> P2-56 Linux CI Workflow Release Final Publish Archive Gate -> P2-57 Linux Gate Manifest Drift Closure Gate -> P2-58 Linux CI Workflow Pipeline Closure to P2-57 Gate -> P2-59 Linux CI Workflow Terminal Verdict Closure Gate -> P2-60 Linux CI Workflow Linux Validation Dispatch Gate -> P2-61 Linux CI Workflow Linux Validation Verdict Gate -> P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate -> P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate -> P2-64 Linux CI Workflow Linux Validation Final Verdict Gate -> P2-65 Linux CI Workflow Linux Validation Final Verdict Publish Gate -> P2-66 Linux CI Workflow Linux Validation Final Publish Archive Gate -> P2-67 Linux CI Workflow Linux Validation Terminal Verdict Gate -> P2-68 Linux CI Workflow Linux Validation Terminal Verdict Publish Gate -> P2-69 Linux CI Workflow Linux Validation Terminal Dispatch Gate -> P2-70 Linux CI Workflow Linux Validation Terminal Dispatch Execution Gate -> P2-71 Linux CI Workflow Linux Validation Terminal Dispatch Trace Gate -> P2-72 Linux CI Workflow Linux Validation Terminal Dispatch Completion Gate -> P2-73 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Publish Gate -> P2-74 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Gate -> P2-75 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Publish Gate -> P2-76 Linux CI Workflow Linux Validation Terminal Dispatch Final Publish Archive Gate -> P2-77 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Gate -> P2-78 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Gate -> P2-79 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Archive Gate```## 5. Phase 0 浠诲姟鍗″崟## P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦- 鍗″彿锛歚P0-01`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `main.py`銆乣app.py`銆乻ession/task/hooks/memory 鍒濆鍖栭摼鏉″垎鏁? - 瀛愮郴缁熷瓨鍦ㄤ絾鏈舰鎴愬敮涓€杩愯鏃堕棴鐜?- 鐮斿彂鍔熻兘锛? - 寤虹珛缁熶竴 runtime bootstrap - 鏄庣‘ `main -> Application -> QueryEngine -> Session -> TaskManager -> Hooks -> Memory` - 娑堥櫎閲嶅鍒濆鍖栬矾寰勪笌闅愬紡鍏ㄥ眬鐘舵€佷緷璧?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` - `claude_code/app.py` - `claude_code/engine/query.py` - 瑙嗛渶瑕佹柊澧?`claude_code/runtime/` 鎴栫瓑浠锋ā鍧?- 瀹屾垚绋嬪害锛? - REPL銆乻ingle query銆乸ipe銆丮CP serve 鍏辩敤鍚屼竴 bootstrap 娴? - runtime 鍏抽敭瀵硅薄鏈夊敮涓€鏋勫缓璺緞 - 鍚庣画鍗″崟涓嶅啀渚濊禆鈥滃悇鑷伔鍋峰垵濮嬪寲鈥?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歜ootstrap 鍙傛暟瑙f瀽銆乺untime object 鏋勫缓杈圭晫 - `T2`锛氫笉鍚岃繍琛屾ā寮忎笅 runtime 缁勪欢瑁呴厤涓€鑷存€? - `T3`锛歊EPL / pipe / single query 鑷冲皯鍚勬湁 1 鏉′富閾?smoke - `T4`锛氳鐩栤€滄煇妯″紡婕忓垵濮嬪寲鏌愭湇鍔♀€濈殑鍘嗗彶鏂偣- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_runtime_bootstrap.py` - 鎵╁睍 `tests/test_main_runtime.py`- 鎵嬪伐楠屾敹锛? - `python -m claude_code.main --help` - `python -m claude_code.main --doctor` - `echo hi | python -m claude_code.main --pipe`- 闈炵洰鏍囷細 - 鏈崱涓嶅紩鍏?daemon/API - 鏈崱涓嶅鐞嗙粍缁囩骇绛栫暐- 鍓嶇疆渚濊禆锛氭棤## P0-02 /resume 鐪熼棴鐜?- 鍗″彿锛歚P0-02`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `QueryEngine.resume_session()` 宸插瓨鍦紝浣?`/resume` 鍛戒护娌℃湁鐪熸璋冪敤鎭㈠閾捐矾 - session 鎭㈠鏄€滄枃妗堝姛鑳解€濓紝涓嶆槸鈥滆繍琛屾椂鍔熻兘鈥?- 鐮斿彂鍔熻兘锛? - 灏?`/resume [session_id]` 鐪熸鎺ュ叆 `QueryEngine.resume_session()` - 鏃犲弬鏁版椂鍒楁渶杩?session锛涙湁鍙傛暟鏃舵仮澶嶆寚瀹?session 骞舵帴缁璇? - 鏄庣‘鎭㈠鍚庣殑 session id銆乵essage list銆亀orking directory 琛屼负- 瀹炴柦鑼冨洿锛? - `claude_code/commands/compact/__init__.py` - `claude_code/engine/query.py` - `claude_code/engine/session.py` - `claude_code/repl/__init__.py`- 瀹屾垚绋嬪害锛? - `/resume` 涓嶅啀鍙繑鍥炶鏄庢枃瀛? - 鎭㈠鍚庝笅涓€杞?query 鑳芥帴鐫€鍘嗗彶娑堟伅杩愯 - 鎭㈠澶辫触鏃舵湁鍙瘖鏂敊璇俊鎭?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ession list / load 杈圭晫 - `T2`锛歝ommand -> engine 鎭㈠閾捐矾 - `T3`锛氬垱寤?session銆佹寔涔呭寲銆佹仮澶嶃€佺户缁棶绛旂殑涓€鏉″畬鏁撮摼 - `T4`锛氭棤 session / session_id 涓嶅瓨鍦?/ 浼氳瘽鏂囦欢鎹熷潖- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_query_engine_runtime.py` - 鎵╁睍 `tests/test_main_runtime.py` - 鏂板 `tests/test_resume_runtime.py`- 鎵嬪伐楠屾敹锛? - 鍚姩涓€娆′細璇濆苟鐢熸垚鎸佷箙鍖? - 鏂拌繘绋嬫墽琛?`/resume` - 纭鎭㈠鍚庣户缁璇?- 闈炵洰鏍囷細 - 涓嶅紩鍏ヨ法鏈哄櫒鎭㈠ - 涓嶅紩鍏?session 鎼滅储 UI- 鍓嶇疆渚濊禆锛歚P0-01`## P0-03 auth/config 涓婚摼淇涓庣粺涓€- 鍗″彿锛歚P0-03`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `logout` / `auth` 浣跨敤鏈鍏ョ殑 `ConfigManager` - `Config` 涓?`ConfigManager` 鍙岃建骞跺瓨锛岃璇佺姸鎬佸垽瀹氫笉绋冲畾- 鐮斿彂鍔熻兘锛? - 閫夊畾 canonical config 璇诲啓璺緞 - 淇 `/login`銆乣/logout`銆乣/auth` - 缁熶竴 env/config/key masking 瑙勫垯- 瀹炴柦鑼冨洿锛? - `claude_code/commands/auth/__init__.py` - `claude_code/config.py` - `claude_code/utils/config_manager.py` - 瑙嗛渶瑕佽皟鏁?`claude_code/main.py`- 瀹屾垚绋嬪害锛? - `auth` 閾捐矾鏃?`NameError` - 璁よ瘉鐘舵€佽緭鍑轰笌瀹為檯 env/config 涓€鑷? - logout 鑳芥竻鐞?runtime 鍙鐨?key 鏉ユ簮- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nv > config > default 浼樺厛绾? - `T2`锛歛uth commands 鍦ㄤ笉鍚?key 鏉ユ簮涓嬬殑琛屼负 - `T3`锛氬懡浠ょ骇鐘舵€佸彉鍖栭獙璇? - `T4`锛氭棤 key / config 缂哄け / key mask 杈撳嚭- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_commands_auth_runtime.py` - 鎵╁睍 `tests/test_main_runtime.py`- 鎵嬪伐楠屾敹锛? - 璁剧疆 env key - 杩愯 `/auth` - 杩愯 `/logout` - 鍐嶆杩愯 `/auth`- 闈炵洰鏍囷細 - 涓嶅紩鍏?OAuth 鐪熺櫥褰曟祦 - 涓嶆帴绗笁鏂?secrets manager- 鍓嶇疆渚濊禆锛歚P0-01`## P0-04 feature flag 杩愯鏃朵慨澶?- 鍗″彿锛歚P0-04`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `FeatureConfig` 涓?frozen dataclass锛屼絾 `enable()/disable()` 璇曞浘鍘熷湴淇敼 - 褰撳墠 flag 鏈哄埗瀵规湭鏉ュぇ鍔熻兘鍒囨崲涓嶅彲闈?- 鐮斿彂鍔熻兘锛? - 閲嶆瀯 feature registry 鐨勫唴閮ㄧ姸鎬佽〃杈? - 淇濇寔 env fallback锛屽悓鏃跺厑璁哥▼搴忓唴瀹夊叏鍒囨崲 - 鏄庣‘ feature 璇诲彇涓庡啓鍏ヨ涔?- 瀹炴柦鑼冨洿锛? - `claude_code/features.py`- 瀹屾垚绋嬪害锛? - `enable()` / `disable()` 涓嶆姏 `FrozenInstanceError` - registry 鍐呭琛屼负涓€鑷? - flags 鍙綔涓哄悗缁ぇ鍗$殑 gated rollout 鏈哄埗- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟nable / disable / env override / unknown feature - `T2`锛歳egistry 瀹炰緥鍦ㄥ娆¤皟鐢ㄤ腑鐨勭姸鎬佷竴鑷存€? - `T4`锛氬鐜板綋鍓?`FrozenInstanceError` 鐨勫洖褰掔敤渚?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_features_runtime.py`- 鎵嬪伐楠屾敹锛? - 鏈湴鑴氭湰鍒囨崲 feature 骞惰鍙?- 闈炵洰鏍囷細 - 涓嶅紩鍏ヨ繙绋?flag service- 鍓嶇疆渚濊禆锛氭棤## P0-05 review 鍛戒护浜у搧鍖?- 鍗″彿锛歚P0-05`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `/review` 鍙槸寮曞鏂囨锛屼笉鎵ц鐪熷疄瀹℃煡 - 鐢ㄦ埛鏃犳硶渚濊禆璇ュ懡浠ゆ鏌?diff / 鏂囦欢椋庨櫓- 鐮斿彂鍔熻兘锛? - 鏀寔 review 褰撳墠 git diff - 鏀寔 review 鎸囧畾鏂囦欢 - 缁熶竴 findings 杈撳嚭鏍煎紡锛氶棶棰樸€佷綅缃€佷弗閲嶅害銆佸缓璁? - 鏄庣‘鈥滄棤鍙戠幇鈥濊緭鍑?- 瀹炴柦鑼冨洿锛? - `claude_code/commands/review/__init__.py` - 瑙嗛渶瑕佹柊澧?`claude_code/services/review_service.py` - 鍙帴鍏?`git diff` / read-only analyzer- 瀹屾垚绋嬪害锛? - `/review` 涓嶅啀杩斿洖鍗犱綅 prompt - 鑳藉褰撳墠鏀瑰姩鎴栬緭鍏ユ枃浠剁粰鍑虹粨鏋勫寲缁撴灉 - 杈撳嚭鍙綔涓哄悗缁?PR/CI review 鐨勯洀褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬弬鏁拌В鏋愩€佹棤杈撳叆榛樿琛屼负 - `T2`锛歴ervice 瀵?fake diff 鐨?findings 鐢熸垚 - `T3`锛氫复鏃?git repo 涓殑 review 涓婚摼 - `T4`锛氭棤 diff / 鏂囦欢涓嶅瓨鍦?/ 绌虹粨鏋?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_review_command_runtime.py`- 鎵嬪伐楠屾敹锛? - 淇敼涓€涓枃浠跺悗鎵ц `/review` - 鎸囧畾鏂囦欢鎵ц `/review path`- 闈炵洰鏍囷細 - 涓嶅仛缁勭粐绾?code review dashboard - 涓嶅己琛屾帴 GitHub API- 鍓嶇疆渚濊禆锛歚P0-01`## P0-06 session/history/memory 鑱岃矗鏀舵暃- 鍗″彿锛歚P0-06`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - `Session`銆乣HistoryManager`銆乣SessionMemory` 鑱岃矗浜ゅ弶 - 鐭湡瀵硅瘽銆侀暱鏈熻蹇嗐€佸綊妗e巻鍙茶竟鐣屼笉娓?- 鐮斿彂鍔熻兘锛? - 鏄庣‘涓夊眰鑱岃矗锛? - `Session`锛氬綋鍓嶄細璇濅簨瀹? - `History`锛氬凡缁撴潫/宸蹭繚瀛樼殑娑堟伅褰掓。 - `Memory`锛氶暱鏈熺粨鏋勫寲鐭ヨ瘑 - 娑堥櫎閲嶅淇濆瓨涓庝笉涓€鑷村姞杞借矾寰? - 涓?P1 active memory 閾哄簳- 瀹炴柦鑼冨洿锛? - `claude_code/engine/session.py` - `claude_code/services/history_manager.py` - `claude_code/services/memory_service.py` - `claude_code/context/builder.py`- 瀹屾垚绋嬪害锛? - 涓夎€呰亴璐d笌璋冪敤杈圭晫鍐欐竻 - 褰撳墠涓婚摼鍙繚鐣欎竴涓?message persistence source of truth - memory 涓嶅啀闅愬紡鎵挎媴 session/history 瑙掕壊- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ave/load/export/search 杈圭晫 - `T2`锛歴ession -> persisted session 琛屼负 - `T3`锛歝ontext builder 璇诲彇 memory/session 鐨勮仈鍔? - `T4`锛氶槻姝㈤噸澶嶆秷鎭€侀噸澶嶆寔涔呭寲銆侀敊璇仮澶?- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_session_history_memory_runtime.py` - 鎵╁睍 `tests/test_context_builder_runtime.py`- 鎵嬪伐楠屾敹锛? - 鐢熸垚浼氳瘽 - 閲嶅惎鍔犺浇 - 楠岃瘉 memory 涓嶈璇啓涓鸿亰澶╁巻鍙?- 闈炵洰鏍囷細 - 涓嶅仛鍚戦噺鏁版嵁搴? - 涓嶅仛闀挎湡妫€绱㈠寮?- 鍓嶇疆渚濊禆锛歚P0-01`## P0-07 hooks 鎺ュ叆涓昏繍琛屾椂- 鍗″彿锛歚P0-07`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - hooks command銆乭ooks manager銆乼ool/command 鎵ц閾炬病鏈夌湡姝h仈鍔? - hooks 鐩墠鏇村儚闈欐€侀厤缃紝鑰屼笉鏄繍琛屾椂鑳藉姏- 鐮斿彂鍔熻兘锛? - 鍦?tool execution 鍓嶅悗瑙﹀彂 hooks - 鍦?command 鎵ц鍓嶅悗瑙﹀彂 hooks - 鍦ㄩ敊璇矾寰勮Е鍙?`on_error` - 绾﹀畾 hook context payload- 瀹炴柦鑼冨洿锛? - `claude_code/services/hooks_manager.py` - `claude_code/commands/hooks/__init__.py` - `claude_code/engine/query.py` - `claude_code/repl/__init__.py` - 瑙嗛渶瑕佹帴鍏?command dispatch 灞?- 瀹屾垚绋嬪害锛? - hooks 閰嶇疆鑳界湡瀹炵敓鏁? - disabled hook銆佽秴鏃?hook銆佸け璐?hook 琛屼负娓呮櫚 - 瀵逛富閾惧け璐ヤ笉浜х敓涓嶅彲鎺у壇浣滅敤- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歨ook load/save/parse - `T2`锛歵rigger before_tool / after_tool / on_error - `T3`锛氱湡瀹?tool 璋冪敤閾捐Е鍙?hook - `T4`锛歨ook timeout / hook command failure / disabled hook- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_hooks_runtime.py`- 鎵嬪伐楠屾敹锛? - 閰嶇疆涓€涓畝鍗?echo hook - 鎵ц read / grep / bash - 纭 hook 琚Е鍙?- 闈炵洰鏍囷細 - 涓嶅仛 org-wide hook registry- 鍓嶇疆渚濊禆锛歚P0-01`## P0-08 Windows 鍚姩涓庤В閲婂櫒鎺㈡祴绋冲畾鍖?- 鍗″彿锛歚P0-08`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - 绯荤粺 `python` 鎸囧悜 WindowsApps stub锛岀敤鎴峰鏄撹窇鍒板亣瑙i噴鍣? - Windows 鐢ㄦ埛鍚姩閾捐剢寮?- 鐮斿彂鍔熻兘锛? - 鍦?`--doctor` 涓瘑鍒В閲婂櫒鏉ユ簮 - 瀵?WindowsApps stub 缁欏嚭鏄庣‘璀﹀憡 - 缁熶竴鎺ㄨ崘鍚姩鍣?/ wrapper 琛屼负 - 鏂囨。鍚屾鏄庣‘ Windows 鎺ㄨ崘杩愯娉?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` - `README.md` - 鍙€夋柊澧?`scripts/` 涓嬭緟鍔╁惎鍔ㄨ剼鏈?- 瀹屾垚绋嬪害锛? - doctor 杈撳嚭鑳借瘖鏂В閲婂櫒椋庨櫓 - Windows 鐢ㄦ埛涓嶄細璇互涓?stub 鍙敤 - 椤圭洰鍏ュ彛鏂囨。娓呮鏍囨敞鎺ㄨ崘瑙i噴鍣?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氳В閲婂櫒璺緞璇嗗埆閫昏緫 - `T2`锛歞octor 杈撳嚭鍒嗘敮 - `T4`锛歐indowsApps stub 鍥炲綊鐢ㄤ緥- 寤鸿娴嬭瘯鏂囦欢锛? - 鏂板 `tests/test_doctor_runtime.py`- 鎵嬪伐楠屾敹锛? - 妯℃嫙 / 妫€鏌?doctor 杈撳嚭- 闈炵洰鏍囷細 - 涓嶅仛瀹屾暣瀹夎鍣?- 鍓嶇疆渚濊禆锛歚P0-01`## P0-09 娴嬭瘯涓存椂浜х墿娓呯悊涓庨殧绂?- 鍗″彿锛歚P0-09`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - `tests/.tmp_plugins/` 瀛樺湪娈嬬暀鐩綍 - 娴嬭瘯闅旂鎬т笌浠撳簱鏁存磥鎬т笉瓒?- 鐮斿彂鍔熻兘锛? - 淇 plugin 娴嬭瘯鐨勬竻鐞嗛€昏緫 - 澧炶ˉ `.gitignore` 鎴栨祴璇?teardown - 纭繚 pytest 缁撴潫鍚庝笉鏂板鑴忕洰褰?- 瀹炴柦鑼冨洿锛? - `tests/test_plugins_runtime.py` - `.gitignore` - 瑙嗛渶瑕佽皟鏁?plugin fixture- 瀹屾垚绋嬪害锛? - 鏈湴瀹屾暣 pytest 鍚庝笉鍐嶆畫鐣欐柊澧?`.tmp_*` / `.tmp_plugins/*` - 娴嬭瘯浜掍笉姹℃煋- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歠ixture cleanup 鏂█ - `T4`锛氶噸澶嶈繍琛?pytest 涓嶇疮璁℃畫鐣?- 寤鸿娴嬭瘯鏂囦欢锛? - 鎵╁睍 `tests/test_plugins_runtime.py`- 鎵嬪伐楠屾敹锛? - 杩炵画璺戜袱娆?pytest - 妫€鏌ュ伐浣滃尯鑴忔枃浠?- 闈炵洰鏍囷細 - 涓嶆竻鐞嗙敤鎴风湡瀹炶繍琛屾暟鎹?- 鍓嶇疆渚濊禆锛氭棤## P0-10 Phase 0 鍥炲綊闂ㄧ- 鍗″彿锛歚P0-10`- 浼樺厛绾э細`P0`- 瑙e喅闂锛? - 鐜版湁娴嬭瘯鍒嗗竷杈冩暎锛屾病鏈夊洿缁?Phase 0 涓婚摼寤虹珛鏄庣‘闂ㄧ- 鐮斿彂鍔熻兘锛? - 瀹氫箟涓€缁勫繀椤婚€氳繃鐨?runtime regression tests - 澧炲姞鎸変富閾惧垎缁勭殑 pytest marker 鎴栬剼鏈叆鍙? - 鏄庣‘鎻愪氦鍓嶅繀璺戝懡浠?- 瀹炴柦鑼冨洿锛? - `pytest.ini` - `scripts/` 涓嬫牎楠岃剼鏈紙濡傞渶瑕侊級 - 娴嬭瘯鏂囦欢鏍囪鏁寸悊- 瀹屾垚绋嬪害锛? - 鑳戒竴鏉″懡浠ゆ墽琛?P0 涓婚摼鍥炲綊 - 鍥㈤槦鐭ラ亾鈥滃紑宸ュ墠/鍚堝苟鍓嶅繀椤昏窇浠€涔堚€?- 娴嬭瘯浠g爜瑕佹眰锛? - 鏈崱鏈川涓婃槸娴嬭瘯缁勭粐鍗? - 闇€瑕佽ˉ 1 涓祴璇曞叆鍙h剼鏈垨 marker 楠岃瘉- 寤鸿娴嬭瘯鏂囦欢锛? - 濡傞渶鏂板锛歚tests/test_phase0_regression_contract.py`- 鎵嬪伐楠屾敹锛? - 鎵ц涓€鏉?phase0 gate 鍛戒护骞堕獙璇侀€氳繃- 闈炵洰鏍囷細 - 涓嶈姹傛鏃舵帴 CI SaaS- 鍓嶇疆渚濊禆锛? - `P0-02` ~ `P0-09`## 6. Phase 1 浠诲姟鍗″崟## P1-01 daemon / API 鎺у埗闈?- 鍗″彿锛歚P1-01`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - 褰撳墠 CLI 鏄帤瀹㈡埛绔紝IDE/desktop/web/CI 鏃犳硶鍏辩敤鎵ц鍚庣- 鐮斿彂鍔熻兘锛? - 鎻愪緵鏈湴 daemon/API 灞? - 鎶界 session銆乼ask銆乼ool execution銆乤rtifact 璁块棶鎺ュ彛 - CLI 鏀逛负 daemon client 鐨勪笂灞傚叆鍙?- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/server/` 鎴栫瓑浠锋帶鍒堕潰妯″潡 - 璋冩暣 `main.py` - 澶嶇敤 `app.py`- 瀹屾垚绋嬪害锛? - 鍗曟満鍙惎鍔?daemon - CLI 涓?daemon 鍙叡浜竴濂楁墽琛屾牳蹇?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欰PI schema / request validation - `T2`锛歴erver service 灞傝涓? - `T3`锛歝lient -> server -> runtime 涓€鏉″畬鏁撮摼 - `T4`锛歞aemon 鏈惎鍔?/ 瓒呮椂 / 浼氳瘽涓嶅瓨鍦?- 鍓嶇疆渚濊禆锛歚P0-01`銆乣P0-10`## P1-02 event journal 浜嬩欢鏃ュ織- 鍗″彿锛歚P1-02`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - prompt銆乼ool銆乼ask銆乸ermission 鍐崇瓥涓嶅彲鍥炴斁锛屼笉鍙璁?- 鐮斿彂鍔熻兘锛? - 瀹氫箟缁熶竴浜嬩欢妯″瀷 - 鍐欏叆 prompt/message/tool/task/permission/artifact 浜嬩欢 - 鎻愪緵鏌ヨ涓?replay 鍩虹鑳藉姏- 瀹炴柦鑼冨洿锛? - 鏂板 `claude_code/services/event_journal.py` 鎴栫瓑浠锋ā鍧? - 鎺ュ叆 `QueryEngine`銆乣TaskManager`- 瀹屾垚绋嬪害锛? - 鎵€鏈夊叧閿富閾惧姩浣滃彲浜х敓缁撴瀯鍖栦簨浠? - 鏀寔鎸?session 鏌ヨ浜嬩欢娴?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氫簨浠?schema銆佸簭鍒楀寲銆乿ersioning - `T2`锛歸riter/reader correctness - `T3`锛歲uery -> tool -> task 浜嬩欢閾? - `T4`锛氶儴鍒嗗啓鍏ュけ璐ャ€佷贡搴忔仮澶?- 鍓嶇疆渚濊禆锛歚P1-01`## P1-03 SQLite runtime state backend- 鍗″彿锛歚P1-03`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - file/memory backend 瀵规仮澶嶃€佹绱€佸瀹炰緥鍓嶅噯澶囦笉瓒?- 鐮斿彂鍔熻兘锛? - 涓?session/task/event 鎻愪緵 SQLite backend - 淇濈暀 file backend 浣滀负鍏煎灞傛垨 fallback - 鏄庣‘ migration 涓?schema version- 瀹炴柦鑼冨洿锛? - `claude_code/tasks/factory.py` - `claude_code/tasks/repository.py` - session/event 鐩稿叧瀛樺偍妯″潡- 瀹屾垚绋嬪害锛? - 鑷冲皯鍙敤 SQLite 鎸佷箙鍖?task/session/event - 鍗曟満閲嶅惎鍚庡彲鎭㈠鏍稿績杩愯鎬?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴chema init / migration - `T2`锛歳epository CRUD contract - `T3`锛氶噸鍚仮澶? - `T4`锛氭暟鎹簱鎹熷潖銆侀攣鍐茬獊銆侀噸澶嶅垵濮嬪寲- 鍓嶇疆渚濊禆锛歚P1-02`## P1-04 active memory 鎺ュ叆杩愯鏃?- 鍗″彿锛歚P1-04`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - `SessionMemory` 鐜板湪鏄绔?KV锛屼笉褰卞搷涓诲璇濅笂涓嬫枃- 鐮斿彂鍔熻兘锛? - 璁?memory 涓?context builder/runtime 鑱斿姩 - 瀹氫箟 memory scope锛歶ser/project/local - 鏀寔鏄惧紡鍐欏叆銆佹寜闇€娉ㄥ叆銆佸懡涓粺璁?- 瀹炴柦鑼冨洿锛? - `claude_code/services/memory_service.py` - `claude_code/context/builder.py` - `claude_code/agents/builtin.py`- 瀹屾垚绋嬪害锛? - memory 鎴愪负杩愯鏃剁湡瀹炶緭鍏ワ紝鑰岄潪鏃佽矾瀛樺偍 - agent / main query 鍙€夋嫨涓嶅悓 memory scope- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歯amespace / key / search 琛屼负 - `T2`锛歝ontext builder memory 娉ㄥ叆 - `T3`锛歲uery 鎵ц鏃跺懡涓?memory - `T4`锛氱┖ memory / 鎹熷潖 memory / scope 闅旂- 鍓嶇疆渚濊禆锛歚P0-06`銆乣P1-02`## P1-05 hooks / permission / audit 鏀舵暃- 鍗″彿锛歚P1-05`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - hooks銆乸ermissions銆乪vent journal 浠嶆槸鍒嗙珛鑳藉姏- 鐮斿彂鍔熻兘锛? - 缁熶竴 hook 浜嬩欢銆乸ermission decision銆乤udit event - 褰㈡垚涓€娆℃墽琛岀殑瀹屾暣瀹¤閾? - 涓烘湭鏉?org policy 閾哄簳- 瀹炴柦鑼冨洿锛? - `claude_code/permissions.py` - `claude_code/services/hooks_manager.py` - event journal 鐩稿叧妯″潡- 瀹屾垚绋嬪害锛? - 姣忔 tool call 鑷冲皯鍏峰 request -> decision -> hook -> result 鐨勯棴鐜褰?- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歱ermission + hook + event 鑱斿姩 - `T3`锛氱湡瀹?tool 鎵ц瀹¤閾? - `T4`锛歞eny / timeout / partial failure- 鍓嶇疆渚濊禆锛歚P0-07`銆乣P1-02`## P1-06 CLI thin client 杩佺Щ- 鍗″彿锛歚P1-06`- 浼樺厛绾э細`P1`- 瑙e喅闂锛? - CLI 涓庢帶鍒堕潰鑰﹀悎杩囨繁锛屼笉鍒╀簬 IDE/CI 鍏辩敤- 鐮斿彂鍔熻兘锛? - CLI 鍙戣姹傜粰鏈湴 runtime server/daemon - 淇濈暀鍗曡繘绋?fallback 妯″紡 - 缁熶竴杈撳嚭涓庨敊璇涔?- 瀹炴柦鑼冨洿锛? - `claude_code/main.py` - CLI/REPL client adapter- 瀹屾垚绋嬪害锛? - CLI 浣滀负 runtime 鍓嶇瀛樺湪锛岃€岄潪鎵ц鍐呮牳鏈韩- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient adapter 琛屼负 - `T3`锛欳LI -> daemon 瀹屾暣涓婚摼 - `T4`锛歞aemon 涓嶅彲鐢?fallback- 鍓嶇疆渚濊禆锛歚P1-01`## 7. Phase 2 浠诲姟鍗″崟## P2-01 澶?Agent supervisor- 鍗″彿锛歚P2-01`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 褰撳墠 agent 鏇村儚鈥滃崟娆″瓙浠诲姟鎵ц鍣ㄢ€濓紝涓嶆槸鎺у埗闈?- 鐮斿彂鍔熻兘锛? - agent DAG - budget / timeout / retry / ownership - parent-child artifact passing- 瀹屾垚绋嬪害锛? - 鑷冲皯鏀寔骞惰 agent + 姹囨€绘敹鍙?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歡raph / budget / timeout 鐘舵€佹満 - `T2`锛歴upervisor 璋冨害 - `T3`锛氬苟琛?agent 鎵ц - `T4`锛氬瓙 agent 澶辫触鎭㈠- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02`## P2-02 artifact bus 涓庡啿绐佹敹鏁?- 鍗″彿锛歚P2-02`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 澶?agent 杈撳嚭缂哄皯缁撴瀯鍖?artifact 浜ゆ崲灞?- 鐮斿彂鍔熻兘锛? - 缁熶竴 artifact schema - patch / note / diff / report / finding 娴佽浆 - 鍐茬獊妫€娴嬩笌鍚堝苟绛栫暐- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歛rtifact schema - `T2`锛歱roducer/consumer - `T3`锛氬 agent artifact 姹囨祦 - `T4`锛氬啿绐佸悎骞跺け璐?- 鍓嶇疆渚濊禆锛歚P2-01`## P2-03 IDE 闆嗘垚锛圴S Code first锛?- 鍗″彿锛歚P2-03`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 褰撳墠浠撳簱缂哄皯鐪熸鍙敤鐨?IDE workflow- 鐮斿彂鍔熻兘锛? - VS Code client - daemon 杩炴帴 - diff銆乼ask銆乻ession銆乫indings 灞曠ず- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歝lient protocol adapter - `T3`锛欼DE -> daemon 鏍稿績閾? - `T5`锛氫汉宸ヤ氦浜掗獙鏀朵负涓?- 鍓嶇疆渚濊禆锛歚P1-06`## P2-04 GitHub / CI 宸ヤ綔娴?agent- 鍗″彿锛歚P2-04`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 褰撳墠鑳藉姏涓昏鍋滅暀鍦ㄦ湰鍦?CLI锛屾棤娉曡繘鍏ュ洟闃熷崗浣滈棴鐜?- 鐮斿彂鍔熻兘锛? - issue -> plan -> code -> test -> review -> PR 宸ヤ綔娴? - CI 鐜鐨?headless execution 妯″紡- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歸orkflow state machine - `T3`锛歠ake repo / fake PR 涓婚摼 - `T4`锛氭潈闄愪笉瓒?/ repo 鐘舵€佷笉骞插噣 / 缃戠粶澶辫触- 鍓嶇疆渚濊禆锛歚P1-01`銆乣P1-02`## P2-05 缁勭粐绾х瓥鐣ヤ笌瀹¤闈㈡澘- 鍗″彿锛歚P2-05`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 鍥㈤槦鍦烘櫙涓嬬己涔忔不鐞嗐€佸鎵广€佸璁°€佸畨鍏ㄨ竟鐣?- 鐮斿彂鍔熻兘锛? - policy model - approval queue - secret redaction - audit query / reporting- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歱olicy matcher / redaction rules - `T2`锛歛pproval state transitions - `T3`锛氭墽琛屽璁¢摼闂幆 - `T4`锛氱瓥鐣ュ啿绐?/ 鏈鎵规墽琛岄樆鏂?- 鍓嶇疆渚濊禆锛歚P1-05`## P2-06 Linux 缁熶竴楠屽案闂ㄧ- 鍗″彿锛歚P2-06`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - 褰撳墠姣忓紶鍗″悇鏈?gate 鑴氭湰锛屼絾缂哄皯涓€娆℃€х粺涓€鎵ц娓呭崟锛屼笉鍒╀簬 Linux 闃舵鍏ㄩ噺楠屽案- 鐮斿彂鍔熻兘锛? - 鑱氬悎 `P0-01` 鍒?`P2-05` 鍏ㄩ摼璺叧閿祴璇曡剼鏈竻鍗? - 鎻愪緵 Linux 缁熶竴鎵ц鍏ュ彛鑴氭湰 - 寤虹珛 unified gate manifest 鐨?contract test锛岄伩鍏嶆竻鍗曟紓绉?- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮anifest 瀛樺湪鎬т笌鏂囦欢鏄犲皠鏍¢獙 - `T2`锛氬懡浠ょ敓鎴愰『搴忎笌鏍煎紡濂戠害 - `T3`锛歀inux 闃舵鎸夋竻鍗曢€愭潯鎵ц锛堟湰鍦颁笉鎵ц锛? - `T4`锛氭柊澧?鍒犲噺娴嬭瘯鏂囦欢鏃跺绾﹀洖褰掓彁绀?- 鍓嶇疆渚濊禆锛歚P2-05`## P2-07 Agent 杩愯鏃朵竴鑷存€э紙sync/background parity锛?- 鍗″彿锛歚P2-07`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `AgentTool` 鐨勫悓姝ヤ笌鍚庡彴璺緞鍦?runtime context 缁ф壙涓婂瓨鍦ㄦ紓绉婚闄? - 鍚庡彴 agent 浠诲姟缂哄皯 session/conversation 绛夊璁″厓鏁版嵁锛屼笉鍒╀簬鍥炴斁涓庢不鐞?- 鐮斿彂鍔熻兘锛? - 瀵归綈 sync/background 瀛?agent 鍦?`working_directory`銆乣tool_registry`銆乣permission_mode`銆乣provider/model`銆乣memory_scope` 鐨勮涓? - 灏?`session_id`銆乣conversation_id`銆乺untime context 瀛楁鍐欏叆鍚庡彴浠诲姟 metadata - 璁╁悗鍙拌矾寰勪紭鍏堝鐢ㄤ笂灞?runtime `TaskManager`锛岄伩鍏嶅叏灞€鍗曚緥婕傜Щ- 瀹炴柦鑼冨洿锛? - `claude_code/tools/base.py` - `claude_code/engine/query.py` - `claude_code/tools/agent/__init__.py` - `claude_code/tasks/manager.py`- 娴嬭瘯浠g爜瑕佹眰锛? - `T2`锛歴ync/background context 缁ф壙涓€鑷存€? - `T3`锛氬悗鍙颁换鍔?metadata 瀹¤瀛楁瀹屾暣鎬? - `T4`锛歱rovider/model 缁ф壙鍥炲綊锛堟湰鍦?provider 涓?Claude model alias 宸紓锛?- 鍓嶇疆渚濊禆锛歚P2-01`銆乣P2-06`## P2-08 鑷畾涔?Agent 鐩綍鍔犺浇闂幆锛?claude/agents锛?- 鍗″彿锛歚P2-08`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - Python 鐗堟湰 `AgentTool` 鍙敮鎸佸唴缃?`BUILTIN_AGENTS`锛屾湭褰㈡垚 `.claude/agents/*.md` 鐨勫姞杞介棴鐜? - `subagent_type` 鎵句笉鍒版椂鍙娴嬫€т笉瓒筹紝鏃犳硶鍚屾椂鐪嬪埌 custom + builtin 鍙敤绫诲瀷- 鐮斿彂鍔熻兘锛? - 鏂板 `.claude/agents/*.md` loader锛屾敮鎸?YAML frontmatter + prompt body 瑙f瀽 - 鏀寔浠庡綋鍓嶇洰褰曞悜涓婃悳绱㈠埌 git 鏍圭殑澶氱骇 `.claude/agents`锛屽苟鎸夆€滆繎璺緞瑕嗙洊杩滆矾寰勨€濆悎骞? - 鍦?`AgentTool` 涓帴鍏?custom agent lazy-load锛屼紭鍏堢骇 `custom > builtin` - `Unknown agent type` 閿欒鎻愮ず杈撳嚭鑱斿悎鍙敤 agent 鍒楄〃- 瀹炴柦鑼冨洿锛? - `claude_code/agents/loader.py` - `claude_code/tools/agent/__init__.py` - `scripts/run_p2_custom_agents_loader_gate.py` - `scripts/run_linux_unified_gate.py`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歠rontmatter 瑙f瀽涓庡瓧娈靛綊涓€鍖栵紙tools/disallowedTools/memory/maxTurns锛? - `T2`锛氬绾х洰褰曡鐩栫瓥鐣ワ紙root -> nested锛? - `T3`锛欰gentTool custom 瑕嗙洊 builtin 瑙f瀽閾? - `T4`锛歩nvalid YAML / 缂哄瓧娈靛閿?+ unknown type 鑱斿悎鍙敤娓呭崟- 鍓嶇疆渚濊禆锛歚P2-07`## P2-09 Linux 缁熶竴楠屽案鎵ц闂ㄧ- 鍗″彿锛歚P2-09`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-06` 褰撳墠鍙緭鍑哄懡浠ゆ竻鍗曪紝涓嶇洿鎺ユ彁渚?Linux 闃舵鐨勪竴閿墽琛屻€佹姤鍛婃矇娣€涓庡け璐ョ瓥鐣ユ帶鍒躲€? - 缁熶竴楠屽案閾剧己灏戞爣鍑嗗寲浜х墿锛坖unit xml + summary json锛夛紝涓嶅埄浜?CI 姹囨€讳笌鍥炴斁銆?- 鐮斿彂鍔熻兘锛? - 鏂板 Linux 鎵ц闂ㄧ鑴氭湰锛屽鐢?`run_linux_unified_gate.py` manifest銆? - 鎻愪緵 `--dry-run` / `--print-commands` / `--continue-on-failure` / `--report-dir` 鑳藉姏銆? - 椤哄簭鎵ц姣忔潯 pytest 鍛戒护骞朵骇鍑烘瘡鏉$敤渚嬬殑 `--junitxml`锛屾渶缁堝啓鍏ョ粺涓€ `summary.json`銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` - `scripts/run_linux_unified_gate.py` - `tests/test_p2_linux_unified_execution_gate_runtime.py` - `README.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭墽琛岄棬绂佸懡浠ょ敓鎴愪弗鏍艰窡闅?unified manifest 椤哄簭銆? - `T2`锛氶澶?pytest 鍙傛暟閫忎紶濂戠害涓?junit 璺緞濂戠害銆? - `T3`锛歀inux 闃舵鎵ц鍏ュ彛鍙?dry-run锛屼笉鍦ㄦ湰鍦拌繍琛屾祴璇曘€? - `T4`锛歮anifest 鍙樻洿鍚庢墽琛岄棬绂佸绾︽祴璇曞彲鍙婃椂鎷︽埅婕傜Щ銆?- 鍓嶇疆渚濊禆锛歚P2-06`銆乣P2-08`## P2-10 JetBrains IDE 闆嗘垚锛圛ntelliJ / PyCharm锛?- 鍗″彿锛歚P2-10`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-03` 浠呰鐩?`VS Code first` 閫傞厤锛孞etBrains 鐢熸€佺己灏戞爣鍑嗗寲蹇収閫傞厤灞傘€? - 鐜版湁 IDE payload 铏藉彲澶嶇敤锛屼絾缂哄皯 JetBrains 渚у彲鐩存帴娑堣垂鐨?inspection 缁撴瀯鏄犲皠銆?- 鐮斿彂鍔熻兘锛? - 鍦?`ide_adapter` 澧炲姞 `JetBrainsClientAdapter`銆? - 灏?daemon `workspace.findings` 鏄犲皠涓?JetBrains inspection 缁撴瀯锛坰everity -> highlight锛夈€? - 淇濇寔 daemon API 涓嶅彉锛岄€傞厤灞傚畬鎴愨€滃悓 payload锛屽 IDE 璇箟鈥濇敹鏁涖€?- 瀹炴柦鑼冨洿锛? - `claude_code/server/ide_adapter.py` - `claude_code/server/__init__.py` - `tests/test_p2_jetbrains_ide_integration_runtime.py` - `scripts/run_p2_jetbrains_ide_integration_gate.py` - `scripts/run_linux_unified_gate.py` - `README.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛欽etBrains adapter 鍗忚褰掍竴鍖栵紙project_root/vcs/sessions/tasks锛夈€? - `T2`锛歠indings -> inspections 瀛楁鏄犲皠涓?severity/highlight 濂戠害銆? - `T3`锛氫繚鎸?daemon `ide/workspace` payload 鍏煎锛屼笉鏂板璺敱銆? - `T4`锛氶潪娉?line/severity 杈撳叆瀹归敊銆?- 鍓嶇疆渚濊禆锛歚P2-03`銆乣P2-09`## P2-11 Linux 鍒嗙墖骞惰楠屽案闂ㄧ- 鍗″彿锛歚P2-11`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-09` 浠呮敮鎸佸崟杩涚▼椤哄簭鎵ц锛孡inux 闃舵鏃犳硶鐩存帴鎸?CI job 鍒嗙墖骞惰锛屾暣浣撳洖褰掕€楁椂楂樸€? - 缂哄皯鍒嗙墖绾?summary 鍏冩暟鎹紝涓嶅埄浜庡 job 鍥炴敹闃舵鍋氱粺涓€鑱氬悎銆?- 鐮斿彂鍔熻兘锛? - 鍦?`run_p2_linux_unified_execution_gate.py` 澧炲姞 `--shard-total` / `--shard-index`銆? - 鎸?unified manifest 椤哄簭鍋氱‘瀹氭€у垎鐗囬€夋嫨锛堝悓涓€杈撳叆濮嬬粓绋冲畾鍒囩墖锛夈€? - summary 澧炲姞 `shard_total` / `shard_index` / `manifest_total_tests` 瀛楁銆? - 淇濇寔 `--dry-run` / `--print-commands` 璺緞鍙褰撳墠鍒嗙墖瑕嗙洊鑼冨洿銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_execution_gate.py` - `tests/test_p2_linux_unified_execution_gate_runtime.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囬€夋嫨涓?manifest 椤哄簭濂戠害锛坉eterministic slicing锛夈€? - `T2`锛氶潪娉曞垎鐗囧弬鏁版嫆缁濆绾︼紙`shard_total < 1` / `shard_index` 瓒婄晫锛夈€? - `T3`锛歴ummary 鍒嗙墖鍏冩暟鎹绾︺€? - `T4`锛氶澶?pytest 鍙傛暟閫忎紶銆乯unit 璺緞濂戠害缁х画淇濇寔銆?- 鍓嶇疆渚濊禆锛歚P2-09`## P2-12 Linux 鍒嗙墖楠屽案姹囨€婚棬绂?- 鍗″彿锛歚P2-12`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-11` 宸叉敮鎸佸垎鐗囧苟琛屾墽琛岋紝浣嗙己灏?Linux 鍥炴敹闃舵鐨勭粺涓€姹囨€诲叆鍙o紝鏃犳硶缁欏嚭鍏ㄥ眬閫氳繃/澶辫触鍒ゅ畾銆? - 澶氫釜 shard 鐨?`summary.json` 缂哄皯涓€鑷存€ф牎楠岋紙缂虹墖/閲嶅鐗?manifest 婕傜Щ锛夛紝瀹规槗璁?CI 璇垽銆?- 鐮斿彂鍔熻兘锛? - 鏂板鍒嗙墖姹囨€婚棬绂佽剼鏈紝鏀寔 `--summary` / `--summary-glob` / `--artifacts-dir` 鑷姩鍙戠幇 summary 鏂囦欢銆? - 鍚堝苟 shard 缁撴灉骞惰緭鍑?`merged_summary.json`锛屾矇娣€ `overall_status`銆乣structural_issues`銆乣shards_missing` 涓庡叏灞€ totals銆? - 瀵瑰叧閿粨鏋勫仛濂戠害鏍¢獙锛氬瓧娈靛畬鏁存€с€乣shard_index` 鑼冨洿銆乣passed+failed<=total_tests`銆乵anifest/鍒嗙墖鎬绘暟涓€鑷存€с€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_aggregation_gate.py` - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` - `tests/test_p2_linux_unified_execution_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歴ummary 鍙戠幇绛栫暐鍘婚噸濂戠害锛坉irect + glob + artifacts-dir锛夈€? - `T2`锛歴ummary 鍩虹瀛楁鏍¢獙涓庣被鍨嬬害鏉熷绾︺€? - `T3`锛氬叏鍒嗙墖瑕嗙洊鏃剁殑鑱氬悎 totals 涓?passed 鐘舵€佸绾︺€? - `T4`锛氱己鐗?鎬绘暟婕傜Щ鏃剁殑 structural issue 涓?failed 鐘舵€佸绾︺€?- 鍓嶇疆渚濊禆锛歚P2-11`## P2-13 Linux 鏈€缁堟姤鍛婂彂甯冮棬绂?- 鍗″彿锛歚P2-13`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-12` 浠呰緭鍑?`merged_summary.json`锛岀己灏?Linux 鍥炴敹闃舵鍙洿鎺ュ綊妗d笌灞曠ず鐨勬渶缁堟姤鍛婁骇鐗┿€? - 瀵?`overall_status` 缂哄皯鈥滃啀璁$畻鏍¢獙鈥濅笌鍙戝竷濂戠害锛孋I 渚ч毦浠ョǔ瀹氭秷璐规渶缁堢粨璁恒€?- 鐮斿彂鍔熻兘锛? - 鏂板鏈€缁堟姤鍛婂彂甯冮棬绂佽剼鏈紝璇诲彇 `merged_summary.json` 骞跺仛瀛楁濂戠害鏍¢獙銆? - 杈撳嚭 `final_report.json` 涓?`final_report.md`锛堢姸鎬併€佸垎鐗囥€乼otals銆乻tructural issues銆乶otes锛夈€? - 瀵?`overall_status` 鎵ц computed-vs-reported 涓€鑷存€у鏍革紝涓嶄竴鑷存椂寮哄埗澶辫触銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_report_publish_gate.py` - `tests/test_p2_linux_report_publish_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮erged summary 蹇呭~瀛楁涓庣被鍨嬪绾︽牎楠屻€? - `T2`锛歨appy path 涓嬫渶缁堢姸鎬佷笌 totals 鍙戝竷濂戠害銆? - `T3`锛歳eported status 涓?computed status 涓嶄竴鑷存椂澶辫触濂戠害銆? - `T4`锛歁arkdown 鎶ュ憡缁撴瀯濂戠害锛圱otals/Shards/Structural Issues/Notes锛夈€?- 鍓嶇疆渚濊禆锛歚P2-12`## P2-14 Linux 缁熶竴楠屽案鍏ㄩ摼缂栨帓闂ㄧ- 鍗″彿锛歚P2-14`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-09` / `P2-12` / `P2-13` 宸叉媶鍒嗕负澶氶樁娈佃剼鏈紝浣嗙己灏戜竴鏉?Linux 涓€閿覆鑱旀墽琛屽叆鍙c€? - 澶氶樁娈垫墜宸ユ嫾瑁呭懡浠ゆ槗婕傜Щ锛屽け璐ョ瓥鐣ヤ笌鍙傛暟閫忎紶鍦?CI 渚ч毦缁熶竴銆?- 鐮斿彂鍔熻兘锛? - 鏂板 pipeline gate锛屾寜 `execution -> aggregation -> publish` 缂栨帓鎵ц銆? - 鏀寔 `--skip-execution` / `--skip-aggregation` / `--skip-publish`锛岃鐩栧崟鏈轰笌澶?job 鍥炴敹鍦烘櫙銆? - 鏀寔 `--print-commands` / `--dry-run` 浜у嚭鍙璁″懡浠ゆ竻鍗曪紝鏀寔 `--fail-fast` 澶辫触鎺у埗銆? - 鏀寔 execution 闃舵 pytest 棰濆鍙傛暟閫忎紶锛坄--` 杞彂鍒?`P2-09`锛夈€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_unified_pipeline_gate.py` - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁や笁闃舵鍛戒护鐢熸垚濂戠害锛坋xecution summary 鑷姩娉ㄥ叆 aggregation锛夈€? - `T2`锛歚skip-execution` 涓嬪閮?summary/glob/artifacts 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氬叏璺宠繃妯″紡杩斿洖绌?pipeline 濂戠害銆? - `T4`锛歱ytest 棰濆鍙傛暟閫忎紶濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-13`## P2-15 Linux 鍒嗙墖鎵ц璁″垝鐢熸垚闂ㄧ- 鍗″彿锛歚P2-15`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-11`/`P2-14` 宸叉敮鎸佸垎鐗囨墽琛屼笌鍏ㄩ摼缂栨帓锛屼絾 Linux CI fan-out job 浠嶉渶鎵嬪伐鎷兼帴 `shard_index` 鍛戒护锛屽鏄撴紓绉汇€? - 缂哄皯鈥滄墽琛屽墠鈥濊鍒掍骇鐗╋紝CI 鍥炴敹闃舵闅句互瀵圭収鏍¢獙鍒嗙墖瑕嗙洊鏄惁瀹屾暣銆?- 鐮斿彂鍔熻兘锛? - 鏂板 shard plan gate锛屽熀浜?unified manifest 鐢熸垚纭畾鎬?`shard_plan.json`銆? - 姣忎釜鍒嗙墖杈撳嚭锛歚test_files`銆乣report_dir`銆乣summary_path`銆乣execution command`銆乣command_parts`銆? - 鏀寔 `--print-commands`/`--dry-run` 鐢ㄤ簬 CI 缂栨帓鎺㈡祴锛涙敮鎸?pytest 棰濆鍙傛暟閫忎紶锛坄--`锛夈€? - 瀵?`shard_total` 涓?manifest 鏂囦欢瀛樺湪鎬у仛鍓嶇疆濂戠害鏍¢獙锛岄潪娉曡緭鍏ョ洿鎺ュけ璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_shard_plan_gate.py` - `tests/test_p2_linux_shard_plan_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬垎鐗囧垏鐗囩‘瀹氭€у绾︼紙涓?`P2-11` slicing 涓€鑷达級銆? - `T2`锛氬垎鐗囨墽琛屽懡浠や笌 pytest 鍙傛暟閫忎紶濂戠害銆? - `T3`锛氶潪娉?`shard_total` 鎷掔粷濂戠害銆? - `T4`锛氳緭鍑?`summary_path/report_dir` 瀛楁濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-14`## P2-16 Linux CI Matrix 瀵煎嚭闂ㄧ- 鍗″彿锛歚P2-16`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-15` 宸蹭骇鍑?`shard_plan.json`锛屼絾 Linux CI 浠嶉渶鎵嬪伐鎶婂垎鐗囪鍒掕浆涓?matrix 鍙橀噺锛屾槗浜х敓缂栨帓婕傜Щ銆? - fan-out 涓?fan-in 涔嬮棿缂哄皯缁熶竴鐨?`summary_paths` 鍥炴敹娓呭崟锛屽鑷村洖鏀堕樁娈靛弬鏁版嫾瑁呬笉绋冲畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 CI matrix gate锛屼粠 `shard_plan.json` 鐢熸垚鍙洿鎺ユ秷璐圭殑 `ci_matrix.json`銆? - 杈撳嚭鏍囧噯 `matrix.include` 鏉$洰锛坄shard_index/shard_total/command/command_parts/report_dir/summary_path`锛夈€? - 杈撳嚭 `summary_paths` 鍥炴敹娓呭崟锛屼緵 `P2-12` 鑱氬悎闃舵鐩存帴浣跨敤銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions output 鏂囦欢锛涙敮鎸?`--skip-empty-shards` 杩囨护绌哄垎鐗囥€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_matrix_gate.py` - `tests/test_p2_linux_ci_matrix_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歮atrix include 椤哄簭涓庡瓧娈靛绾︺€? - `T2`锛歚skip-empty-shards` 杩囨护濂戠害銆? - `T3`锛欸itHub output 瀛楁鐢熸垚濂戠害锛坢atrix/summary_paths/selected_shards锛夈€? - `T4`锛歚manifest_total_tests` 涓庡垎鐗?totals 涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-15`## P2-17 Linux CI Workflow 璁″垝闂ㄧ- 鍗″彿锛歚P2-17`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-16` 宸蹭骇鍑?`ci_matrix.json`锛屼絾 fan-out/fan-in CI job 浠嶉渶浜哄伐鎷兼帴鍥炴敹鍛戒护锛屽彂甯冮摼瀛樺湪鎵嬪伐婕傜Щ椋庨櫓銆? - 缂哄皯缁熶竴 workflow 璁″垝浜х墿锛孋I 缂栨帓渚ф棤娉曠洿鎺ユ秷璐光€滃垎鐗囨墽琛?+ 姹囨€?+ 鍙戝竷鈥濆叏閾惧懡浠ゃ€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow plan gate锛屼粠 `ci_matrix.json` 鐢熸垚 `ci_workflow_plan.json`銆? - 杈撳嚭 `fan_out_jobs` 涓?`fan_out_matrix.include`锛堝惈 shard job command/command_parts/report/summary 淇℃伅锛夈€? - 杈撳嚭 `fan_in` 姹囨€讳笌鍙戝竷鍛戒护锛坅ggregation/publish command + command_parts锛夛紝鐩存帴瀵规帴 `P2-12`/`P2-13`銆? - 鏀寔 `--github-output` 鍐欏叆 GitHub Actions outputs锛坄fan_out_matrix`銆乣fan_in_summary_paths`銆乣aggregation_command`銆乣publish_command` 绛夛級銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_plan_gate.py` - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow payload 涓?fan-out/fan-in 瀛楁涓庡懡浠ゅ绾︺€? - `T2`锛欸itHub output 瀛楁鐢熸垚濂戠害銆? - `T3`锛歚summary_paths` 涓?`matrix.include` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-16`## P2-18 Linux CI Workflow YAML 娓叉煋闂ㄧ- 鍗″彿锛歚P2-18`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-17` 宸蹭骇鍑?`ci_workflow_plan.json`锛屼絾 CI 骞冲彴浠嶉渶浜哄伐缂栧啓/缁存姢 workflow YAML锛宖an-out/fan-in 鍛戒护鏄撴紓绉汇€? - 缂哄皯鈥滆鍒掍骇鐗?-> 鍙墽琛?workflow 鏂囦欢鈥濈殑鏈€鍚庝竴璺筹紝瀵艰嚧闂ㄧ閾句粛瀛樺湪鎵嬪伐浠嬪叆鐐广€?- 鐮斿彂鍔熻兘锛? - 鏂板 CI workflow YAML gate锛屼粠 `ci_workflow_plan.json` 娓叉煋 `.github/workflows/linux_unified_gate.yml`銆? - 娓叉煋 fan-out matrix job锛堝垎鐗囨墽琛?+ summary artifact 涓婁紶锛変笌 fan-in job锛坅rtifact 鍥炴敹 + 鑱氬悎 + 鍙戝竷 + 鏈€缁堟姤鍛婁笂浼狅級銆? - 杈撳嚭娓叉煋鍏冩暟鎹?`ci_workflow_render.json`锛岃褰?source plan銆乫an-in 鍛戒护銆佹姤鍛婅矾寰勪笌鍒嗙墖缁熻锛屼緵瀹¤杩借釜銆? - 鏀寔 `--dry-run`/`--print-yaml` 蹇€熼獙鐪嬫覆鏌撶粨鏋滐紝涓嶅湪鏈湴瑙﹀彂娴嬭瘯鎵ц銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow YAML fan-out/fan-in 瀛楁涓庡懡浠ゆ覆鏌撳绾︺€? - `T2`锛歳ender metadata 瀛楁濂戠害锛坰ource/workflow path/summary stats锛夈€? - `T3`锛歚fan_in.summary_paths` 涓?`fan_out_matrix.include[*].summary_path` 椤哄簭涓嶄竴鑷存椂鎷掔粷濂戠害銆? - `T4`锛歚selected_shards` 涓?include 鏁伴噺涓嶄竴鑷存椂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-17`## P2-19 Linux CI Workflow 婕傜Щ鏍¢獙闂ㄧ- 鍗″彿锛歚P2-19`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-18` 宸茶兘娓叉煋 workflow YAML 涓?metadata锛屼絾浠撳簱涓殑 `.github/workflows/linux_unified_gate.yml` 涓?`ci_workflow_render.json` 鍙兘琚墜宸ユ敼鍔ㄥ悗涓?`ci_workflow_plan.json` 鑴辫妭銆? - 缂哄皯鈥滆鍒掍骇鐗?-> 娓叉煋浜х墿涓€鑷存€ф牎楠屸€濋棬绂侊紝CI 鍙兘鍦ㄦ紓绉荤姸鎬佷笅杩愯锛屽鑷?fan-out/fan-in 鍛戒护涓庢姤鍛婅矾寰勫け鐪熴€?- 鐮斿彂鍔熻兘锛? - 鏂板 workflow sync gate锛屽熀浜?`ci_workflow_plan.json` 澶嶇畻鏈熸湜 workflow YAML 涓?metadata锛屽苟涓庡綋鍓嶄骇鐗╁仛涓€鑷存€ф瘮瀵广€? - 杈撳嚭 drift 鎽樿锛坵orkflow_drift/metadata_drift/drift_count锛夛紝鏀寔 `--print-diff` 鎵撳嵃 unified diff 渚夸簬瀹¤銆? - 鏀寔 `--write` 鍦ㄦ湰鍦版垨 CI 閲岃嚜鍔ㄥ洖鍐欐爣鍑嗕骇鐗╋紝鏀舵暃浜哄伐婕傜Щ銆? - 鏀寔 `--strict-generated-at` 寮€鍚?metadata `generated_at` 涓ユ牸姣斿锛涢粯璁ゅ拷鐣ヨ瀛楁浠呮牎楠岀粨鏋勪笌鏍稿績鍐呭銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_sync_gate.py` - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬綋 workflow/metadata 缂哄け鏃跺繀椤昏Е鍙?drift 鍒ゅ畾銆? - `T2`锛氬綋 workflow 鍐呭琚敼鍐欐椂锛屽繀椤昏緭鍑?drift 涓庡彲璇?diff 濂戠害銆? - `T3`锛氶粯璁ゆā寮忓拷鐣?metadata `generated_at` 婕傜Щ锛岄伩鍏嶆椂闂存埑鍣0璇姤銆? - `T4`锛氬紑鍚?strict 妯″紡鏃讹紝`generated_at` 婕傜Щ蹇呴』瑙﹀彂鎷掔粷濂戠害銆?- 鍓嶇疆渚濊禆锛歚P2-18`## P2-20 Linux CI Workflow 鍛戒护鎶ゆ爮闂ㄧ- 鍗″彿锛歚P2-20`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-19` 宸茶兘鏍¢獙 plan->yaml/metadata 婕傜Щ锛屼絾灏氭湭绾︽潫 `ci_workflow_plan.json` 鍐?`command` 涓?`command_parts` 鐨勪竴鑷存€э紝瀛樺湪鈥滃瓧绗︿覆琚敼銆乸arts 鏈敼鈥濆鑷?CI 瀹為檯鎵ц鍋忕Щ鐨勯闄┿€? - fan-out 涓?fan-in 鍛戒护涓殑鍏抽敭璺緞锛坄report_dir`銆乣summary_paths`銆乣merged_summary/final_report`锛夌己灏戝己濂戠害缁戝畾锛屽彲鑳藉湪鎵嬪伐缂栬緫鍚庡紩鍏ラ潤榛橀敊璇€?- 鐮斿彂鍔熻兘锛? - 鏂板 command guard gate锛屾牎楠?`command`/`command_parts` 鍙€嗕竴鑷存€э紙`shlex.split(command)` == `command_parts`锛変笌 canonical shell 瑙勮寖鍖栬緭鍑恒€? - 鏍¢獙鑴氭湰鐩爣涓庡叧閿?flag 濂戠害锛? - fan-out 蹇呴』鎸囧悜 `run_p2_linux_unified_execution_gate.py`锛屼笖鎼哄甫 `--report-dir/--shard-total/--shard-index`銆? - fan-in aggregation 蹇呴』鎸囧悜 `run_p2_linux_shard_aggregation_gate.py`锛屼笖 `--summary` 椤哄簭涓ユ牸鍖归厤 `fan_in.summary_paths`銆? - fan-in publish 蹇呴』鎸囧悜 `run_p2_linux_report_publish_gate.py`锛屼笖杈撳嚭璺緞涓?`fan_in` 瀛楁涓€鑷淬€? - 鏀寔 `--write` 鍥炲啓 canonical command 瀛楃涓诧紝鏀舵暃璁″垝鏂囦欢鍛戒护鏍煎紡婕傜Щ銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氬悎娉?workflow plan 蹇呴』閫氳繃 guard锛宍failed_commands=0`銆? - `T2`锛歚command`/`command_parts` 涓嶄竴鑷存椂蹇呴』瑙﹀彂鎷掔粷锛屽苟缁欏嚭瑙勮寖鍖栧懡浠ゃ€? - `T3`锛歛ggregation 鐨?`--summary` 椤哄簭涓?`fan_in.summary_paths` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛歱ublish 杈撳嚭璺緞涓?`fan_in.final_report_*` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆?- 鍓嶇疆渚濊禆锛歚P2-19`## P2-21 Linux CI Workflow 娌荤悊鏀舵暃闂ㄧ- 鍗″彿锛歚P2-21`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-19` 涓?`P2-20` 宸插垎鍒鐩栤€滄覆鏌撴紓绉烩€濆拰鈥滃懡浠ゅ绾︹€濓紝浣嗙己灏戠粺涓€娌荤悊缁撹锛孋I 渚т粛闇€鎵嬪伐鎷兼帴澶氫唤鎶ュ憡鍒ゆ柇鏄惁鍙墽琛?workflow銆? - 缂哄皯 metadata 琛€缂樹竴鑷存€ч棬绂侊紙`source_ci_workflow_plan` 涓?`workflow_output_path`锛夛紝瀛樺湪鈥滀骇鐗╃湅浼奸€氳繃銆佹潵婧愬凡閿欎綅鈥濈殑闅愭€ч闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance gate锛屼竴娆℃墽琛屾眹鎬?`workflow_sync + command_guard + metadata lineage` 涓夌被妫€鏌ワ紝浜у嚭鍗曚竴 `overall_status`銆? - 杈撳嚭 `failed_checks` 鏄庣‘澶辫触绫诲埆锛歚workflow_sync_drift` / `command_guard_failed` / `lineage_mismatch`銆? - 灏?`command_guard` 鎽樿鍖栦负 `failed_commands/issue_count/normalization_required`锛屼究浜?CI 鑱氬悎灞曠ず銆? - 瀵?metadata 琛€缂樺仛寮烘牎楠岋細`source_ci_workflow_plan` 涓?`workflow_output_path` 蹇呴』涓庡綋鍓嶉棬绂佽緭鍏ヨ矾寰勪竴鑷淬€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_gate.py` - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歸orkflow/command/lineage 鍏ㄩ儴涓€鑷存椂蹇呴』閫氳繃锛宍overall_status=passed`銆? - `T2`锛歸orkflow YAML 婕傜Щ鏃跺繀椤昏惤鍏?`workflow_sync_drift` 澶辫触绫汇€? - `T3`锛歝ommand 濂戠害寮傚父鏃跺繀椤昏惤鍏?`command_guard_failed` 澶辫触绫汇€? - `T4`锛歮etadata 琛€缂樺瓧娈典笌杈撳叆璺緞涓嶄竴鑷存椂蹇呴』钀藉叆 `lineage_mismatch` 澶辫触绫汇€?- 鍓嶇疆渚濊禆锛歚P2-20`## P2-22 Linux CI Workflow 娌荤悊鍙戝竷闂ㄧ- 鍗″彿锛歚P2-22`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-21` 宸蹭骇鍑虹粺涓€娌荤悊缁撹锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽 `ci_workflow_governance.json` 鎵嶈兘鍐冲畾鈥滄槸鍚︽墽琛?workflow鈥濄€? - 缂哄皯鍥哄畾鐨勫彂甯冧骇鐗╀笌 GitHub output 濂戠害锛屽鑷?fan-in 闃舵闅句互绋冲畾娑堣垂娌荤悊缁撴灉涓庡け璐ョ被鍒€?- 鐮斿彂鍔熻兘锛? - 鏂板 governance publish gate锛岃鍙?`P2-21` 鎶ュ憡鍚庣敓鎴愬彲娑堣垂鍙戝竷浜х墿锛? - `ci_workflow_governance_publish.json` - `ci_workflow_governance_publish.md` - 杈撳嚭绋冲畾鍐崇瓥瀛楁锛歚overall_status`銆乣should_execute_workflow`銆乣failed_checks`銆乣failed_check_count`銆? - 鏀寔 `--github-output` 瀵煎嚭 CI 鍙橀噺锛歚governance_status`銆乣governance_should_execute_workflow`銆乣governance_failed_checks` 绛夈€? - 澧炲姞鍙戝竷渚т竴鑷存€ф牎楠岋細鑻?`failed_checks` 鎴?`overall_status` 涓庨噸绠楃粨鏋滀笉涓€鑷达紝蹇呴』钀藉叆 `structural_issues` 骞跺垽瀹氬け璐ャ€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗘姤鍛婂叏閲忎竴鑷存椂搴旇緭鍑?`overall_status=passed` 涓?`should_execute_workflow=true`銆? - `T2`锛氬綋娌荤悊鎶ュ憡鍖呭惈澶辫触妫€鏌ユ椂锛屽簲杈撳嚭 `overall_status=failed` 涓?`should_execute_workflow=false`銆? - `T3`锛氬綋娌荤悊鎶ュ憡鐨?`failed_checks` 涓庨噸绠楃粨鏋滀笉涓€鑷存椂锛屽繀椤昏Е鍙?`structural_issues` 骞跺け璐ャ€? - `T4`锛欸itHub output 瀵煎嚭瀛楁蹇呴』鍖呭惈鐘舵€併€佸け璐ュ垪琛ㄤ笌鍙戝竷浜х墿璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-21`## P2-23 Linux CI Workflow 鎵ц鍐崇瓥闂ㄧ- 鍗″彿锛歚P2-23`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-22` 铏藉凡浜у嚭 `should_execute_workflow`锛屼絾 CI 涓嬫父浠嶇己鈥滅粺涓€鎵ц闂搁棬鈥濆皢娌荤悊缁撴灉杞垚鏄庣‘鐨?execute/blocked 缁撴灉涓庨€€鍑虹爜璇箟銆? - 缂哄皯瀵规墽琛屽墠鍏抽敭浜х墿锛坵orkflow plan / rendered workflow锛夌殑瀛樺湪鎬ф牎楠岋紝鍙兘鍑虹幇鈥滄不鐞嗛€氳繃浣嗘墽琛屽叆鍙g己澶扁€濈殑鏂摼銆?- 鐮斿彂鍔熻兘锛? - 鏂板 execution decision gate锛岃鍙?`P2-22` 鍙戝竷缁撴灉锛岀粺涓€杈撳嚭锛? - `ci_workflow_execution_decision.json` - `ci_workflow_execution_decision.md` - 鍥哄寲鍐崇瓥瀛楁锛歚decision`銆乣decision_status`銆乣should_execute_workflow`銆乣exit_code`銆乣reason_codes`銆乣missing_artifacts`銆? - 鏀寔闃绘柇绛栫暐寮€鍏筹細`--on-block fail|skip`锛岃 CI 鍙湪闃绘柇鏃堕€夋嫨 fail-fast 鎴?soft-skip銆? - 鏀寔 `--github-output` 杈撳嚭涓嬫父鍙橀噺锛屽噺灏?job 闂存墜宸ヨВ鏋愩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_decision_gate.py` - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氭不鐞嗛€氳繃涓旀墽琛屼骇鐗╅綈澶囨椂蹇呴』杈撳嚭 `decision=execute`銆乣exit_code=0`銆? - `T2`锛氭不鐞嗗け璐ユ椂蹇呴』杈撳嚭 `decision=blocked` 涓旀惡甯﹀け璐ュ師鍥犵爜銆? - `T3`锛氶樆鏂瓥鐣ヤ负 `skip` 鏃讹紝闃绘柇鍐崇瓥搴旇繑鍥?`exit_code=0`銆? - `T4`锛欸itHub output 濂戠害闇€鍖呭惈 decision/status/should_execute/exit_code 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-22`## P2-24 Linux CI Workflow 璋冨害灏辩华闂ㄧ- 鍗″彿锛歚P2-24`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-23` 宸蹭骇鍑?execute/blocked 鍐崇瓥锛屼絾 CI 涓嬫父浠嶉渶鎵嬪伐灏嗗喅绛栧瓧娈垫嫾瑁呬负瀹為檯璋冨害鍛戒护锛屽瓨鍦ㄥ懡浠や笉涓€鑷翠笌閿欒瑙﹀彂椋庨櫓銆? - 缂哄皯缁熶竴 dispatch 濂戠害鎶ュ憡锛屽鑷粹€滄槸鍚﹀彲鍙戣捣 `gh workflow run`鈥濈殑鍒ゆ柇鏃犳硶琚?job 闂寸ǔ瀹氬鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch readiness gate锛岃鍙?`P2-23` 鍐崇瓥浜х墿骞惰緭鍑猴細 - `ci_workflow_dispatch.json` - `ci_workflow_dispatch.md` - 鍥哄寲璋冨害瀛楁锛歚dispatch_status`銆乣dispatch_mode`銆乣should_dispatch_workflow`銆乣dispatch_command`銆乣dispatch_command_parts`銆? - execute 鍐崇瓥鏃剁敓鎴愯鑼冨寲 `gh workflow run` 鍛戒护锛沚locked 鍐崇瓥鏃惰緭鍑虹┖鍛戒护骞朵繚鎸侀樆鏂師鍥犲彲杩芥函銆? - 鏀寔 `--github-output` 瀵煎嚭 dispatch 鐘舵€併€佸懡浠や笌鎶ュ憡璺緞锛屼緵涓嬫父 CI 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歟xecute 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=ready` 涓庡悎娉?`gh workflow run` 鍛戒护銆? - `T2`锛歜locked 鍐崇瓥鏃跺繀椤讳骇鍑?`dispatch_status=blocked` 涓?`dispatch_command` 涓虹┖銆? - `T3`锛氬喅绛栧绾﹀啿绐侊紙濡?decision=execute 浣?should_execute=false锛夋椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 dispatch 鐘舵€併€佽皟搴﹀懡浠や笌鎶ュ憡璺緞銆?- 鍓嶇疆渚濊禆锛歚P2-23`## P2-25 Linux CI Workflow 璋冨害鎵ц闂ㄧ- 鍗″彿锛歚P2-25`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-24` 浠呬骇鍑衡€滃彲璋冨害濂戠害鈥濓紝鏈粺涓€澶勭悊鐪熸 dispatch 鎵ц缁撴灉锛孋I 涓嬫父浠嶉渶閲嶅鎷兼帴鈥滄槸鍚︽墽琛?鎵ц澶辫触濡備綍鍥炴姤鈥濈殑閫昏緫銆? - 缂哄皯璋冨害鎵ц灞傜粺涓€鎶ュ憡锛屽鑷?execute/blocked/fail 涓夋€佹棤娉曠ǔ瀹氭矇娣€涓哄崟涓€瀹¤浜х墿銆?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch execution gate锛岃鍙?`P2-24` 璋冨害濂戠害骞惰緭鍑猴細 - `ci_workflow_dispatch_execution.json` - `ci_workflow_dispatch_execution.md` - 鍥哄寲鎵ц瀛楁锛歚execution_status`銆乣execution_exit_code`銆乣dispatch_attempted`銆乣command_returncode`銆乣reason_codes`銆? - `dispatch_status=ready` 鏃舵墽琛?`dispatch_command_parts`锛堟敮鎸?`--dry-run` 璺宠繃鐪熷疄鎵ц锛夛紱`dispatch_status=blocked` 鏃朵繚鎸佹棤鎵ц浣嗕繚鐣欓樆鏂涔夈€? - 鏀寔 `--github-output` 瀵煎嚭鎵ц鐘舵€併€侀€€鍑虹爜涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歳eady + dry-run 蹇呴』浜у嚭 `execution_status=ready_dry_run` 涓?`execution_exit_code=0`銆? - `T2`锛歜locked 鍐崇瓥蹇呴』淇濇寔 `dispatch_attempted=false` 涓旈€€鍑虹爜閬靛惊涓婃父闃绘柇绛栫暐璇箟銆? - `T3`锛歞ispatch 濂戠害鍐茬獊锛堝 command 涓?command_parts 涓嶄竴鑷达級蹇呴』鎷掔粷銆? - `T4`锛氬懡浠ゆ墽琛屽け璐ユ椂蹇呴』钀藉湴 `execution_status=dispatch_failed` 骞舵惡甯﹀け璐ュ師鍥犵爜銆? - `T5`锛欸itHub output 濂戠害闇€鍖呭惈鎵ц鐘舵€併€佹槸鍚﹀皾璇曡皟搴︺€侀€€鍑虹爜涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-24`## P2-26 Linux CI Workflow 鍏ㄩ摼缂栨帓闂ㄧ- 鍗″彿锛歚P2-26`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-17` 鑷?`P2-33` 鍚勯棬绂佸凡鍏峰锛屼絾 CI 渚т粛闇€浜哄伐鎷兼帴澶氭潯鍛戒护锛屽鏄撳嚭鐜伴樁娈甸仐婕忋€佸弬鏁版紓绉讳笌椤哄簭閿欒銆? - 缂哄皯鈥滃崟鍏ュ彛鈥濋棬绂佺粺涓€璋冨害 workflow plan/yaml/sync/governance/dispatch/terminal publish 鍏ㄩ摼锛屽鑷?Linux CI 缂栨帓閲嶅缁存姢鎴愭湰楂樸€?- 鐮斿彂鍔熻兘锛? - 鏂板 full pipeline gate锛屼竴娆$紪鎺?`P2-17 -> P2-33` 鍗佸叚闃舵锛? - plan -> yaml -> sync -> command_guard -> governance -> governance_publish -> decision -> dispatch -> dispatch_execution -> dispatch_trace -> dispatch_completion -> terminal_publish -> release_handoff -> release_trigger -> release_trace -> release_completion銆? - 鏀寔缁熶竴闃舵瑁佸壀 `--skip-*`锛屽厑璁稿湪 CI fan-in / rerun 鍦烘櫙鎸夐渶璺宠繃鍓嶅簭闃舵銆? - 鏀寔缁熶竴绛栫暐閫忎紶锛歚--strict-generated-at`銆乣--sync-write`銆乣--command-guard-write`銆乣--on-block`銆乣--workflow-ref`銆乣--dispatch-timeout-seconds`銆乣--dispatch-trace-poll-now`銆乣--completion-allow-in-progress`銆? - 鏀寔 `--print-commands/--dry-run` 棰勮涓?`--fail-fast` 鎵ц绛栫暐锛屾矇娣€鍗曚竴 Linux CI 缂栨帓鍏ュ彛銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛氶粯璁ら樁娈甸摼蹇呴』瀹屾暣瑕嗙洊 `P2-17 -> P2-33` 涓旈『搴忓浐瀹氥€? - `T2`锛氱瓥鐣ュ弬鏁帮紙strict/sync-write/command-guard-write/on-block/workflow-ref/timeout/trace-poll锛夊繀椤绘纭€忎紶鍒板搴斿瓙闂ㄧ銆? - `T3`锛氶儴鍒?`--skip-*` 鏃跺繀椤诲彧淇濈暀鍚庡崐閾鹃樁娈靛苟淇濇寔椤哄簭銆? - `T4`锛氬叏閮?`--skip-*` 鏃跺簲杩斿洖绌鸿鍒掞紙涓嶈Е鍙戦樁娈垫墽琛岋級銆? - `T5`锛氭敮鎸佷粎缁堝眬鍙戝竷闃舵锛坄terminal_publish`锛変笌 release completion 闃舵鐙珛鎵ц銆?- 鍓嶇疆渚濊禆锛歚P2-25`## P2-27 Linux CI Workflow 璋冨害杩借釜闂ㄧ- 鍗″彿锛歚P2-27`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-25` 鍙鐩?dispatch 鍛戒护鎵ц鍥炴墽锛屼絾鏈敹鏁?workflow run 鐨勮拷韪绾︼紝CI 涓嬫父浠嶉渶鎵嬪伐瑙f瀽鏃ュ織鎻愬彇 `run_id`/`run_url`銆? - 缂哄皯缁熶竴 run poll 鍛戒护涓庤拷韪姸鎬侊紝瀵艰嚧鍚庣画 job 瀵光€滃凡璋冨害浣嗘湭瀹屾垚鈥濈殑鐘舵€佸垽鏂笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch trace gate锛岃鍙?`P2-25` 鎵ц鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_dispatch_trace.json` - `ci_workflow_dispatch_trace.md` - 浠?dispatch stdout/stderr 鎻愬彇 canonical GitHub Actions run URL 涓?`run_id`锛岀敓鎴愬彲澶嶇敤 poll 鍛戒护锛坄gh run view --json ...`锛夈€? - 鏀寔 `--poll-now` 绔嬪嵆鎵ц涓€娆?run 鐘舵€佹煡璇紝缁熶竴 `run_tracking_ready` / `run_in_progress` / `run_completed_success` / `run_completed_failure` / `run_poll_failed` 鍒ゅ畾銆? - 鏀寔 `--github-output` 瀵煎嚭杩借釜鐘舵€併€乺un 鏍囪瘑銆乸oll 缁撴灉涓庢姤鍛婅矾寰勶紝渚?CI 涓嬫父鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚dispatched` 涓斿惈 run URL 鏃跺繀椤绘彁鍙?`run_id/run_url` 骞剁敓鎴?poll 鍛戒护銆? - `T2`锛歚dispatched` 浣嗙己 run URL 鏃跺繀椤昏惤鍏?`run_tracking_missing` 涓旇繑鍥炲け璐ラ€€鍑虹爜銆? - `T3`锛歚blocked`/`ready_dry_run` 璺緞蹇呴』淇濇寔鏃?poll 灏濊瘯骞堕€忎紶涓婃父璇箟銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈杩借釜鐘舵€併€乺un_id/run_url銆佹姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-26`## P2-28 Linux CI Workflow 璋冨害瀹屾垚鏀舵暃闂ㄧ- 鍗″彿锛歚P2-28`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-27` 浠呮彁渚涘崟娆?trace 涓庡彲閫夊崟娆?poll锛屾湭鎻愪緵鈥滅瓑寰呰嚦缁堟€佲€濈殑缁熶竴鏀舵暃濂戠害锛汣I 涓嬫父浠嶉渶鑷寰幆 `gh run view` 骞跺鐞嗚秴鏃?杩涜涓?澶辫触鍒嗘敮銆? - 缂哄皯缁熶竴 completion verdict锛屽鑷?`run_in_progress`銆乣run_completed_failure`銆乣run_poll_failed`銆乣run_tracking_missing` 鍦ㄤ笉鍚?job 涓€€鍑鸿涔変笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 dispatch completion gate锛岃鍙?`P2-27` trace 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_dispatch_completion.json` - `ci_workflow_dispatch_completion.md` - 鏀寔杞绛夊緟鍙傛暟锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`锛屽皢 run 鐘舵€佺粺涓€鏀舵暃涓猴細 - `run_completed_success` - `run_completed_failure` - `run_poll_failed` - `run_await_timeout` - 浠ュ強閫忎紶鍨?`blocked` / `ready_dry_run` / `run_tracking_missing` - 鎻愪緵 `--allow-in-progress` 绛栫暐寮€鍏筹細瓒呮椂浠?in-progress 鏃跺彲閫夋槸鍚﹁繑鍥?exit 0锛屾弧瓒充笉鍚?CI 缂栨帓绛栫暐銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€乸oll 杩唬銆乺un_id/url銆佹姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_tracking_ready` 涓?poll 杩斿洖 completed+success 鏃讹紝蹇呴』鏀舵暃涓?`run_completed_success` 涓?`completion_exit_code=0`銆? - `T2`锛氭寔缁?in-progress 鍒?`max_polls` 鍚庯紝蹇呴』钀?`run_await_timeout`锛屽苟鎸?`--allow-in-progress` 鍐冲畾閫€鍑虹爜銆? - `T3`锛歵race 涓?`poll_command` 涓?`poll_command_parts` 涓嶄竴鑷存椂蹇呴』鎷掔粷銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乪xit code銆乸oll 娆℃暟銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-27`## P2-29 Linux CI Workflow 缁堝眬鍙戝竷闂ㄧ- 鍗″彿锛歚P2-29`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-28` 宸茬粰鍑?completion verdict锛屼絾涓嬫父鍙戝竷娴佺▼浠嶉渶鑷灏?`completion_status` 鏄犲皠涓哄彲鍙戝竷璇箟锛坧romote/hold/fail锛夛紝瀵艰嚧鏀跺彛绛栫暐涓嶄竴鑷淬€? - 缂哄皯缁熶竴缁堝眬 publish 濂戠害锛岄毦浠ョǔ瀹氬鐢ㄥ湪 release gate 涓庡悗缁幆澧冩帹骞跨瓥鐣ヤ腑銆?- 鐮斿彂鍔熻兘锛? - 鏂板 terminal publish gate锛岃鍙?`P2-28` completion 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_terminal_publish.json` - `ci_workflow_terminal_publish.md` - 灏?completion 璇箟缁熶竴鏀舵暃涓?`publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_promote`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`run_completed_success` 蹇呴』 `completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚run_completed_success` + `completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `publish_status=passed` 涓?`should_promote=true`銆? - `T2`锛歚run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `publish_status=in_progress` 涓?`publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈缁堝眬鐘舵€併€乸romote 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-28`## P2-30 Linux CI Workflow Release Handoff Gate- 鍗″彿锛歚P2-30`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-29` 宸茬粰鍑?publish verdict锛屼絾 release job 浠嶉渶鑷瑙f瀽 `publish_status/should_promote` 缁勮瑙﹀彂鏉′欢锛屽鑷存帹骞胯涔夊垎鍙夈€? - 缂哄皯缁熶竴 handoff 濂戠害锛屼笉鍒╀簬鍚庣画閮ㄧ讲骞冲彴鎴栧鎵规祦澶嶇敤 Linux CI 缁堝眬缁撹銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release handoff gate锛岃鍙?`P2-29` terminal publish 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_handoff.json` - `ci_workflow_release_handoff.md` - 灏?publish 璇箟缁熶竴鏀舵暃涓?`handoff_status`锛坄ready_for_release`/`awaiting_completion`/`blocked`/`failed`/`contract_failed`锛変笌 `should_trigger_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`publish_status=passed` 蹇呴』 `publish_exit_code=0` 涓?`should_promote=true`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--target-environment`銆乣--release-channel` 閫忎紶鍙戝竷鐩爣璇箟锛屽苟鏀寔 `--github-output` 瀵煎嚭 handoff 鐘舵€併€乺elease action銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚publish_status=passed` + `should_promote=true` 鏃跺繀椤绘敹鏁涗负 `handoff_status=ready_for_release` 涓?`should_trigger_release=true`銆? - `T2`锛歚publish_status=in_progress` 鏃跺繀椤绘敹鏁涗负 `handoff_status=awaiting_completion` 涓?`release_exit_code=0`銆? - `T3`锛歵erminal publish 濂戠害鍐茬獊锛堝 passed 浣?should_promote=false锛夋椂蹇呴』鏀舵暃涓?`handoff_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 handoff 鐘舵€併€乺elease action銆乼arget env/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-29`## P2-31 Linux CI Workflow Release Trigger Gate- 鍗″彿锛歚P2-31`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-30` 宸蹭骇鍑?`should_trigger_release` 璇箟锛屼絾浠嶇己灏戔€滅湡姝f墽琛?release dispatch鈥濈殑缁熶竴鎵ц闂ㄧ锛汣I 涓嬫父闇€鑷鎷艰鍛戒护骞跺鐞嗚秴鏃?澶辫触鍒嗘敮锛岃Е鍙戣涓轰笉涓€鑷淬€? - 缂哄皯缁熶竴 trigger 鎵ц鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 杩借釜銆佸洖婧笌瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trigger gate锛岃鍙?`P2-30` release handoff 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_trigger.json` - `ci_workflow_release_trigger.md` - 褰?`should_trigger_release=true` 鏃讹紝鑷姩鏋勫缓骞舵墽琛?release dispatch command锛堟敮鎸佽嚜瀹氫箟 `--release-command` 瑕嗙洊榛樿鍛戒护锛夈€? - 褰?`should_trigger_release=false` 鏃讹紝鎸?handoff 鐘舵€佹敹鏁涗负 `release_trigger_status`锛坄awaiting_completion`/`blocked`/`handoff_failed`锛夛紝骞朵繚鎸侀浂瑙﹀彂鍓綔鐢ㄣ€? - 鍥哄寲濂戠害鏍¢獙锛歨andoff 鐘舵€併€乤ction銆乪xit code 涓庤Е鍙戝竷灏斿€煎繀椤讳竴鑷达紱涓嶄竴鑷村嵆鎷掔粷鎵ц銆? - 鏀寔 `--github-output` 瀵煎嚭 trigger 鐘舵€併€佽Е鍙戝皾璇曘€乪xit code銆乺un 鏍囪瘑涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚ready_for_release + should_trigger_release=true` 鍦?dry-run 璺緞蹇呴』鏀舵暃涓?`release_trigger_status=ready_dry_run`銆? - `T2`锛歚awaiting_completion` 璺緞蹇呴』淇濇寔 `release_trigger_status=awaiting_completion` 涓斾笉瑙﹀彂鍛戒护鎵ц銆? - `T3`锛歨andoff 濂戠害鍐茬獊锛堝 `ready_for_release` 浣?`should_trigger_release=false`锛夊繀椤昏鎷掔粷鍔犺浇銆? - `T4`锛氳Е鍙戝懡浠ら潪闆惰繑鍥炲繀椤绘敹鏁涗负 `release_trigger_status=trigger_failed` 骞惰褰曞け璐?reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trigger 鐘舵€併€乤ttempted銆乪xit code銆乪nv/channel銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-30`## P2-32 Linux CI Workflow Release Traceability Gate- 鍗″彿锛歚P2-32`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-31` 宸插畬鎴?release trigger 鎵ц锛屼絾缂哄皯 release run 杩借釜濂戠害锛涗笅娓?job 闇€瑕侀噸澶嶈В鏋?stdout/stderr 鎵嶈兘鎷垮埌 `run_id/run_url`锛岃拷韪涔変笉涓€鑷淬€? - 缂哄皯缁熶竴 release trace 鎶ュ憡锛屼笉鍒╀簬鍚庣画 release 瀹屾垚鎬佹敹鏁涖€佸璁¤拷婧笌璺?job 澶嶇敤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release trace gate锛岃鍙?`P2-31` release trigger 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_trace.json` - `ci_workflow_release_trace.md` - 褰?`release_trigger_status=triggered` 鏃讹紝缁熶竴鎻愬彇 release run 寮曠敤骞舵敹鏁涗负锛? - `release_run_id` / `release_run_url` - `release_tracking_status`锛坄release_run_tracking_ready`/`release_run_tracking_missing`锛? - 鍙€?poll 鍛戒护濂戠害锛坄release_poll_command` + `release_poll_command_parts`锛? - 鏀寔 `--poll-now` 绔嬪嵆鏌ヨ release run 鐘舵€侊紝鏀舵暃涓猴細 - `release_run_in_progress` / `release_run_completed_success` / `release_run_completed_failure` / `release_run_poll_failed` - 瀵归潪瑙﹀彂鐘舵€侊紙`ready_dry_run` / `awaiting_completion` / `blocked` / `handoff_failed` / `trigger_failed`锛夌粺涓€閫忎紶鎴愭棤鍓綔鐢?trace 璇箟銆? - 鏀寔 `--github-output` 瀵煎嚭 trace 鐘舵€併€乻hould_poll銆乺un_id/url銆乸oll 鐘舵€佷笌鎶ュ憡璺緞銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚triggered` 鎶ュ憡蹇呴』鎻愬彇鍑?run 寮曠敤锛屽苟鏀舵暃涓?`release_tracking_status=release_run_tracking_ready`銆? - `T2`锛歚triggered` 浣嗙己灏?run 寮曠敤鏃跺繀椤绘敹鏁涗负 `release_tracking_status=release_run_tracking_missing` 涓?`release_trace_exit_code=1`銆? - `T3`锛歚trigger_failed` 璺緞蹇呴』閫忎紶涓?`release_tracking_status=release_trigger_failed` 涓斾笉瑙﹀彂 poll銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 trace 鐘舵€併€乻hould_poll銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-31`## P2-33 Linux CI Workflow Release Completion Gate- 鍗″彿锛歚P2-33`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-32` 宸插叿澶?release run trace 濂戠害锛屼絾鈥滅瓑寰?run 瀹屾垚鈥濅粛闇€涓嬫父 job 鑷杞锛岀己灏戠粺涓€瀹屾垚鎬佹敹鏁涖€? - 鏃犵粺涓€ release completion 鎶ュ憡锛屽鑷?release 鎴愬姛/澶辫触/瓒呮椂/杩涜涓殑鏈€缁堣涔夊湪 CI 鍚勯樁娈典笉涓€鑷淬€?- 鐮斿彂鍔熻兘锛? - 鏂板 release completion gate锛岃鍙?`P2-32` release trace 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_completion.json` - `ci_workflow_release_completion.md` - 鏀寔 bounded poll loop锛歚--poll-interval-seconds`銆乣--max-polls`銆乣--poll-timeout-seconds`銆? - 鏀舵暃 completion_status锛? - `release_run_completed_success` - `release_run_completed_failure` - `release_run_in_progress` - `release_run_await_timeout` - `release_run_poll_failed` - 鍙?blocked/dry-run/trigger_failed 绛夐€忎紶鐘舵€併€? - 鏀寔 `--allow-in-progress`锛氬厑璁歌秴鏃朵粛 in-progress 鏃惰繑鍥?exit 0銆? - 鏀寔 `--github-output` 瀵煎嚭 completion 鐘舵€併€佽疆璇俊鎭€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_tracking_ready` + poll success 蹇呴』鏀舵暃涓?`release_run_completed_success`銆? - `T2`锛氭寔缁?in-progress 涓旀湭鍏佽 in-progress 蹇呴』鏀舵暃涓?`release_run_await_timeout` 涓旈€€鍑虹爜闈為浂銆? - `T3`锛歳elease poll command 涓?parts 涓嶄竴鑷存椂蹇呴』鎷掔粷鍔犺浇銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 completion 鐘舵€併€乸oll 淇℃伅銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-32`## P2-34 Linux CI Workflow Release Terminal Publish Gate- 鍗″彿锛歚P2-34`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-33` 宸茬粰鍑?release completion verdict锛屼絾涓嬫父 release 鏈€缁堟敹鍙d粛闇€鑷瑙f瀽 completion 璇箟锛屽鏄撲骇鐢?finalize/hold/fail 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release terminal publish 濂戠害锛屼笉鍒╀簬鍚庣画 release governance 涓庣幆澧冩帹杩涚瓥鐣ュ鐢ㄣ€?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal publish gate锛岃鍙?`P2-33` release completion 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_terminal_publish.json` - `ci_workflow_release_terminal_publish.md` - 灏?release completion 璇箟缁熶竴鏀舵暃涓?`release_publish_status`锛坄passed`/`blocked`/`in_progress`/`failed`/`contract_failed`锛変笌 `should_finalize_release`銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_run_completed_success` 蹇呴』 `release_completion_exit_code=0`锛夛紝涓嶄竴鑷存椂钀?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚夸簬 Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_run_completed_success` + `release_completion_exit_code=0` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=passed` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_run_await_timeout` + `allow_in_progress=true` 鏃跺繀椤绘敹鏁涗负 `release_publish_status=in_progress` 涓?`release_publish_exit_code=0`銆? - `T3`锛歝ompletion 濂戠害涓嶄竴鑷达紙濡?success 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_publish_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 release terminal 鐘舵€併€乫inalize 鍐崇瓥銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-33`## P2-35 Linux CI Workflow Release Finalization Gate- 鍗″彿锛歚P2-35`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-34` 宸茶緭鍑?release terminal publish verdict锛屼絾鈥滄渶缁堟槸鍚︽敹鍙?release 鐢熷懡鍛ㄦ湡鈥濈殑绛栫暐浠嶅垎鏁e湪涓嬫父 job锛屽鏄撳嚭鐜?finalize/hold/abort 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 finalization 濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈电粰鍑虹ǔ瀹氱殑 release closure 缁撴灉涓庡彲瀹¤鍑哄彛銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release finalization gate锛岃鍙?`P2-34` release terminal publish 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_finalization.json` - `ci_workflow_release_finalization.md` - 灏?release terminal publish 璇箟缁熶竴鏀舵暃涓猴細 - `release_finalization_status`锛坄finalized` / `awaiting_release` / `failed` / `contract_failed`锛? - `release_finalization_decision`锛坄finalize` / `hold` / `abort`锛? - `release_finalization_exit_code` - 鏀寔 `--on-hold` 绛栫暐锛坄pass`/`fail`锛夋帶鍒?blocked/in_progress 鏀跺彛閫€鍑虹爜璇箟銆? - 鍥哄寲濂戠害涓€鑷存€ф牎楠岋紙濡?`release_publish_status=passed` 蹇呴』 `release_publish_exit_code=0` 涓?`should_finalize_release=true`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_publish_status=passed` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_finalization_status=finalized` 涓?`should_finalize_release=true`銆? - `T2`锛歚release_publish_status=in_progress` 涓?`--on-hold=fail` 鏃跺繀椤绘敹鏁涗负 `release_finalization_status=awaiting_release` 涓旈€€鍑虹爜闈?0銆? - `T3`锛歵erminal publish 濂戠害涓嶄竴鑷达紙濡?passed 浣?exit_code 闈?0锛夋椂蹇呴』鏀舵暃涓?`release_finalization_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 finalization 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-34`## P2-36 Linux CI Workflow Release Closure Publish Gate- 鍗″彿锛歚P2-36`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-35` 宸茶緭鍑?finalization verdict锛屼絾瀵瑰娑堣垂锛堥€氱煡銆佸綊妗c€佷笅娓哥郴缁熸帴鍏ワ級浠嶉渶鑷鎷兼帴瀛楁锛宑losure 璇箟瀹规槗鍑虹幇 close/hold/rollback 鍒ゅ畾婕傜Щ銆? - 缂哄皯缁熶竴 release closure 鍙戝竷濂戠害锛屼笉鍒╀簬 Linux CI 鍦ㄧ粓鐐归樁娈靛澶栨彁渚涚ǔ瀹氬崟鍑哄彛锛坢achine-readable + human-readable锛夌粨鏋溿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release closure gate锛岃鍙?`P2-35` release finalization 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_closure.json` - `ci_workflow_release_closure.md` - 灏?finalization 璇箟缁熶竴鏀舵暃涓猴細 - `release_closure_status`锛坄closed` / `pending` / `failed` / `contract_failed`锛? - `release_closure_decision`锛坄ship` / `hold` / `rollback`锛? - `release_closure_exit_code` - `should_close_release` 涓?`should_notify` - 鍥哄寲 finalization 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_finalization_status=finalized` 蹇呴』 `release_finalization_decision=finalize` 涓?`release_finalization_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_finalization_status=finalized` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_closure_status=closed` 涓?`release_closure_decision=ship`銆? - `T2`锛歚release_finalization_status=awaiting_release` 鏃跺繀椤绘敹鏁涗负 `release_closure_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歠inalization 濂戠害涓嶄竴鑷达紙濡?finalized 浣?decision!=finalize锛夋椂蹇呴』鏀舵暃涓?`release_closure_status=contract_failed`銆? - `T4`锛欸itHub output 濂戠害蹇呴』鍖呭惈 closure 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-35`## P2-37 Linux CI Workflow Release Archive Publish Gate- 鍗″彿锛歚P2-37`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-36` 宸茶緭鍑?release closure verdict锛屼絾 release evidence锛坈losure/finalization/trace/dispatch source artifacts锛変粛鏈舰鎴愮粺涓€ machine-readable 褰掓。濂戠害銆? - 缂哄皯缁堢偣褰掓。鍗曞嚭鍙f椂锛孡inux CI 涓嬫父锛堥€氱煡銆佸璁°€佸綊妗g郴缁燂級浠嶉渶鑷鎷兼帴璺緞涓庡垽瀹氾紝瀛樺湪鍙戝竷缁撹涓庤瘉鎹紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release archive gate锛岃鍙?`P2-36` release closure 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_archive.json` - `ci_workflow_release_archive.md` - 灏?closure 璇箟缁熶竴鏀舵暃涓猴細 - `release_archive_status`锛坄ready` / `pending` / `failed` / `contract_failed`锛? - `release_archive_decision`锛坄publish` / `hold` / `block`锛? - `release_archive_exit_code` - `should_publish_archive` - 鏋勫缓 evidence manifest锛堝寘鍚?closure 涓庡叾涓婃父 source_* 鎶ュ憡璺緞瀛樺湪鎬э級锛屽彂鐜扮己澶卞嵆寮哄埗 `contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 archive 鐘舵€併€佸喅绛栥€乵issing artifacts 涓庢姤鍛婅矾寰勶紝渚?Linux CI 涓嬫父 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_closure_status=closed` 涓?evidence 瀹屾暣鏃跺繀椤绘敹鏁涗负 `release_archive_status=ready` 涓?`release_archive_decision=publish`銆? - `T2`锛歚release_closure_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_archive_status=pending` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歝losure 濂戠害涓嶄竴鑷达紙濡?closed 浣?decision!=ship锛夋椂蹇呴』鏀舵暃涓?`release_archive_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 archive 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-36`## P2-38 Linux CI Workflow Release Verdict Publish Gate- 鍗″彿锛歚P2-38`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-37` 宸茶緭鍑?release archive verdict锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄槸鍚︽斁琛屽彂甯冣€濈殑鍗曞嚭鍙e垽璇嶏紝缁堟€?ship/hold/block 璇箟鍒嗘暎鍦ㄥ瀛楁涓€? - 缂哄皯鏈€缁?verdict 濂戠害鏃讹紝涓嬫父娴佹按绾夸笌鍙戝竷鐪嬫澘浠嶉渶鑷鎷艰鍒ゅ畾閫昏緫锛屽瓨鍦ㄥ嚭鍙h涔夋紓绉婚闄┿€?- 鐮斿彂鍔熻兘锛? - 鏂板 release verdict gate锛岃鍙?`P2-37` release archive 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_verdict.json` - `ci_workflow_release_verdict.md` - 灏?archive 璇箟缁熶竴鏀舵暃涓猴細 - `release_verdict_status`锛坄published` / `awaiting_archive` / `blocked` / `contract_failed`锛? - `release_verdict_decision`锛坄ship` / `hold` / `block`锛? - `release_verdict_exit_code` - `should_ship_release` 涓?`should_open_incident` - 鍥哄寲 archive 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_archive_status=ready` 蹇呴』 `release_archive_decision=publish` 涓?`release_archive_exit_code=0`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 浜屾妫€鏌?evidence manifest锛屽彂鐜扮己澶辨椂寮哄埗 `contract_failed` 骞惰ˉ鍏?reason code銆? - 鏀寔 `--github-output` 瀵煎嚭 verdict 鐘舵€併€佸喅绛栥€乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_archive_status=ready` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`release_verdict_status=published` 涓?`release_verdict_decision=ship`銆? - `T2`锛歚release_archive_status=pending` 鏃跺繀椤绘敹鏁涗负 `release_verdict_status=awaiting_archive` 涓斾繚鎸?hold 璇箟銆? - `T3`锛歛rchive 濂戠害涓嶄竴鑷达紙濡?ready 浣?decision!=publish锛夋椂蹇呴』鏀舵暃涓?`release_verdict_status=contract_failed`銆? - `T4`锛歟vidence 缂哄け鏃跺繀椤绘爣璁?missing artifacts 骞舵敹鏁涗负 `contract_failed`銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 verdict 鐘舵€併€佸喅绛栥€乺un_id/url銆乵issing artifacts 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-37`## P2-39 Linux CI Workflow Release Incident Dispatch Gate- 鍗″彿锛歚P2-39`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-38` 宸茶緭鍑?release verdict 涓?`should_open_incident` 璇箟锛屼絾 Linux CI 浠嶇己灏戠粺涓€鈥滀綍鏃?濡備綍瑙﹀彂 incident鈥濈殑鎵ц濂戠害锛屼笅娓搁渶瑕佽嚜琛屾嫾鎺ュ懡浠や笌澶辫触璇箟銆? - 缂哄皯 incident dispatch 鍗曞嚭鍙f椂锛岀粓鐐瑰憡璀﹂摼璺紙issue/incident system锛夊鏄撳嚭鐜拌Е鍙戞紡鎶ャ€侀噸澶嶈Е鍙戞垨澶辫触涓嶅彲瀹¤銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release incident gate锛岃鍙?`P2-38` release verdict 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_incident.json` - `ci_workflow_release_incident.md` - 灏?verdict 璇箟缁熶竴鏀舵暃涓猴細 - `incident_dispatch_status`锛坄not_required` / `ready_dry_run` / `dispatched` / `dispatch_failed` / `contract_failed`锛? - `should_dispatch_incident` - `incident_dispatch_exit_code` - 鍥哄寲 verdict 濂戠害涓€鑷存€ф牎楠岋紙濡?`release_verdict_status=published` 蹇呴』 `should_open_incident=false`锛夛紝涓嶄竴鑷存椂寮哄埗 `contract_failed`銆? - 鏀寔 auto incident command锛堥粯璁?`gh issue create`锛変笌 `--incident-command` 瑕嗙洊锛屽け璐ユ椂缁熶竴鏀舵暃 error reason銆? - 鏀寔 `--github-output` 瀵煎嚭 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + 濂戠害涓€鑷存椂蹇呴』鏀舵暃涓?`incident_dispatch_status=not_required`銆? - `T2`锛歚release_verdict_status=blocked` 涓?`should_open_incident=true` 鏃讹紝dry-run 蹇呴』鏀舵暃涓?`incident_dispatch_status=ready_dry_run`銆? - `T3`锛歷erdict 濂戠害涓嶄竴鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`incident_dispatch_status=contract_failed`銆? - `T4`锛歩ncident command 澶辫触鏃跺繀椤绘敹鏁涗负 `incident_dispatch_status=dispatch_failed` 骞惰ˉ鍏?failure reason code銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 incident 鐘舵€併€佹槸鍚﹁Е鍙戙€乮ncident url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-38`## P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate- 鍗″彿锛歚P2-40`- 浼樺厛绾э細`P2`- 瑙e喅闂锛? - `P2-39` 宸茶緭鍑?incident dispatch 濂戠害锛屼絾 Linux CI 缁堢偣浠嶇己灏戠粺涓€鈥滄渶缁堝彂甯冨垽璇嶁€濇敹鍙o紱涓嬫父浠嶉渶鑷鎷兼帴 ship/hold/escalate/block 璇箟銆? - 缂哄皯 terminal verdict 鍗曞嚭鍙f椂锛宺elease 缁堝眬鐪嬫澘涓庤嚜鍔ㄥ寲闂幆锛堥€氱煡銆佷汉宸ユ帴绠°€侀樆鏂級鏄撳嚭鐜拌涔夋紓绉讳笌閲嶅鍒ゅ畾銆?- 鐮斿彂鍔熻兘锛? - 鏂板 release terminal verdict gate锛岃鍙?`P2-39` release incident 鎶ュ憡骞惰緭鍑猴細 - `ci_workflow_release_terminal_verdict.json` - `ci_workflow_release_terminal_verdict.md` - 灏?release+incident 璇箟缁熶竴鏀舵暃涓猴細 - `release_terminal_verdict_status`锛坄released` / `awaiting_archive` / `blocked_incident_ready_dry_run` / `blocked_incident_dispatched` / `blocked_incident_failed` / `blocked` / `contract_failed`锛? - `release_terminal_verdict_decision`锛坄ship` / `hold` / `escalate` / `block`锛? - `release_terminal_verdict_exit_code` - `terminal_should_ship_release` - `terminal_requires_follow_up` - 鍥哄寲 `P2-38` + `P2-39` 濂戠害涓€鑷存€ф牎楠岋紙渚嬪 `release_verdict_status=published` 蹇呴』 `should_open_incident=false` 涓?`incident_dispatch_status=not_required`锛夛紝涓嶄竴鑷村己鍒?`contract_failed`銆? - 鏀寔 `--github-output` 瀵煎嚭 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勶紝渚?Linux CI 缁堢偣 job 鐩存帴娑堣垂銆?- 瀹炴柦鑼冨洿锛? - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- 娴嬭瘯浠g爜瑕佹眰锛? - `T1`锛歚release_verdict_status=published` + `incident_dispatch_status=not_required` 鏃跺繀椤绘敹鏁涗负 `release_terminal_verdict_status=released`銆? - `T2`锛歚release_verdict_status=blocked` + `incident_dispatch_status=ready_dry_run` 鏃讹紝蹇呴』鏀舵暃涓?`release_terminal_verdict_status=blocked_incident_ready_dry_run` 涓?decision=`escalate`銆? - `T3`锛氬绾︿笉涓€鑷达紙濡?published 浣?`should_open_incident=true`锛夋椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed`銆? - `T4`锛氳瘉鎹己澶辨椂蹇呴』鏀舵暃涓?`release_terminal_verdict_status=contract_failed` 骞惰ˉ鍏?missing artifact reason銆? - `T5`锛欸itHub output 濂戠害蹇呴』鍖呭惈 terminal verdict 鐘舵€併€佸喅绛栥€乪xit code銆乮ncident 鐘舵€?url銆乺un_id/url 涓庢姤鍛婅矾寰勩€?- 鍓嶇疆渚濊禆锛歚P2-39`## P2-41 Linux CI Workflow Release Delivery Closure Gate- ???`P2-41`- ????`P2`- ????? - `P2-40` ??? terminal verdict ???? Linux CI ?????????????????????????????????? - ?? delivery closure gate ??????? ship/hold/escalate/block ????????????????????????????- ????? - ?? release delivery gate??? `P2-40` release terminal verdict ?????? - `ci_workflow_release_delivery.json` - `ci_workflow_release_delivery.md` - ? terminal verdict + incident ???????? - `release_delivery_status`?`shipped` / `pending_follow_up` / `blocked_incident` / `blocked_incident_failed` / `blocked` / `contract_failed`? - `release_delivery_decision`?`deliver` / `hold` / `escalate` / `block`? - `release_delivery_exit_code` - `delivery_should_ship_release` - `delivery_requires_human_action` - `delivery_should_announce_blocker` - ?? `P2-40` ?????????? `release_terminal_verdict_status=released` ?? `incident_dispatch_status=not_required`??????? `contract_failed`? - ?? `--github-output` ?? delivery ??????exit code??????????incident ??/url?run_id/url ??????- ????? - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- ??????? - `T1`?`release_terminal_verdict_status=released` + `incident_dispatch_status=not_required` ?????? `release_delivery_status=shipped` ? decision=`deliver`? - `T2`?`release_terminal_verdict_status=blocked_incident_ready_dry_run` ?????? `release_delivery_status=blocked_incident` ? decision=`escalate`? - `T3`???????? `released` ? `incident_dispatch_status=ready_dry_run`??????? `release_delivery_status=contract_failed`? - `T4`??????????? `release_delivery_status=contract_failed` ??? missing artifact reason? - `T5`?GitHub output ?????? delivery ??????exit code????????incident ??/url?run_id/url ??????- ?????`P2-40`## P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate- Card ID: `P2-42`- Priority: `P2`- Problem: - `P2-41` converges to delivery semantics, but Linux CI still lacks one terminal publish contract for downstream notification/hand-off consumers. - Without one terminal publish output, follow-up automation and human takeover paths must re-interpret delivery fields independently.- Scope: - Add terminal publish gate that consumes `P2-41` delivery report and emits: - `ci_workflow_release_delivery_terminal_publish.json` - `ci_workflow_release_delivery_terminal_publish.md` - Converge to normalized outputs: - `release_delivery_terminal_publish_status` (`published`/`pending_follow_up`/`blocked`/`contract_failed`) - `release_delivery_terminal_publish_decision` (`announce_release`/`announce_hold`/`announce_blocker`/`abort_publish`) - `release_delivery_terminal_publish_exit_code` - `terminal_publish_should_notify` / `terminal_publish_should_create_follow_up` / `terminal_publish_channel` - Export `--github-output` fields for Linux CI terminal job consumption.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_delivery_status=shipped` converges to `published` + `announce_release`. - `T2`: `release_delivery_status=blocked_incident` converges to `blocked` + `announce_blocker`. - `T3`: delivery/incident contract mismatch converges to `contract_failed`. - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. - `T5`: GitHub output includes status/decision/exit_code/channel/run_id/run_url.- Depends on: `P2-41`## P2-43 Linux CI Workflow Release Delivery Final Verdict Gate- Card ID: `P2-43`- Priority: `P2`- Problem: - `P2-42` converges release delivery to terminal publish semantics, but Linux CI still lacks one final closure verdict contract for release close/follow-up/escalation takeover. - Without one final verdict output, downstream consumers must re-interpret terminal publish + delivery + incident fields independently.- Scope: - Add final verdict gate that consumes `P2-42` delivery terminal publish report and emits: - `ci_workflow_release_delivery_final_verdict.json` - `ci_workflow_release_delivery_final_verdict.md` - Converge to normalized outputs: - `release_delivery_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) - `release_delivery_final_verdict_decision` (`close_release`/`open_follow_up`/`escalate_blocker`/`abort_close`) - `release_delivery_final_verdict_exit_code` - `final_should_close_release` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` - Export `--github-output` fields for Linux CI terminal closeout job consumption.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_delivery_terminal_publish_status=published` converges to `completed` + `close_release`. - `T2`: `release_delivery_terminal_publish_status=pending_follow_up` converges to `requires_follow_up` + `open_follow_up`. - `T3`: terminal publish / delivery / incident contract mismatch converges to `contract_failed`. - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. - `T5`: GitHub output includes status/decision/exit_code/final-action booleans/announcement_target/run_id/run_url.- Depends on: `P2-42`## P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate- Card ID: `P2-44`- Priority: `P2`- Problem: - `P2-43` converges release delivery to final verdict semantics, but Linux CI still lacks one terminal follow-up dispatch contract for queue/escalation takeover consumers. - Without one follow-up dispatch output, downstream automation must re-interpret final verdict + incident + publish fields independently.- Scope: - Add follow-up dispatch gate that consumes `P2-43` delivery final verdict report and emits: - `ci_workflow_release_follow_up_dispatch.json` - `ci_workflow_release_follow_up_dispatch.md` - Converge to normalized outputs: - `release_follow_up_dispatch_status` (`closed`/`follow_up_required`/`escalated`/`contract_failed`) - `release_follow_up_dispatch_decision` (`no_action`/`dispatch_follow_up`/`dispatch_escalation`/`abort_dispatch`) - `release_follow_up_dispatch_exit_code` - `follow_up_required` / `escalation_required` / `dispatch_target` - Export `--github-output` fields for Linux CI final hand-off consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_delivery_final_verdict_status=completed` converges to `closed` + `no_action`. - `T2`: `release_delivery_final_verdict_status=requires_follow_up` converges to `follow_up_required` + `dispatch_follow_up`. - `T3`: final-verdict contract mismatch converges to `contract_failed`. - `T4`: missing evidence converges to `contract_failed` with missing-artifact reason. - `T5`: GitHub output includes status/decision/exit_code/follow-up/escalation booleans/dispatch_target/run_id/run_url.- Depends on: `P2-43`## P2-45 Linux CI Workflow Release Follow-Up Closure Gate- Card ID: `P2-45`- Priority: `P2`- Problem: - `P2-44` converges release delivery final verdict to follow-up dispatch semantics, but Linux CI still lacks one terminal follow-up closure contract for backlog ownership/queue intake consumers. - Without one follow-up closure output, downstream automation must re-interpret dispatch status + target + escalation flags and cannot safely distinguish dry-run readiness vs actual queue success.- Scope: - Add follow-up closure gate that consumes `P2-44` release follow-up dispatch report and emits: - `ci_workflow_release_follow_up_closure.json` - `ci_workflow_release_follow_up_closure.md` - Converge to normalized outputs: - `release_follow_up_closure_status` (`closed`/`queued_dry_run`/`queued`/`queue_failed`/`contract_failed`) - `release_follow_up_closure_decision` (`no_action`/`queue_follow_up`/`queue_escalation`/`abort_queue`) - `release_follow_up_closure_exit_code` - `should_queue_follow_up` / `follow_up_queue_attempted` / `follow_up_task_queued` / `escalation_task_queued` - Export `--github-output` fields for Linux CI terminal follow-up hand-off consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_follow_up_dispatch_status=closed` converges to `closed` + `no_action`. - `T2`: `release_follow_up_dispatch_status=follow_up_required` converges to `queued_dry_run` + `queue_follow_up` in dry-run mode. - `T3`: follow-up dispatch contract mismatch converges to `contract_failed`. - `T4`: executable follow-up queue command failure converges to `queue_failed`. - `T5`: GitHub output includes status/decision/exit_code/queue booleans/target/queue_url/run_id/run_url.- Depends on: `P2-44`## P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate- Card ID: `P2-46`- Priority: `P2`- Problem: - `P2-45` converges follow-up dispatch to closure semantics, but Linux CI still lacks one terminal publish contract that cleanly separates `closed`/`queued`/`pending_queue`/`queue_failed` outcomes for downstream notification and hand-off jobs. - Without one terminal publish output, downstream automation must re-interpret closure status + queue attempt fields and cannot consume a single stable final follow-up signal.- Scope: - Add follow-up terminal publish gate that consumes `P2-45` release follow-up closure report and emits: - `ci_workflow_release_follow_up_terminal_publish.json` - `ci_workflow_release_follow_up_terminal_publish.md` - Converge to normalized outputs: - `release_follow_up_terminal_publish_status` (`published`/`pending_queue`/`queue_failed`/`contract_failed`) - `release_follow_up_terminal_publish_decision` (`announce_closed`/`announce_queued`/`announce_pending_queue`/`announce_queue_failure`/`abort_publish`) - `release_follow_up_terminal_publish_exit_code` - `follow_up_terminal_publish_should_notify` / `follow_up_terminal_requires_manual_action` / `follow_up_terminal_publish_channel` - Export `--github-output` fields for Linux CI terminal follow-up publish consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_follow_up_closure_status=closed` converges to `published` + `announce_closed`. - `T2`: `release_follow_up_closure_status=queued_dry_run` converges to `pending_queue` + `announce_pending_queue`. - `T3`: closure contract mismatch converges to `contract_failed`. - `T4`: `release_follow_up_closure_status=queue_failed` converges to `queue_failed` + `announce_queue_failure`. - `T5`: GitHub output includes status/decision/exit_code/notify/manual-action/channel/closure-status/queue-url/run_id/run_url.- Depends on: `P2-45`## P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate- Card ID: `P2-47`- Priority: `P2`- Problem: - `P2-46` converges follow-up closure to terminal publish semantics, but Linux CI still lacks one final verdict contract that cleanly separates `completed`/`requires_follow_up`/`blocked`/`contract_failed` outcomes for post-release ownership and escalation consumers. - Without one final verdict output, downstream automation must re-interpret terminal publish + closure + queue fields and cannot consume one stable close/open/escalate signal.- Scope: - Add follow-up final verdict gate that consumes `P2-46` release follow-up terminal publish report and emits: - `ci_workflow_release_follow_up_final_verdict.json` - `ci_workflow_release_follow_up_final_verdict.md` - Converge to normalized outputs: - `release_follow_up_final_verdict_status` (`completed`/`requires_follow_up`/`blocked`/`contract_failed`) - `release_follow_up_final_verdict_decision` (`close_follow_up`/`keep_follow_up_open`/`escalate_queue_failure`/`abort_close`) - `release_follow_up_final_verdict_exit_code` - `final_should_close_follow_up` / `final_should_open_follow_up` / `final_should_page_owner` / `final_announcement_target` - Export `--github-output` fields for Linux CI terminal follow-up ownership consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: `release_follow_up_terminal_publish_status=published` + `announce_closed` converges to `completed` + `close_follow_up`. - `T2`: `release_follow_up_terminal_publish_status=pending_queue` converges to `requires_follow_up` + `keep_follow_up_open`. - `T3`: `release_follow_up_terminal_publish_status=queue_failed` converges to `blocked` + `escalate_queue_failure`. - `T4`: terminal publish / closure contract mismatch converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/final booleans/announcement-target/terminal-channel/queue-url/run_id/run_url.- Depends on: `P2-46`## P2-48 Linux CI Workflow Release Final Outcome Gate- Card ID: `P2-48`- Priority: `P2`- Problem: - `P2-43` delivers release final verdict semantics and `P2-47` delivers follow-up final verdict semantics, but Linux CI still lacks one single terminal outcome contract that downstream release governance and notification consumers can consume without re-stitching two reports. - Without one final outcome output, downstream automation must merge delivery/follow-up statuses itself and risks drift on ship-with-follow-up vs blocked escalation semantics.- Scope: - Add release final outcome gate that consumes `P2-43` release delivery final verdict report and `P2-47` release follow-up final verdict report and emits: - `ci_workflow_release_final_outcome.json` - `ci_workflow_release_final_outcome.md` - Converge to normalized outputs: - `release_final_outcome_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) - `release_final_outcome_decision` (`ship_and_close`/`ship_with_follow_up_open`/`escalate_blocker`/`abort_outcome`) - `release_final_outcome_exit_code` - `final_should_ship_release` / `final_follow_up_open` / `final_should_page_owner` / `final_outcome_target` - Export `--github-output` fields for Linux CI terminal release ownership consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: delivery=`completed` + follow-up=`completed` converges to `released` + `ship_and_close`. - `T2`: delivery=`completed` + follow-up=`requires_follow_up` converges to `released_with_follow_up` + `ship_with_follow_up_open`. - `T3`: delivery or follow-up=`blocked` converges to `blocked` + `escalate_blocker`. - `T4`: delivery/follow-up contract mismatch (decision/flags/run-id) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/ship+follow-up flags/target/queue-url/run-id/run-url/report paths.- Depends on: `P2-43`, `P2-47`## P2-49 Linux CI Workflow Release Final Terminal Publish Gate- Card ID: `P2-49`- Priority: `P2`- Problem: - `P2-48` already converges one final release outcome artifact, but Linux CI still lacks one terminal publish contract dedicated to final stakeholder-facing announcement semantics. - Without a dedicated terminal publish gate, downstream consumers still need custom mapping from outcome states to channel/action semantics, increasing drift risk on `released_with_follow_up` vs `blocked` paths.- Scope: - Add release final terminal publish gate that consumes `P2-48` release final outcome report and emits: - `ci_workflow_release_final_terminal_publish.json` - `ci_workflow_release_final_terminal_publish.md` - Converge to normalized outputs: - `release_final_terminal_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `release_final_terminal_publish_decision` (`announce_release_closure`/`announce_release_with_follow_up`/`announce_blocker`/`abort_publish`) - `release_final_terminal_publish_exit_code` - `final_terminal_should_notify` / `final_terminal_requires_manual_action` / `final_terminal_channel` - Export `--github-output` fields for Linux CI terminal publish/notification consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final outcome=`released` converges to `published` + `announce_release_closure`. - `T2`: final outcome=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_with_follow_up`. - `T3`: final outcome=`blocked` converges to `blocked` + `announce_blocker`. - `T4`: final outcome contract mismatch (decision/flags/target) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/run-id/run-url/report paths.- Depends on: `P2-48`## P2-50 Linux CI Workflow Release Final Handoff Gate- Card ID: `P2-50`- Priority: `P2`- Problem: - `P2-49` already converges final terminal publish semantics, but Linux CI still lacks one terminal handoff contract that downstream closure/archive consumers can consume directly without remapping publish statuses. - Without this dedicated handoff gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/handoff ownership semantics.- Scope: - Add release final handoff gate that consumes `P2-49` release final terminal publish report and emits: - `ci_workflow_release_final_handoff.json` - `ci_workflow_release_final_handoff.md` - Converge to normalized outputs: - `release_final_handoff_status` (`completed`/`completed_with_follow_up`/`blocked`/`contract_failed`) - `release_final_handoff_decision` (`handoff_release_closure`/`handoff_release_with_follow_up`/`handoff_blocker`/`abort_handoff`) - `release_final_handoff_exit_code` - `final_handoff_should_archive_release` / `final_handoff_keep_follow_up_open` / `final_handoff_should_page_owner` / `final_handoff_target` - Export `--github-output` fields for Linux CI terminal handoff/closure consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final terminal publish=`published` converges to `completed` + `handoff_release_closure`. - `T2`: final terminal publish=`published_with_follow_up` converges to `completed_with_follow_up` + `handoff_release_with_follow_up`. - `T3`: final terminal publish=`blocked` converges to `blocked` + `handoff_blocker`. - `T4`: final terminal publish contract mismatch (decision/flags/channel/target) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/archive+follow-up+page flags/target/queue-url/run-id/run-url/report paths.- Depends on: `P2-49`## P2-51 Linux CI Workflow Release Final Closure Gate- Card ID: `P2-51`- Priority: `P2`- Problem: - `P2-50` already converges final handoff semantics, but Linux CI still lacks one terminal closure contract that downstream owner-closure consumers can consume directly without remapping handoff statuses. - Without this dedicated closure gate, downstream automation still repeats custom logic for `completed_with_follow_up` versus `blocked` paths, increasing drift risk on final close ownership semantics.- Scope: - Add release final closure gate that consumes `P2-50` release final handoff report and emits: - `ci_workflow_release_final_closure.json` - `ci_workflow_release_final_closure.md` - Converge to normalized outputs: - `release_final_closure_status` (`closed`/`closed_with_follow_up`/`blocked`/`contract_failed`) - `release_final_closure_decision` (`close_release`/`close_with_follow_up`/`close_blocker`/`abort_close`) - `release_final_closure_exit_code` - `final_closure_is_closed` / `final_closure_has_open_follow_up` / `final_closure_should_page_owner` / `final_closure_target` - Export `--github-output` fields for Linux CI terminal closure consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final handoff=`completed` converges to `closed` + `close_release`. - `T2`: final handoff=`completed_with_follow_up` converges to `closed_with_follow_up` + `close_with_follow_up`. - `T3`: final handoff=`blocked` converges to `blocked` + `close_blocker`. - `T4`: final handoff contract mismatch (decision/flags/target) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/closed+follow-up+page flags/target/queue-url/run-id/run-url/report paths.- Depends on: `P2-50`## P2-52 Linux CI Workflow Release Final Closure Publish Gate- Card ID: `P2-52`- Priority: `P2`- Problem: - `P2-51` already converges final closure semantics, but Linux CI still lacks one terminal publish contract dedicated to final closure announcement semantics for downstream consumers. - Without this dedicated closure-publish gate, downstream automation still repeats custom logic for `closed_with_follow_up` versus `blocked` paths, increasing drift risk on final announce/manual-action semantics.- Scope: - Add release final closure publish gate that consumes `P2-51` release final closure report and emits: - `ci_workflow_release_final_closure_publish.json` - `ci_workflow_release_final_closure_publish.md` - Converge to normalized outputs: - `release_final_closure_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `release_final_closure_publish_decision` (`announce_release_closed`/`announce_release_closed_with_follow_up`/`announce_release_blocker`/`abort_publish`) - `release_final_closure_publish_exit_code` - `final_closure_publish_should_notify` / `final_closure_publish_requires_manual_action` / `final_closure_publish_channel` - Export `--github-output` fields for Linux CI final closure publish consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final closure=`closed` converges to `published` + `announce_release_closed`. - `T2`: final closure=`closed_with_follow_up` converges to `published_with_follow_up` + `announce_release_closed_with_follow_up`. - `T3`: final closure=`blocked` converges to `blocked` + `announce_release_blocker`. - `T4`: final closure contract mismatch (decision/flags/target) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths.- Depends on: `P2-51`## P2-53 Linux CI Workflow Release Final Archive Gate- Card ID: `P2-53`- Priority: `P2`- Problem: - `P2-52` already converges terminal closure publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. - Without this dedicated final-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics.- Scope: - Add release final archive gate that consumes `P2-52` release final closure publish report and emits: - `ci_workflow_release_final_archive.json` - `ci_workflow_release_final_archive.md` - Converge to normalized outputs: - `release_final_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) - `release_final_archive_decision` (`archive_release_closed`/`archive_release_closed_with_follow_up`/`archive_release_blocker`/`abort_archive`) - `release_final_archive_exit_code` - `final_archive_should_publish` / `final_archive_requires_manual_action` / `final_archive_channel` - Export `--github-output` fields for Linux CI final archive consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final closure publish=`published` converges to `archived` + `archive_release_closed`. - `T2`: final closure publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_closed_with_follow_up`. - `T3`: final closure publish=`blocked` converges to `blocked` + `archive_release_blocker`. - `T4`: final closure publish contract mismatch (decision/flags/channel) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/publish+manual-action/channel/queue-url/run-id/run-url/report paths.- Depends on: `P2-52`## P2-54 Linux CI Workflow Release Final Verdict Gate- Card ID: `P2-54`- Priority: `P2`- Problem: - `P2-53` already converges final archive semantics, but Linux CI still lacks one terminal final-verdict contract that downstream release decision consumers can consume directly without remapping archive statuses. - Without this dedicated final-verdict gate, downstream automation still repeats custom logic for `archived_with_follow_up` versus `blocked` paths, increasing drift risk on ship/manual-action semantics.- Scope: - Add release final verdict gate that consumes `P2-53` release final archive report and emits: - `ci_workflow_release_final_verdict.json` - `ci_workflow_release_final_verdict.md` - Converge to normalized outputs: - `release_final_verdict_status` (`released`/`released_with_follow_up`/`blocked`/`contract_failed`) - `release_final_verdict_decision` (`ship_release`/`ship_release_with_follow_up`/`escalate_release_blocker`/`abort_verdict`) - `release_final_verdict_exit_code` - `final_verdict_should_ship` / `final_verdict_requires_manual_action` / `final_verdict_channel` - Export `--github-output` fields for Linux CI final verdict consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final archive=`archived` converges to `released` + `ship_release`. - `T2`: final archive=`archived_with_follow_up` converges to `released_with_follow_up` + `ship_release_with_follow_up`. - `T3`: final archive=`blocked` converges to `blocked` + `escalate_release_blocker`. - `T4`: final archive contract mismatch (decision/flags/channel) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/ship+manual-action/channel/queue-url/run-id/run-url/report paths.- Depends on: `P2-53`## P2-55 Linux CI Workflow Release Final Verdict Publish Gate- Card ID: `P2-55`- Priority: `P2`- Problem: - `P2-54` already converges final verdict semantics, but Linux CI still lacks one terminal publish contract that downstream notification/audit consumers can consume directly without remapping verdict statuses. - Without this dedicated final-verdict-publish gate, downstream automation still repeats custom logic for `released_with_follow_up` versus `blocked` paths, increasing drift risk on notify/manual-action semantics.- Scope: - Add release final verdict publish gate that consumes `P2-54` release final verdict report and emits: - `ci_workflow_release_final_verdict_publish.json` - `ci_workflow_release_final_verdict_publish.md` - Converge to normalized outputs: - `release_final_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `release_final_verdict_publish_decision` (`announce_release_shipped`/`announce_release_shipped_with_follow_up`/`announce_release_blocker`/`abort_publish`) - `release_final_verdict_publish_exit_code` - `final_verdict_publish_should_notify` / `final_verdict_publish_requires_manual_action` / `final_verdict_publish_channel` - Export `--github-output` fields for Linux CI final verdict publish consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict=`released` converges to `published` + `announce_release_shipped`. - `T2`: final verdict=`released_with_follow_up` converges to `published_with_follow_up` + `announce_release_shipped_with_follow_up`. - `T3`: final verdict=`blocked` converges to `blocked` + `announce_release_blocker`. - `T4`: final verdict contract mismatch (decision/flags/channel) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action/channel/queue-url/run-id/run-url/report paths.- Depends on: `P2-54`## P2-56 Linux CI Workflow Release Final Publish Archive Gate- Card ID: `P2-56`- Priority: `P2`- Problem: - `P2-55` already converges final verdict publish semantics, but Linux CI still lacks one terminal archive contract that downstream archival/compliance consumers can consume directly without remapping publish statuses. - Without this dedicated final-publish-archive gate, downstream automation still repeats custom logic for `published_with_follow_up` versus `blocked` paths, increasing drift risk on archive/manual-action semantics.- Scope: - Add release final publish archive gate that consumes `P2-55` release final verdict publish report and emits: - `ci_workflow_release_final_publish_archive.json` - `ci_workflow_release_final_publish_archive.md` - Converge to normalized outputs: - `release_final_publish_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) - `release_final_publish_archive_decision` (`archive_release_shipped`/`archive_release_shipped_with_follow_up`/`archive_release_blocker`/`abort_archive`) - `release_final_publish_archive_exit_code` - `final_publish_archive_should_archive` / `final_publish_archive_requires_manual_action` / `final_publish_archive_channel` - Export `--github-output` fields for Linux CI final publish archive consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict publish=`published` converges to `archived` + `archive_release_shipped`. - `T2`: final verdict publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_shipped_with_follow_up`. - `T3`: final verdict publish=`blocked` converges to `blocked` + `archive_release_blocker`. - `T4`: final verdict publish contract mismatch (decision/flags/channel) converges to `contract_failed`. - `T5`: GitHub output includes status/decision/exit_code/archive+manual-action/channel/queue-url/run-id/run-url/report paths.- Depends on: `P2-55`## P2-57 Linux Gate Manifest Drift Closure Gate- Card ID: `P2-57`- Priority: `P2`- Problem: - As `P2-xx` gate cards keep growing, there is no single guard that continuously verifies closure across gate scripts, runtime contract tests, and Linux unified manifest wiring. - Without this closure gate, future card additions can silently miss `test_*_gate_runtime.py` or omit Linux manifest entries, only failing late in Linux stage.- Scope: - Add a dedicated drift-closure gate that scans: - `scripts/run_*_gate.py` (excluding `run_linux_unified_gate.py`) - expected `tests/test_*_gate_runtime.py` - `scripts/run_linux_unified_gate.py::LINUX_UNIFIED_TEST_FILES` - Emit normalized drift report artifacts: - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.md` - Export `--github-output` fields for CI observability: - status + missing-runtime-tests count + missing-manifest count + orphan-manifest count.- Implementation: - `scripts/run_p2_linux_gate_manifest_drift_gate.py` - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: gate script scan includes `run_p2_linux_gate_manifest_drift_gate.py` and excludes `run_linux_unified_gate.py`. - `T2`: expected runtime test mapping follows `run_xxx_gate.py` -> `test_xxx_gate_runtime.py`. - `T3`: repository baseline report converges to `status=passed` with zero missing runtime tests / manifest entries / orphan manifest entries. - `T4`: GitHub output fields expose status and drift counters.- Depends on: `P2-56`## P2-58 Linux CI Workflow Pipeline Closure to P2-57- Card ID: `P2-58`- Priority: `P2`- Problem: - `P2-57` introduces a dedicated drift-closure contract, but the Linux CI workflow pipeline gate still needs one canonical orchestration stage so full-chain execution can consume that contract without ad-hoc wiring. - Without explicit pipeline closure, downstream users can skip drift validation by accident or lose report-path consistency across dry-run and CI stages.- Scope: - Extend `P2-26` pipeline orchestration (`P2-17 -> ...`) to include one dedicated manifest-drift stage: - stage name: `workflow_gate_manifest_drift` - command: `scripts/run_p2_linux_gate_manifest_drift_gate.py` - Add normalized pipeline outputs: - `--gate-manifest-drift-json-output` - `--gate-manifest-drift-markdown-output` - Add skip-control flag: - `--skip-gate-manifest-drift` - Keep existing stage ordering and compatibility for all previous skip combinations.- Implementation: - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: default pipeline includes `workflow_gate_manifest_drift` after release final publish archive stage and before terminal verdict stage. - `T2`: command arguments include both drift output paths and point to `scripts/run_p2_linux_gate_manifest_drift_gate.py`. - `T3`: skip matrix propagates `skip_gate_manifest_drift` correctly in partial-chain and skip-all scenarios. - `T4`: P2-57-only path remains reproducible via pipeline skip switches.- Depends on: `P2-57`## P2-59 Linux CI Workflow Terminal Verdict Closure Gate- Card ID: `P2-59`- Priority: `P2`- Problem: - `P2-56` and `P2-57` are both terminal quality signals, but Linux CI still lacks one single contract that downstream automation can consume as a final go/no-go verdict. - Without a unified terminal verdict, Linux validation entry and release control logic must duplicate merge rules across two artifacts, increasing drift risk.- Scope: - Add one terminal verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json` (`P2-56`) - `.claude/reports/linux_unified_gate/linux_gate_manifest_drift.json` (`P2-57`) - Emit normalized outputs: - `terminal_verdict_status` (`ready_for_linux_validation`/`ready_with_follow_up_for_linux_validation`/`blocked`/`contract_failed`) - `terminal_verdict_decision` (`proceed_linux_validation`/`proceed_linux_validation_with_follow_up`/`halt_linux_validation_blocker`/`abort_linux_validation`) - `terminal_verdict_exit_code` - `terminal_verdict_should_proceed` / `terminal_verdict_requires_manual_action` / `terminal_verdict_channel` - Export `--github-output` fields for Linux CI consumers.- Implementation: - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: publish-archive=`archived` + drift=`passed` converges to `ready_for_linux_validation` + `proceed_linux_validation`. - `T2`: publish-archive=`archived_with_follow_up` + drift=`passed` converges to `ready_with_follow_up_for_linux_validation` + `proceed_linux_validation_with_follow_up`. - `T3`: publish-archive=`blocked` + drift=`passed` converges to `blocked` + `halt_linux_validation_blocker`. - `T4`: publish-archive mismatch or drift failure converges to `contract_failed` + `abort_linux_validation`. - `T5`: GitHub output includes status/decision/exit_code/proceed+manual-action/channel/drift counters/report paths.- Depends on: `P2-57`## P2-60 Linux CI Workflow Linux Validation Dispatch Gate- Card ID: `P2-60`- Priority: `P2`- Problem: - `P2-59` gives a terminal go/no-go contract, but Linux CI still lacks a single dispatch contract that transforms terminal verdict into an executable Linux validation action. - Without a dedicated Linux-validation-dispatch gate, downstream jobs still duplicate mapping logic across `ready_for_linux_validation` / `ready_with_follow_up_for_linux_validation` / `blocked` / `contract_failed` paths.- Scope: - Add one Linux validation dispatch gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json` (`P2-59`) - Emit normalized outputs: - `linux_validation_dispatch_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatched`/`dispatch_failed`/`blocked`/`contract_failed`) - `linux_validation_dispatch_decision` (`dispatch_linux_validation`/`dispatch_linux_validation_with_follow_up`/`hold_linux_validation_blocker`/`abort_linux_validation_dispatch`) - `linux_validation_dispatch_exit_code` - `linux_validation_should_dispatch` / `linux_validation_requires_manual_action` / `linux_validation_channel` - Support optional command execution with timeout control: - default command: `python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure` - optional override: `--linux-validation-command` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_dispatch_gate.py` - `tests/test_p2_lv_dispatch_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal verdict=`ready_for_linux_validation` converges to `ready_dry_run` + `dispatch_linux_validation`. - `T2`: terminal verdict=`ready_with_follow_up_for_linux_validation` converges to `ready_with_follow_up_dry_run` + `dispatch_linux_validation_with_follow_up`. - `T3`: terminal verdict=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. - `T4`: terminal verdict mismatch or drift/evidence issue converges to `contract_failed` + `abort_linux_validation_dispatch`. - `T5`: GitHub output includes status/decision/exit_code/dispatch+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-59`## P2-61 Linux CI Workflow Linux Validation Verdict Gate- Card ID: `P2-61`- Priority: `P2`- Problem: - `P2-60` converges Linux validation dispatch behavior, but Linux CI still lacks one terminal verdict artifact that downstream release governance can consume without re-parsing dispatch internals. - Without a dedicated Linux-validation-verdict gate, status handling for `dispatched/dispatch_failed/blocked/contract_failed` remains duplicated across consumers and can drift from terminal-verdict and manifest-drift contracts.- Scope: - Add one Linux validation verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json` (`P2-60`) - Emit normalized outputs: - `linux_validation_verdict_status` (`validated`/`validated_with_follow_up`/`validation_failed`/`blocked`/`contract_failed`) - `linux_validation_verdict_decision` (`accept_linux_validation`/`accept_linux_validation_with_follow_up`/`escalate_linux_validation_failure`/`hold_linux_validation_blocker`/`abort_linux_validation_verdict`) - `linux_validation_verdict_exit_code` - `linux_validation_passed` / `linux_validation_verdict_requires_manual_action` / `linux_validation_verdict_channel` - Support optional verdict command execution with timeout control: - optional override: `--linux-validation-verdict-command` - timeout control: `--linux-validation-verdict-timeout-seconds` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_verdict_gate.py` - `tests/test_p2_lv_verdict_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: dispatch=`dispatched` + terminal=`ready_for_linux_validation` converges to `validated` + `accept_linux_validation`. - `T2`: dispatch=`dispatched` + terminal=`ready_with_follow_up_for_linux_validation` converges to `validated_with_follow_up` + `accept_linux_validation_with_follow_up`. - `T3`: dispatch=`blocked` converges to `blocked` + `hold_linux_validation_blocker`. - `T4`: dispatch=`dispatch_failed` converges to `validation_failed` + `escalate_linux_validation_failure`; any contract mismatch/drift/evidence issue converges to `contract_failed` + `abort_linux_validation_verdict`. - `T5`: GitHub output includes verdict status/decision/exit_code/passed+manual-action/channel/dispatch status+decision/report paths.- Depends on: `P2-60`## P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate- Card ID: `P2-62`- Priority: `P2`- Problem: - `P2-61` converges Linux validation execution into one verdict contract, but Linux CI still lacks a dedicated publish contract that downstream release governance can consume without re-parsing verdict internals. - Without a publish gate, status handling for `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` can drift across dashboards, notifications, and final closure pipelines.- Scope: - Add one Linux validation verdict publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json` (`P2-61`) - Emit normalized outputs: - `linux_validation_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `linux_validation_verdict_publish_decision` (`announce_linux_validation_passed`/`announce_linux_validation_passed_with_follow_up`/`announce_linux_validation_blocker`/`abort_publish`) - `linux_validation_verdict_publish_exit_code` - `linux_validation_verdict_publish_should_notify` / `linux_validation_verdict_publish_requires_manual_action` / `linux_validation_verdict_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_verdict_publish_gate.py` - `tests/test_p2_lv_verdict_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: verdict=`validated` converges to `published` + `announce_linux_validation_passed`. - `T2`: verdict=`validated_with_follow_up` converges to `published_with_follow_up` + `announce_linux_validation_passed_with_follow_up`. - `T3`: verdict=`blocked` converges to `blocked` + `announce_linux_validation_blocker`. - `T4`: verdict contract mismatch/evidence issue converges to `contract_failed` + `abort_publish`. - `T5`: GitHub output includes publish status/decision/exit_code/notify+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-61`## P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate- Card ID: `P2-63`- Priority: `P2`- Problem: - `P2-62` converges Linux validation verdict publish behavior, but Linux CI still lacks one terminal publish contract that downstream release closure can consume without re-parsing publish internals. - Without a terminal publish artifact, handling for `published/published_with_follow_up/blocked/contract_failed` can drift across notification channels, observability endpoints, and handoff automation.- Scope: - Add one Linux validation terminal publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json` (`P2-62`) - Emit normalized outputs: - `linux_validation_terminal_publish_status` (`terminal_published`/`terminal_published_with_follow_up`/`terminal_blocked`/`terminal_contract_failed`) - `linux_validation_terminal_publish_decision` (`announce_linux_validation_terminal_passed`/`announce_linux_validation_terminal_passed_with_follow_up`/`announce_linux_validation_terminal_blocker`/`abort_terminal_publish`) - `linux_validation_terminal_publish_exit_code` - `linux_validation_terminal_publish_should_notify` / `linux_validation_terminal_publish_ready_for_handoff` / `linux_validation_terminal_publish_requires_manual_action` / `linux_validation_terminal_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_terminal_publish_gate.py` - `tests/test_p2_lv_terminal_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: verdict publish=`published` converges to `terminal_published` + `announce_linux_validation_terminal_passed`. - `T2`: verdict publish=`published_with_follow_up` converges to `terminal_published_with_follow_up` + `announce_linux_validation_terminal_passed_with_follow_up`. - `T3`: verdict publish=`blocked` converges to `terminal_blocked` + `announce_linux_validation_terminal_blocker`. - `T4`: verdict publish contract mismatch/evidence issue converges to `terminal_contract_failed` + `abort_terminal_publish`. - `T5`: GitHub output includes terminal publish status/decision/exit_code/notify+handoff+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-62`## P2-64 Linux CI Workflow Linux Validation Final Verdict Gate- Card ID: `P2-64`- Priority: `P2`- Problem: - `P2-63` converges Linux validation terminal publish behavior, but Linux CI still lacks one final verdict contract that downstream release closure and observability consumers can read without re-parsing terminal publish internals. - Without one final Linux-validation verdict artifact, status handling for `terminal_published/terminal_published_with_follow_up/terminal_blocked/terminal_contract_failed` can drift across release closure automation, notifications, and manual escalation paths.- Scope: - Add one Linux validation final verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json` (`P2-63`) - Emit normalized outputs: - `linux_validation_final_verdict_status` (`validated`/`validated_with_follow_up`/`blocked`/`contract_failed`) - `linux_validation_final_verdict_decision` (`accept_linux_validation_terminal`/`accept_linux_validation_terminal_with_follow_up`/`escalate_linux_validation_terminal_blocker`/`abort_linux_validation_terminal_verdict`) - `linux_validation_final_verdict_exit_code` - `linux_validation_final_should_accept` / `linux_validation_final_requires_follow_up` / `linux_validation_final_should_page_owner` / `linux_validation_final_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_final_verdict_gate.py` - `tests/test_p2_lv_final_verdict_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal publish=`terminal_published` converges to `validated` + `accept_linux_validation_terminal`. - `T2`: terminal publish=`terminal_published_with_follow_up` converges to `validated_with_follow_up` + `accept_linux_validation_terminal_with_follow_up`. - `T3`: terminal publish=`terminal_blocked` converges to `blocked` + `escalate_linux_validation_terminal_blocker`. - `T4`: terminal publish contract mismatch/evidence issue converges to `contract_failed` + `abort_linux_validation_terminal_verdict`. - `T5`: GitHub output includes final verdict status/decision/exit_code/accept+follow-up+page-owner/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-63`## P2-65 Linux CI Workflow Linux Validation Final Verdict Publish Gate- Card ID: `P2-65`- Priority: `P2`- Problem: - `P2-64` converges Linux validation into one final verdict contract, but Linux CI still lacks one final publish contract that downstream release closure, observability, and incident routing consumers can ingest without re-parsing final-verdict internals. - Without a dedicated publish artifact for final Linux-validation verdict, handling for `validated/validated_with_follow_up/blocked/contract_failed` can drift across notification channels and manual escalation automation.- Scope: - Add one Linux validation final verdict publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json` (`P2-64`) - Emit normalized outputs: - `linux_validation_final_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `linux_validation_final_verdict_publish_decision` (`announce_linux_validation_final_validated`/`announce_linux_validation_final_validated_with_follow_up`/`announce_linux_validation_final_blocker`/`abort_publish`) - `linux_validation_final_verdict_publish_exit_code` - `linux_validation_final_verdict_publish_should_notify` / `linux_validation_final_verdict_publish_requires_manual_action` / `linux_validation_final_verdict_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_final_verdict_publish_gate.py` - `tests/test_p2_lv_final_verdict_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict=`validated` converges to `published` + `announce_linux_validation_final_validated`. - `T2`: final verdict=`validated_with_follow_up` converges to `published_with_follow_up` + `announce_linux_validation_final_validated_with_follow_up`. - `T3`: final verdict=`blocked` converges to `blocked` + `announce_linux_validation_final_blocker`. - `T4`: final verdict contract mismatch/evidence issue converges to `contract_failed` + `abort_publish`. - `T5`: GitHub output includes publish status/decision/exit_code/notify+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-64`## P2-66 Linux CI Workflow Linux Validation Final Publish Archive Gate- Card ID: `P2-66`- Priority: `P2`- Problem: - `P2-65` already converges Linux validation final verdict publish behavior, but Linux CI still lacks one terminal archive contract for downstream release closure and audit consumers. - Without a dedicated final-publish-archive artifact, handling for `published/published_with_follow_up/blocked/contract_failed` can drift across archive/manual-action and follow-up routing automation.- Scope: - Add one Linux validation final publish archive gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json` (`P2-65`) - Emit normalized outputs: - `linux_validation_final_publish_archive_status` (`archived`/`archived_with_follow_up`/`blocked`/`contract_failed`) - `linux_validation_final_publish_archive_decision` (`archive_release_shipped`/`archive_release_shipped_with_follow_up`/`archive_release_blocker`/`abort_archive`) - `linux_validation_final_publish_archive_exit_code` - `linux_validation_final_publish_archive_should_archive` / `linux_validation_final_publish_archive_requires_manual_action` / `linux_validation_final_publish_archive_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_final_publish_archive_gate.py` - `tests/test_p2_lv_final_publish_archive_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict publish=`published` converges to `archived` + `archive_release_shipped`. - `T2`: final verdict publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_release_shipped_with_follow_up`. - `T3`: final verdict publish=`blocked` converges to `blocked` + `archive_release_blocker`. - `T4`: publish contract mismatch/evidence issue converges to `contract_failed` + `abort_archive`. - `T5`: GitHub output includes archive status/decision/exit_code/archive+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-65`## P2-67 Linux CI Workflow Linux Validation Terminal Verdict Gate- Card ID: `P2-67`- Priority: `P2`- Problem: - `P2-66` converges Linux validation final publish archive behavior, but Linux CI still lacks one terminal verdict contract for downstream Linux-stage orchestration consumers. - Without a dedicated terminal-verdict artifact, handling for `archived/archived_with_follow_up/blocked/contract_failed` can drift across final Linux validation dispatch policies and manual escalation paths.- Scope: - Add one Linux validation terminal verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json` (`P2-66`) - Emit normalized outputs: - `linux_validation_terminal_verdict_status` (`ready_for_linux_validation`/`ready_with_follow_up_for_linux_validation`/`blocked`/`contract_failed`) - `linux_validation_terminal_verdict_decision` (`proceed_linux_validation`/`proceed_linux_validation_with_follow_up`/`halt_linux_validation_blocker`/`abort_linux_validation`) - `linux_validation_terminal_verdict_exit_code` - `linux_validation_terminal_verdict_should_proceed` / `linux_validation_terminal_verdict_requires_manual_action` / `linux_validation_terminal_verdict_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_terminal_verdict_gate.py` - `tests/test_p2_lv_terminal_verdict_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final publish archive=`archived` converges to `ready_for_linux_validation` + `proceed_linux_validation`. - `T2`: final publish archive=`archived_with_follow_up` converges to `ready_with_follow_up_for_linux_validation` + `proceed_linux_validation_with_follow_up`. - `T3`: final publish archive=`blocked` converges to `blocked` + `halt_linux_validation_blocker`. - `T4`: final publish archive contract mismatch/evidence issue converges to `contract_failed` + `abort_linux_validation`. - `T5`: GitHub output includes terminal verdict status/decision/exit_code/proceed+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-66`## P2-68 Linux CI Workflow Linux Validation Terminal Verdict Publish Gate- Card ID: `P2-68`- Priority: `P2`- Problem: - `P2-67` converges Linux validation to one terminal verdict contract, but Linux CI still lacks a dedicated publish artifact that downstream dashboards and cross-pipeline consumers can subscribe to directly. - Without a terminal-verdict publish contract, handling for `ready_for_linux_validation/ready_with_follow_up_for_linux_validation/blocked/contract_failed` can drift across notification, follow-up routing, and blocker escalation automation.- Scope: - Add one Linux validation terminal verdict publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json` (`P2-67`) - Emit normalized outputs: - `linux_validation_terminal_verdict_publish_status` (`published`/`published_with_follow_up`/`blocked`/`contract_failed`) - `linux_validation_terminal_verdict_publish_decision` (`announce_linux_validation_terminal_ready`/`announce_linux_validation_terminal_ready_with_follow_up`/`announce_linux_validation_terminal_blocker`/`abort_publish`) - `linux_validation_terminal_verdict_publish_exit_code` - `linux_validation_terminal_verdict_publish_should_notify` / `linux_validation_terminal_verdict_publish_requires_manual_action` / `linux_validation_terminal_verdict_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_terminal_verdict_publish_gate.py` - `tests/test_p2_lv_terminal_verdict_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal verdict=`ready_for_linux_validation` converges to `published` + `announce_linux_validation_terminal_ready`. - `T2`: terminal verdict=`ready_with_follow_up_for_linux_validation` converges to `published_with_follow_up` + `announce_linux_validation_terminal_ready_with_follow_up`. - `T3`: terminal verdict=`blocked` converges to `blocked` + `announce_linux_validation_terminal_blocker`. - `T4`: terminal verdict contract mismatch/evidence issue converges to `contract_failed` + `abort_publish`. - `T5`: GitHub output includes terminal verdict publish status/decision/exit_code/notify+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-67`## P2-69 Linux CI Workflow Linux Validation Terminal Dispatch Gate- Card ID: `P2-69`- Priority: `P2`- Problem: - `P2-68` converges Linux validation terminal verdict publish behavior, but Linux CI still lacks one terminal dispatch contract for downstream Linux-stage dry-run orchestration consumers. - Without a dedicated terminal-dispatch artifact, handling for `published/published_with_follow_up/blocked/contract_failed` can drift across dispatch readiness, follow-up routing, and blocker escalation paths.- Scope: - Add one Linux validation terminal dispatch gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json` (`P2-68`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`blocked`/`contract_failed`) - `linux_validation_terminal_dispatch_decision` (`dispatch_linux_validation_terminal`/`dispatch_linux_validation_terminal_with_follow_up`/`hold_linux_validation_terminal_blocker`/`abort_linux_validation_terminal_dispatch`) - `linux_validation_terminal_dispatch_exit_code` - `linux_validation_terminal_should_dispatch` / `linux_validation_terminal_dispatch_requires_manual_action` / `linux_validation_terminal_dispatch_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_dispatch_gate.py` - `tests/test_p2_lv_td_dispatch_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal verdict publish=`published` converges to `ready_dry_run` + `dispatch_linux_validation_terminal`. - `T2`: terminal verdict publish=`published_with_follow_up` converges to `ready_with_follow_up_dry_run` + `dispatch_linux_validation_terminal_with_follow_up`. - `T3`: terminal verdict publish=`blocked` converges to `blocked` + `hold_linux_validation_terminal_blocker`. - `T4`: terminal verdict publish contract mismatch/evidence issue converges to `contract_failed` + `abort_linux_validation_terminal_dispatch`. - `T5`: GitHub output includes terminal dispatch status/decision/exit_code/dispatch+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-68`## P2-70 Linux CI Workflow Linux Validation Terminal Dispatch Execution Gate- Card ID: `P2-70`- Priority: `P2`- Problem: - `P2-69` converges Linux validation terminal dispatch readiness behavior, but Linux CI still lacks one execution contract that records terminal dispatch attempt outcomes for Linux-stage orchestration and incident routing consumers. - Without a dedicated terminal-dispatch execution artifact, handling for `ready_dry_run/ready_with_follow_up_dry_run/blocked/contract_failed` can drift when converting readiness into `dispatched/dispatch_failed` runtime outcomes.- Scope: - Add one Linux validation terminal dispatch execution gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json` (`P2-69`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_execution_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatched`/`dispatch_failed`/`blocked`/`contract_failed`) - `linux_validation_terminal_dispatch_execution_decision` (`dispatch_linux_validation_terminal`/`dispatch_linux_validation_terminal_with_follow_up`/`hold_linux_validation_terminal_blocker`/`abort_linux_validation_terminal_dispatch`) - `linux_validation_terminal_dispatch_execution_exit_code` - `linux_validation_terminal_dispatch_attempted` / `linux_validation_terminal_dispatch_execution_requires_manual_action` / `linux_validation_terminal_dispatch_execution_channel` - Support optional command execution with timeout control: - default command: `python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure` - optional override: `--linux-validation-terminal-command` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_execution_gate.py` - `tests/test_p2_lv_td_execution_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal dispatch=`ready_dry_run` converges to execution=`ready_dry_run` + `dispatch_linux_validation_terminal` in dry-run mode. - `T2`: terminal dispatch=`ready_with_follow_up_dry_run` with successful command converges to execution=`dispatched` + `dispatch_linux_validation_terminal_with_follow_up`. - `T3`: terminal dispatch=`blocked` converges to execution=`blocked` + `hold_linux_validation_terminal_blocker`. - `T4`: terminal dispatch contract mismatch is rejected at load boundary; runtime command failure converges to execution=`dispatch_failed`. - `T5`: GitHub output includes execution status/decision/exit_code/attempted+manual-action/channel/run-id/run-url/follow-up/report paths.- Depends on: `P2-69`## P2-71 Linux CI Workflow Linux Validation Terminal Dispatch Trace Gate- Card ID: `P2-71`- Priority: `P2`- Problem: - `P2-70` records Linux validation terminal dispatch execution outcomes, but Linux CI still lacks one standardized trace artifact for run-reference extraction and poll-ready tracking handoff. - Without a dedicated terminal-dispatch trace contract, run-state transitions (`tracking_ready`/`poll_failed`/`in_progress`/`completed`) can drift across downstream observability, incident, and handoff consumers.- Scope: - Add one Linux validation terminal dispatch trace gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json` (`P2-70`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_trace_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatch_failed`/`blocked`/`contract_failed`/`run_tracking_ready`/`run_tracking_missing`/`run_poll_failed`/`run_in_progress`/`run_completed_success`/`run_completed_failure`) - `linux_validation_terminal_dispatch_trace_exit_code` - `linux_validation_terminal_should_poll_workflow_run` / `linux_validation_terminal_poll_attempted` - run reference fields (`linux_validation_terminal_run_id` / `linux_validation_terminal_run_url` / `linux_validation_terminal_repo_owner` / `linux_validation_terminal_repo_name`) - Support optional immediate poll execution: - `--poll-now` + `--poll-timeout-seconds` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_trace_gate.py` - `tests/test_p2_lv_td_trace_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: execution=`dispatched` with run URL converges to `run_tracking_ready` and emits poll command contract. - `T2`: execution=`dispatched` without run URL converges to `run_tracking_missing`. - `T3`: execution=`dispatch_failed` keeps non-polling failure passthrough contract. - `T4`: execution payload contract mismatch is rejected at load boundary. - `T5`: GitHub output includes trace status/exit_code/should-poll/run-id/run-url/poll status + report paths.- Depends on: `P2-70`## P2-72 Linux CI Workflow Linux Validation Terminal Dispatch Completion Gate- Card ID: `P2-72`- Priority: `P2`- Problem: - `P2-71` converges Linux validation terminal dispatch trace behavior, but Linux CI still lacks one terminal completion-await contract that standardizes poll-loop outcomes for downstream handoff and closure consumers. - Without a dedicated terminal-dispatch completion artifact, transitions (`in_progress`/`completed`/`poll_failed`/`await_timeout`) can drift across Linux-stage final orchestration and escalation routing.- Scope: - Add one Linux validation terminal dispatch completion gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json` (`P2-71`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_completion_status` (`ready_dry_run`/`ready_with_follow_up_dry_run`/`dispatch_failed`/`blocked`/`contract_failed`/`run_tracking_missing`/`run_poll_failed`/`run_in_progress`/`run_completed_success`/`run_completed_failure`/`run_await_timeout`) - `linux_validation_terminal_dispatch_completion_exit_code` - `poll_attempted` / `poll_iterations` / poll result fields (`poll_status` / `poll_conclusion` / `poll_url` / `poll_error_type`) - Support poll-await behavior: - `--poll-interval-seconds` + `--max-polls` + `--poll-timeout-seconds` - optional `--allow-in-progress` for non-blocking in-progress outcomes - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_completion_gate.py` - `tests/test_p2_lv_td_completion_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: trace=`run_tracking_ready` with successful completion poll converges to `run_completed_success`. - `T2`: trace=`run_tracking_ready` with repeated `in_progress` poll until max attempts converges to `run_await_timeout` (or non-blocking via `--allow-in-progress`). - `T3`: trace payload in non-polling terminal states keeps passthrough completion contract. - `T4`: trace poll command contract mismatch is rejected at load boundary. - `T5`: GitHub output includes completion status/exit_code/poll-attempted/poll-iterations/run-id/run-url/reason-codes/report paths.- Depends on: `P2-71`## P2-73 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Publish Gate- Card ID: `P2-73`- Priority: `P2`- Problem: - `P2-72` converges Linux validation terminal dispatch completion await behavior, but Linux CI still lacks one terminal publish contract that standardizes downstream handoff-ready notification semantics. - Without a dedicated terminal-dispatch terminal-publish artifact, handling for `run_completed_success`/`run_in_progress`/`run_await_timeout`/`run_completed_failure` can drift across Linux-stage closure, follow-up routing, and blocker escalation consumers.- Scope: - Add one Linux validation terminal dispatch terminal publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json` (`P2-72`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_terminal_publish_status` (`terminal_published`/`terminal_published_with_follow_up`/`terminal_in_progress`/`terminal_blocked`/`terminal_failed`/`terminal_contract_failed`) - `linux_validation_terminal_dispatch_terminal_publish_decision` (`announce_linux_validation_terminal_dispatch_completed`/`announce_linux_validation_terminal_dispatch_completed_with_follow_up`/`announce_linux_validation_terminal_dispatch_in_progress`/`announce_linux_validation_terminal_dispatch_blocker`/`announce_linux_validation_terminal_dispatch_failed`/`abort_linux_validation_terminal_dispatch_terminal_publish`) - `linux_validation_terminal_dispatch_terminal_publish_exit_code` - `linux_validation_terminal_dispatch_terminal_publish_should_notify` / `linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff` / `linux_validation_terminal_dispatch_terminal_publish_requires_manual_action` / `linux_validation_terminal_dispatch_terminal_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_terminal_publish_gate.py` - `tests/test_p2_lv_td_terminal_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: completion=`run_completed_success` converges to `terminal_published` + `announce_linux_validation_terminal_dispatch_completed`. - `T2`: completion=`run_await_timeout` with allow-in-progress converges to `terminal_in_progress` + `announce_linux_validation_terminal_dispatch_in_progress`. - `T3`: completion status/exit-code mismatch converges to `terminal_contract_failed`. - `T4`: follow-up dispatch completion (`*_with_follow_up`) converges to `terminal_published_with_follow_up`. - `T5`: GitHub output includes status/decision/exit_code/notify+handoff+manual-action/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-72`## P2-74 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Gate- Card ID: `P2-74`- Priority: `P2`- Problem: - `P2-73` converges Linux validation terminal dispatch completion behavior into one terminal publish contract, but Linux CI still lacks one final verdict contract that downstream release closure and audit consumers can read without re-parsing terminal-publish internals. - Without a dedicated terminal-dispatch final-verdict artifact, handling for `terminal_published`/`terminal_in_progress`/`terminal_failed` can drift across Linux-stage acceptance, follow-up routing, and blocker escalation paths.- Scope: - Add one Linux validation terminal dispatch final verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json` (`P2-73`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_final_verdict_status` (`validated`/`validated_with_follow_up`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_final_verdict_decision` (`accept_linux_validation_terminal_dispatch`/`accept_linux_validation_terminal_dispatch_with_follow_up`/`keep_linux_validation_terminal_dispatch_in_progress`/`escalate_linux_validation_terminal_dispatch_blocker`/`escalate_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_final_verdict`) - `linux_validation_terminal_dispatch_final_verdict_exit_code` - `linux_validation_terminal_dispatch_final_should_accept` / `linux_validation_terminal_dispatch_final_requires_follow_up` / `linux_validation_terminal_dispatch_final_should_page_owner` / `linux_validation_terminal_dispatch_final_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_final_verdict_gate.py` - `tests/test_p2_lv_td_final_verdict_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal publish=`terminal_published` converges to `validated` + `accept_linux_validation_terminal_dispatch`. - `T2`: terminal publish=`terminal_published_with_follow_up` converges to `validated_with_follow_up` + `accept_linux_validation_terminal_dispatch_with_follow_up`. - `T3`: terminal publish=`terminal_blocked` converges to `blocked` + `escalate_linux_validation_terminal_dispatch_blocker`. - `T4`: terminal publish contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_final_verdict`. - `T5`: GitHub output includes status/decision/exit_code/accept+follow-up+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-73`## P2-75 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Publish Gate- Card ID: `P2-75`- Priority: `P2`- Problem: - `P2-74` converges Linux validation terminal dispatch completion chain into one final verdict contract, but Linux CI still lacks one publish contract that terminally announces validated/in-progress/blocked/failed outcomes for downstream consumers. - Without a dedicated final-verdict-publish artifact, notification, manual-action, and escalation semantics can drift across Linux-stage closure and operational handoff paths.- Scope: - Add one Linux validation terminal dispatch final verdict publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json` (`P2-74`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_final_verdict_publish_status` (`published`/`published_with_follow_up`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_final_verdict_publish_decision` (`announce_linux_validation_terminal_dispatch_validated`/`announce_linux_validation_terminal_dispatch_validated_with_follow_up`/`announce_linux_validation_terminal_dispatch_in_progress`/`announce_linux_validation_terminal_dispatch_blocker`/`announce_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_final_verdict_publish`) - `linux_validation_terminal_dispatch_final_verdict_publish_exit_code` - `linux_validation_terminal_dispatch_final_verdict_publish_should_notify` / `linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action` / `linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner` / `linux_validation_terminal_dispatch_final_verdict_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_final_verdict_publish_gate.py` - `tests/test_p2_lv_td_final_verdict_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict=`validated` converges to `published` + `announce_linux_validation_terminal_dispatch_validated`. - `T2`: final verdict=`validated_with_follow_up` converges to `published_with_follow_up` + `announce_linux_validation_terminal_dispatch_validated_with_follow_up`. - `T3`: final verdict=`blocked` converges to `blocked` + `announce_linux_validation_terminal_dispatch_blocker`. - `T4`: final verdict contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_final_verdict_publish`. - `T5`: GitHub output includes status/decision/exit_code/notify+manual-action+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-74`## P2-76 Linux CI Workflow Linux Validation Terminal Dispatch Final Publish Archive Gate- Card ID: `P2-76`- Priority: `P2`- Problem: - `P2-75` converges Linux validation terminal dispatch final verdict into one publish contract, but Linux CI still lacks one terminal archive contract that downstream closure and audit consumers can ingest directly. - Without a dedicated final-publish-archive artifact, archive/manual-action/escalation semantics for published/in-progress/blocked/failed outcomes can drift across Linux-stage closure and operations handoff paths.- Scope: - Add one Linux validation terminal dispatch final publish archive gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json` (`P2-75`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_final_publish_archive_status` (`archived`/`archived_with_follow_up`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_final_publish_archive_decision` (`archive_linux_validation_terminal_dispatch_validated`/`archive_linux_validation_terminal_dispatch_validated_with_follow_up`/`archive_linux_validation_terminal_dispatch_in_progress`/`archive_linux_validation_terminal_dispatch_blocker`/`archive_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_final_publish_archive`) - `linux_validation_terminal_dispatch_final_publish_archive_exit_code` - `linux_validation_terminal_dispatch_final_publish_archive_should_archive` / `linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action` / `linux_validation_terminal_dispatch_final_publish_archive_should_page_owner` / `linux_validation_terminal_dispatch_final_publish_archive_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_final_publish_archive_gate.py` - `tests/test_p2_lv_td_final_publish_archive_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final verdict publish=`published` converges to `archived` + `archive_linux_validation_terminal_dispatch_validated`. - `T2`: final verdict publish=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_linux_validation_terminal_dispatch_validated_with_follow_up`. - `T3`: final verdict publish=`in_progress` converges to `in_progress` + `archive_linux_validation_terminal_dispatch_in_progress`. - `T4`: final verdict publish=`blocked`/`failed` converges to `blocked`/`failed` archive outcomes and keeps manual-action/page-owner semantics. - `T5`: final verdict publish contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_final_publish_archive`. - `T6`: GitHub output includes status/decision/exit_code/archive+manual-action+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-75`## P2-77 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Gate- Card ID: `P2-77`- Priority: `P2`- Problem: - `P2-76` converges Linux validation terminal dispatch final publish archive semantics, but Linux CI still lacks one terminal verdict contract that gates whether downstream Linux terminal dispatch should proceed, hold, or halt. - Without one terminal-verdict artifact, proceed/manual-action/escalation semantics can drift between archive closure and downstream Linux-stage dispatch orchestration.- Scope: - Add one Linux validation terminal dispatch terminal verdict gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json` (`P2-76`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_terminal_verdict_status` (`ready_for_linux_validation_terminal_dispatch`/`ready_with_follow_up_for_linux_validation_terminal_dispatch`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_terminal_verdict_decision` (`proceed_linux_validation_terminal_dispatch`/`proceed_linux_validation_terminal_dispatch_with_follow_up`/`hold_linux_validation_terminal_dispatch_in_progress`/`halt_linux_validation_terminal_dispatch_blocker`/`halt_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_terminal_verdict`) - `linux_validation_terminal_dispatch_terminal_verdict_exit_code` - `linux_validation_terminal_dispatch_terminal_verdict_should_proceed` / `linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action` / `linux_validation_terminal_dispatch_terminal_verdict_should_page_owner` / `linux_validation_terminal_dispatch_terminal_verdict_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_terminal_verdict_gate.py` - `tests/test_p2_lv_td_terminal_verdict_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: final publish archive=`archived` converges to `ready_for_linux_validation_terminal_dispatch` + `proceed_linux_validation_terminal_dispatch`. - `T2`: final publish archive=`archived_with_follow_up` converges to `ready_with_follow_up_for_linux_validation_terminal_dispatch` + `proceed_linux_validation_terminal_dispatch_with_follow_up`. - `T3`: final publish archive=`in_progress` converges to `in_progress` + `hold_linux_validation_terminal_dispatch_in_progress`. - `T4`: final publish archive=`blocked`/`failed` converges to `blocked`/`failed` halt outcomes and preserves manual-action/page-owner semantics. - `T5`: final publish archive contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_terminal_verdict`. - `T6`: GitHub output includes status/decision/exit_code/proceed+manual-action+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-76`## P2-78 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Gate- Card ID: `P2-78`- Priority: `P2`- Problem: - `P2-77` converges Linux validation terminal dispatch final publish archive outcomes into one terminal verdict contract, but Linux CI still lacks one terminal verdict publish contract for downstream audit, notifications, and closure consumers. - Without one terminal-verdict-publish artifact, notification/manual-action/escalation semantics can drift between terminal verdict and Linux-stage closure dispatch.- Scope: - Add one Linux validation terminal dispatch terminal verdict publish gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json` (`P2-77`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_terminal_verdict_publish_status` (`published`/`published_with_follow_up`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_terminal_verdict_publish_decision` (`announce_linux_validation_terminal_dispatch_ready`/`announce_linux_validation_terminal_dispatch_ready_with_follow_up`/`announce_linux_validation_terminal_dispatch_in_progress`/`announce_linux_validation_terminal_dispatch_blocker`/`announce_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_terminal_verdict_publish`) - `linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code` - `linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify` / `linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action` / `linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner` / `linux_validation_terminal_dispatch_terminal_verdict_publish_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_tverdict_publish_gate.py` - `tests/test_p2_lv_td_tverdict_publish_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: terminal verdict=`ready_for_linux_validation_terminal_dispatch` converges to `published` + `announce_linux_validation_terminal_dispatch_ready`. - `T2`: terminal verdict=`ready_with_follow_up_for_linux_validation_terminal_dispatch` converges to `published_with_follow_up` + `announce_linux_validation_terminal_dispatch_ready_with_follow_up`. - `T3`: terminal verdict=`in_progress` converges to `in_progress` + `announce_linux_validation_terminal_dispatch_in_progress`. - `T4`: terminal verdict=`blocked`/`failed` converges to `blocked`/`failed` announce outcomes and preserves manual-action/page-owner semantics. - `T5`: terminal verdict contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_terminal_verdict_publish`. - `T6`: GitHub output includes status/decision/exit_code/notify+manual-action+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-77`## P2-79 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Archive Gate- Card ID: `P2-79`- Priority: `P2`- Problem: - `P2-78` converges Linux validation terminal dispatch terminal verdict into one publish artifact, but Linux CI still lacks one archive contract for this publish stage. - Without a terminal-verdict-publish-archive artifact, downstream closure/audit consumers cannot rely on one stable terminal dispatch archive status.- Scope: - Add one Linux validation terminal dispatch terminal verdict publish archive gate that consumes: - `.claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json` (`P2-78`) - Emit normalized outputs: - `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status` (`archived`/`archived_with_follow_up`/`in_progress`/`blocked`/`failed`/`contract_failed`) - `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision` (`archive_linux_validation_terminal_dispatch_ready`/`archive_linux_validation_terminal_dispatch_ready_with_follow_up`/`archive_linux_validation_terminal_dispatch_in_progress`/`archive_linux_validation_terminal_dispatch_blocker`/`archive_linux_validation_terminal_dispatch_failure`/`abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive`) - `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code` - `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive` / `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action` / `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner` / `linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel` - Export `--github-output` fields for CI consumers.- Implementation: - `scripts/run_p2_lv_td_tverdict_publish_archive_gate.py` - `tests/test_p2_lv_td_tverdict_publish_archive_rt.py` - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` - `scripts/run_linux_unified_gate.py` - `README.md` - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md`- Test Contract: - `T1`: publish status=`published` converges to `archived` + `archive_linux_validation_terminal_dispatch_ready`. - `T2`: publish status=`published_with_follow_up` converges to `archived_with_follow_up` + `archive_linux_validation_terminal_dispatch_ready_with_follow_up`. - `T3`: publish status=`in_progress` converges to `in_progress` + `archive_linux_validation_terminal_dispatch_in_progress`. - `T4`: publish status=`blocked`/`failed` converges to `blocked`/`failed` archive outcomes and preserves manual-action/page-owner semantics. - `T5`: publish contract mismatch converges to `contract_failed` + `abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive`. - `T6`: GitHub output includes status/decision/exit_code/archive+manual-action+page-owner/channel/follow-up/run-id/run-url/report paths.- Depends on: `P2-78`## 8. 鎺ㄨ崘杩唬椤哄簭鑻ュ彧鍏佽涓€杞竴杞帹杩涳紝寤鸿椤哄簭濡備笅锛?1. `P0-01`2. `P0-02`3. `P0-03`4. `P0-04`5. `P0-05`6. `P0-06`7. `P0-07`8. `P0-09`9. `P0-08`10. `P0-10`璇存槑锛?- `P0-08` 涓嶉樆濉炰富鍔熻兘淇锛屽洜姝ゆ斁鍦?`P0-09` 鍚庨潰浜﹀彲- `P0-10` 浣滀负鏀跺彛鍗★紝搴旀斁鍦?Phase 0 灏鹃儴## 9. 姣忚疆杩唬鐨勬渶灏忎骇鍑鸿姹?姣忓畬鎴愪竴寮犲崱锛岃嚦灏戝悓姝ヤ互涓嬩骇鐗╋細1. 浠g爜鍙樻洿2. 娴嬭瘯鍙樻洿3. 鏂囨。鍙樻洿4. 鎵嬪伐楠屾敹鍛戒护5. 椋庨櫓涓庡墿浣欑洸鍖?寤鸿姣忓紶鍗$殑浜や粯璇存槑缁熶竴閲囩敤锛?```text銆愪换鍔″崱銆慞0-XX銆愯В鍐炽€戔€︹€?銆愬畬鎴愩€戔€︹€?銆愭祴璇曘€戔€︹€?銆愰闄┿€戔€︹€?銆愪笅涓€鍗°€戔€︹€?```## 10. 褰撳墠鏈€閫傚悎绔嬪嵆寮€宸ョ殑鍗″崟鑻ョ珛鍒昏 Codex 涓嬪垁锛屾帹鑽愮涓€鎵瑰紑宸ュ崱锛?1. `P0-01 缁熶竴涓昏繍琛屾椂楠ㄦ灦`2. `P0-02 /resume 鐪熼棴鐜痐3. `P0-03 auth/config 涓婚摼淇涓庣粺涓€`4. `P0-04 feature flag 杩愯鏃朵慨澶峘5. `P0-05 review 鍛戒护浜у搧鍖朻鐞嗙敱锛?- 杩?5 寮犲崱鐩存帴鍐冲畾椤圭洰鏄惁浠庘€滃彲璺戜唬鐮佲€濊繘鍏モ€滃彲鏂藉伐浜у搧鈥?- 瀹冧滑閮藉睘浜庨珮纭畾鎬с€侀珮鏀剁泭銆佸彲鏈湴楠岃瘉鐨勪换鍔?- 鍏舵祴璇曞洖鎶ョ巼鏈€楂橈紝涓旇兘涓哄悗缁钩鍙板寲鍗″崟鎵竻闅滅## 11. 鏂囨。缁存姢瑙勫垯- 鑻ユ煇寮犱换鍔″崱杩涘叆寮€鍙戯紝鏂板鎵ц璁板綍鏃朵紭鍏堟洿鏂版湰鏂囦欢锛岃€屼笉鏄钩琛屾柊寤衡€滃悓涓婚灏忔枃妗b€?- 鑻ヤ竴寮犱换鍔″崱琚户缁媶鍒嗭紝浣跨敤瀛愮紪鍙凤細 - `P0-03A` - `P0-03B`- 鑻ョ瓥鐣ュ彉鍖栧奖鍝嶄换鍔¤竟鐣岋紝鍏堟洿鏂颁笂娓告垬鐣ユ枃妗o紝鍐嶅洖鍐欐湰鏂囦欢## 12. 缁撹鏈枃妗g殑鐩爣涓嶆槸鈥滃垪寰呭姙浜嬮」鈥濓紝鑰屾槸寤虹珛涓€涓爺鍙戝彲鎵ц銆佹祴璇曞彲楠屾敹銆佺鐞嗗彲璺熻釜鐨勬柦宸ラ潰銆?浠庣幇鍦ㄥ紑濮嬶紝鍚庣画鎵€鏈夊紑宸ュ缓璁兘搴斿熀浜庝换鍔″崱锛岃€屼笉鏄熀浜庢ā绯婃剰鍥俱€?鎹㈣█涔嬶細> 鍏堟寜鍗″紑宸ワ紝鍐嶆寜鍗¢獙灏革紝鍐嶆寜鍗℃帹杩涳紝涓嶅啀鍑劅瑙夊仛閲嶆瀯銆?## 13. Execution Log (2026-04-29)- Card: `P0-01 Runtime Backbone`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/main.py` - `claude_code/mcp/server.py`- Test scripts landed: - `tests/test_main_runtime.py` (runtime bootstrap + mcp serve wiring coverage) - `tests/test_runtime_bootstrap.py` (entrypoint consistency + mcp registry injection)- Not executed locally per release policy: - Unit/component/integration tests will be executed in Linux unified validation stage.## 14. Execution Log (2026-04-29, P0-02)- Card: `P0-02 /resume Runtime Closure`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/commands/compact/__init__.py` - `claude_code/engine/query.py` - `claude_code/repl/__init__.py`- Test scripts landed: - `tests/test_resume_runtime.py`- Not executed locally per release policy: - Resume-related unit/component/integration checks will run in Linux unified validation stage.## 15. Execution Log (2026-04-29, P0-03)- Card: `P0-03 Auth/Config Runtime Unification`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/commands/auth/__init__.py`- Test scripts landed: - `tests/test_commands_auth_runtime.py` - `tests/test_main_runtime.py` (auth key precedence coverage extension)- Not executed locally per release policy: - Auth/config unit/component/integration checks will run in Linux unified validation stage.## 16. Execution Log (2026-04-29, P0-04)- Card: `P0-04 Feature Flag Runtime Repair`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/features.py`- Test scripts landed: - `tests/test_features_runtime.py`- Not executed locally per release policy: - Feature flag unit/component checks will run in Linux unified validation stage.## 17. Execution Log (2026-04-29, P0-05)- Card: `P0-05 Review Command Productization`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/commands/review/__init__.py` - `claude_code/services/review_service.py`- Test scripts landed: - `tests/test_review_command_runtime.py`- Not executed locally per release policy: - Review command unit/component/integration checks will run in Linux unified validation stage.## 18. Execution Log (2026-04-29, P0-06)- Card: `P0-06 Session/History/Memory Boundary Convergence`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/engine/query.py` - `claude_code/engine/session.py` - `claude_code/services/history_manager.py` - `claude_code/services/memory_service.py` - `claude_code/main.py`- Test scripts landed: - `tests/test_session_history_memory_runtime.py` - `tests/test_context_builder_runtime.py` (boundary assertions extension) - `tests/test_main_runtime.py` (runtime service wiring extension)- Not executed locally per release policy: - Session/history/memory unit/component/integration checks will run in Linux unified validation stage.## 19. Execution Log (2026-04-29, P0-07)- Card: `P0-07 Hooks Runtime Integration`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/hooks_manager.py` - `claude_code/commands/hooks/__init__.py` - `claude_code/engine/query.py` - `claude_code/repl/__init__.py` - `claude_code/main.py`- Test scripts landed: - `tests/test_hooks_runtime.py` - `tests/test_main_runtime.py` (hooks manager runtime bootstrap wiring update)- Not executed locally per release policy: - Hooks runtime unit/component/integration checks will run in Linux unified validation stage.## 20. Execution Log (2026-04-29, P0-09)- Card: `P0-09 Temp Artifact Hygiene`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `tests/test_plugins_runtime.py` - `.gitignore`- Test scripts landed: - `tests/test_plugins_runtime.py` (fixture teardown cleanup assertion + idempotent cleanup coverage)- Not executed locally per release policy: - Plugin runtime cleanup and residue-isolation checks will run in Linux unified validation stage.## 21. Execution Log (2026-04-29, P0-08)- Card: `P0-08 Windows Bootstrap & Interpreter Probe Stabilization`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/main.py` - `README.md`- Test scripts landed: - `tests/test_doctor_runtime.py`- Not executed locally per release policy: - Doctor interpreter-detection unit/component checks and Windows stub regression cases will run in Linux unified validation stage.## 22. Execution Log (2026-04-29, P0-10)- Card: `P0-10 Phase 0 Regression Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `pytest.ini` - `scripts/run_phase0_gate.py` - `README.md`- Test scripts landed: - `tests/test_phase0_regression_contract.py`- Not executed locally per release policy: - Phase 0 gate contract and curated regression-entry checks will run in Linux unified validation stage.## 23. Execution Log (2026-04-29, P1-01)- Card: `P1-01 Daemon/API Control Plane`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/server/control_plane.py` - `claude_code/server/__init__.py` - `claude_code/main.py` - `scripts/run_p1_control_plane_gate.py` - `README.md`- Test scripts landed: - `tests/test_server_runtime.py` (`T1` schema validation, `T2` service behavior, `T3` client->server->runtime chain, `T4` daemon down/timeout/session-not-found) - `tests/test_main_runtime.py` (daemon CLI bootstrap wiring coverage)- Not executed locally per release policy: - Daemon/API unit/component/integration/regression checks will run in Linux unified validation stage.## 24. Execution Log (2026-04-29, P1-02)- Card: `P1-02 Event Journal`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/event_journal.py` - `claude_code/services/__init__.py` - `claude_code/tasks/manager.py` - `claude_code/tasks/factory.py` - `claude_code/tasks/__init__.py` - `claude_code/engine/query.py` - `claude_code/server/control_plane.py` - `claude_code/main.py` - `scripts/run_p1_event_journal_gate.py` - `README.md`- Test scripts landed: - `tests/test_event_journal_runtime.py` (`T1` schema/versioning, `T2` writer/reader correctness, `T3` query->tool->task event chain, `T4` partial write/corruption recovery) - `tests/test_server_runtime.py` (event list/replay daemon API coverage) - `tests/test_main_runtime.py` (runtime bootstrap wiring includes event journal)- Not executed locally per release policy: - Event journal unit/component/integration/regression checks will run in Linux unified validation stage.## 25. Execution Log (2026-04-29, P1-03)- Card: `P1-03 SQLite Runtime State Backend`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/tasks/repository.py` (SQLite runtime repository + schema version/migration guard) - `claude_code/tasks/factory.py` (runtime backend default switched to `sqlite`) - `claude_code/tasks/__init__.py` (SQLite repository exports) - `claude_code/engine/session.py` (SQLite session store + manager constructor hook) - `claude_code/services/event_journal.py` (SQLite event journal + schema version/migration guard) - `claude_code/services/__init__.py` (SQLite event journal exports) - `claude_code/engine/query.py` (event journal duck-typing for backend-agnostic write path) - `claude_code/main.py` (runtime bootstrap switched to unified `.claude/runtime_state.db`) - `scripts/run_p1_sqlite_state_backend_gate.py` - `README.md`- Test scripts landed: - `tests/test_sqlite_runtime_state_backend.py` (`T1` schema init/version guard, `T2` repository/session/event correctness, `T3` runtime chain wiring) - `tests/test_tasks_backend_contract.py` (SQLite repository contract + schema guard extension) - `tests/test_tasks_factory_runtime.py` (default runtime backend contract update to `sqlite`) - `tests/test_tasks_manager_runtime.py` (SQLite lifecycle persistence/recovery extension) - `tests/test_event_journal_runtime.py` (SQLite event journal schema/query/replay guard extension) - `tests/test_main_runtime.py` (runtime bootstrap mocks updated for SQLite state wiring)- Not executed locally per release policy: - SQLite runtime state backend unit/component/integration/regression checks will run in Linux unified validation stage.## 26. Execution Log (2026-04-30, P1-04)- Card: `P1-04 Active Memory Runtime Integration`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/memory_service.py` (scoped memory namespaces + scope snapshot API) - `claude_code/engine/query.py` (active memory hit audit event + runtime memory scope propagation) - `claude_code/main.py` (system prompt active memory snapshot injection + runtime memory scope wiring) - `claude_code/tools/base.py` (tool context memory scope) - `claude_code/tools/agent/__init__.py` (agent-level memory scope propagation) - `claude_code/agents/builtin.py` (builtin agent memory scope defaults) - `scripts/run_p1_active_memory_gate.py` - `README.md`- Test scripts landed: - `tests/test_memory_scope_runtime.py` (`T1` scope namespace isolation, scoped snapshot boundaries) - `tests/test_active_memory_runtime.py` (`T2` system-prompt memory injection, `T3` query memory-hit event chain, runtime scope propagation)- Not executed locally per release policy: - Active memory unit/component/integration/regression checks will run in Linux unified validation stage.## 27. Execution Log (2026-04-30, P1-05)- Card: `P1-05 Hook/Permission/Audit Convergence`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/permissions.py` (structured permission evaluation reason output for audit chain) - `claude_code/services/hooks_manager.py` (hook result serialization helper) - `claude_code/engine/query.py` (permission request/decision events + hook execution audit events) - `scripts/run_p1_hook_permission_audit_gate.py` - `README.md`- Test scripts landed: - `tests/test_hook_permission_audit_runtime.py` (`T2` permission+hook+audit linkage, `T3` tool execution audit chain, `T4` deny/error partial-failure path)- Not executed locally per release policy: - Hook/permission/audit convergence unit/component/integration/regression checks will run in Linux unified validation stage.## 28. Execution Log (2026-04-30, P1-06)- Card: `P1-06 CLI Thin Client Migration`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/main.py` (daemon thin-client mode for query/pipe path, daemon-required hard-fail option, local fallback branch) - `scripts/run_p1_cli_thin_client_gate.py` - `README.md`- Test scripts landed: - `tests/test_cli_daemon_thin_client_runtime.py` (`T2` client adapter mode resolution contract) - `tests/test_main_runtime.py` (daemon thin-client success/fallback/fail-fast and pipe-mode chain coverage)- Not executed locally per release policy: - CLI thin-client adapter and daemon-fallback regression checks will run in Linux unified validation stage.## 29. Execution Log (2026-04-30, P2-01)- Card: `P2-01 Multi-Agent Supervisor`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/multi_agent_supervisor.py` (agent DAG scheduler, budget/timeout/retry policy, dependency artifact passing, parallel fan-out and aggregate close) - `claude_code/services/__init__.py` (supervisor exports) - `claude_code/tools/agent/__init__.py` (orchestrate mode + workflow schema + supervisor integration with structured artifact summary) - `scripts/run_p2_multi_agent_supervisor_gate.py` - `README.md`- Test scripts landed: - `tests/test_multi_agent_supervisor_runtime.py` (`T1` graph cycle guard, `T2` budget/timeout state machine, `T3` parallel scheduling, `T4` retry-based failure recovery; plus agent tool orchestrate path and config error branch)- Not executed locally per release policy: - Multi-agent supervisor unit/component/integration/regression checks will run in Linux unified validation stage.## 30. Execution Log (2026-04-30, P2-02)- Card: `P2-02 Artifact Bus & Conflict Convergence`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/artifact_bus.py` (artifact schema contract for patch/note/diff/report/finding, producer payload parsing, merge strategies `fail|append|replace`, conflict error path) - `claude_code/services/multi_agent_supervisor.py` (artifact bus integration for dependency artifact passing, workflow-level artifact snapshot, node artifact conflict/scheme failure handling) - `claude_code/services/__init__.py` (artifact bus exports) - `scripts/run_p2_artifact_bus_gate.py` - `README.md`- Test scripts landed: - `tests/test_artifact_bus_runtime.py` (`T1` schema validation, `T2` producer/consumer dependency view, `T4` conflict-fail path, append merge behavior) - `tests/test_multi_agent_supervisor_runtime.py` (artifact bus summary extension + conflict merge failure branch in DAG execution)- Not executed locally per release policy: - Artifact bus unit/component/integration/regression checks will run in Linux unified validation stage.## 31. Execution Log (2026-04-30, P2-03)- Card: `P2-03 IDE Integration (VS Code first)`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/server/control_plane.py` (added `/api/v1/ide/workspace` endpoint for IDE aggregate snapshot: diff, changed files, tasks, sessions, findings) - `claude_code/server/ide_adapter.py` (VS Code client adapter and normalized workspace snapshot model) - `claude_code/server/__init__.py` (IDE adapter exports) - `scripts/run_p2_ide_integration_gate.py` - `README.md`- Test scripts landed: - `tests/test_ide_integration_runtime.py` (`T2` client protocol adapter normalization, `T3` IDE -> daemon core chain for diff/task/session/findings)- Not executed locally per release policy: - IDE integration unit/component/integration/manual acceptance checks will run in Linux unified validation stage.## 32. Execution Log (2026-04-30, P2-04)- Card: `P2-04 GitHub/CI Workflow Agent`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/github_ci_workflow.py` (issue->plan->code->test->review->PR state machine, headless CI mode, command failure classification, structured workflow report) - `claude_code/server/control_plane.py` (added `POST /api/v1/workflows/github-ci` endpoint + client helper for workflow execution) - `claude_code/services/__init__.py` (GitHub/CI workflow service exports) - `scripts/run_p2_github_ci_workflow_gate.py` - `README.md`- Test scripts landed: - `tests/test_github_ci_workflow_runtime.py` (`T2` workflow state machine, `T3` fake repo/fake PR main chain, `T4` permission-denied/repo-dirty/network-failure branches) - `tests/test_server_runtime.py` (daemon API route coverage for workflow success/failure mapping)- Not executed locally per release policy: - GitHub/CI workflow unit/component/integration/regression checks will run in Linux unified validation stage.## 33. Execution Log (2026-04-30, P2-05)- Card: `P2-05 Org Policy & Audit`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/services/org_policy_audit.py` (org policy matcher, secret redaction, approval queue transitions, audit event query/report) - `claude_code/server/control_plane.py` (added org policy endpoints and client helpers: evaluate/approve/list/report) - `claude_code/services/__init__.py` (Org policy audit service exports) - `scripts/run_p2_org_policy_audit_gate.py` - `README.md`- Test scripts landed: - `tests/test_org_policy_audit_runtime.py` (`T1` policy matcher+redaction, `T2` approval transitions, `T3` audit report chain, `T4` policy conflict block) - `tests/test_server_runtime.py` (daemon API coverage for org policy evaluate/approval/audit/report and failure mapping)- Not executed locally per release policy: - Org policy/audit unit/component/integration/regression checks will run in Linux unified validation stage.## 34. Execution Log (2026-04-30, P2-06)- Card: `P2-06 Linux Unified Verification Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_linux_unified_gate.py` (aggregated Linux-stage command manifest for Phase 0 -> Phase 2 runtime chain) - `README.md` (added unified gate command entry) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (added P2-06 card + execution log)- Test scripts landed: - `tests/test_linux_unified_gate_runtime.py` (`T1` manifest file existence contract, `T2` command generation order/format contract)- Not executed locally per release policy: - Unified gate contract checks and all listed runtime tests will run in Linux unified validation stage.## 35. Execution Log (2026-04-30, P2-07)- Card: `P2-07 Agent Runtime Parity (sync/background)`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/tools/base.py` (ToolContext adds conversation/provider/task_manager/tool_registry fields for runtime propagation) - `claude_code/engine/query.py` (ToolContext now carries conversation/provider/task_manager/registry into tool execution path) - `claude_code/tasks/manager.py` (`create_agent_task` supports metadata injection for audit/replay) - `claude_code/tools/agent/__init__.py` (sync/background sub-agent parity: runtime provider/model resolution, registry propagation, task-manager reuse, background metadata contract) - `scripts/run_p2_agent_runtime_parity_gate.py` - `scripts/run_linux_unified_gate.py` (adds P2-07 regression test to unified manifest) - `README.md` (adds P2-07 gate command entry) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-07 card and execution log)- Test scripts landed: - `tests/test_agent_background_context_runtime.py` (`T2` sync/background context parity, `T3` background task metadata audit fields, `T4` provider/model inheritance regression)- Not executed locally per release policy: - Agent runtime parity unit/component/integration checks will run in Linux unified validation stage.## 36. Execution Log (2026-04-30, P2-08)- Card: `P2-08 Custom Agents Directory Loader`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/agents/loader.py` (custom agent markdown loader with YAML frontmatter parsing and cwd->git-root directory merge semantics) - `claude_code/tools/agent/__init__.py` (lazy custom agent load, custom>builtin resolution, and merged unknown-type availability hints) - `scripts/run_p2_custom_agents_loader_gate.py` - `scripts/run_linux_unified_gate.py` (adds P2-08 regression test to unified manifest) - `README.md` (adds P2-08 gate command entry) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-08 card and execution log)- Test scripts landed: - `tests/test_custom_agents_loader_runtime.py` (`T1` frontmatter parsing, `T2` nested override merge, `T3` custom override builtin, `T4` invalid schema tolerance and unknown-type availability message)- Not executed locally per release policy: - Custom-agent loader unit/component/integration checks will run in Linux unified validation stage.## 37. Execution Log (2026-04-30, P2-09)- Card: `P2-09 Linux Unified Execution Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_unified_execution_gate.py` (Linux-stage executor over unified manifest; supports dry-run/print/continue-on-failure/report-dir and writes junit + summary json) - `scripts/run_linux_unified_gate.py` (adds P2-09 execution gate contract test to unified manifest) - `README.md` (adds P2-09 execution gate command entries) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-09 card and execution log)- Test scripts landed: - `tests/test_p2_linux_unified_execution_gate_runtime.py` (`T1` unified manifest order contract, `T2` junit path and extra-args forwarding contract)- Not executed locally per release policy: - Linux unified execution gate contract checks and full-suite execution will run in Linux validation stage.## 38. Execution Log (2026-04-30, P2-10)- Card: `P2-10 JetBrains IDE Integration`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `claude_code/server/ide_adapter.py` (added `JetBrainsClientAdapter` + `JetBrainsWorkspaceSnapshot` + findings->inspections mapping with severity highlight normalization) - `claude_code/server/__init__.py` (exports JetBrains adapter symbols) - `scripts/run_p2_jetbrains_ide_integration_gate.py` - `scripts/run_linux_unified_gate.py` (adds P2-10 adapter contract test to unified manifest) - `README.md` (adds P2-10 gate command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-10 card and execution log)- Test scripts landed: - `tests/test_p2_jetbrains_ide_integration_runtime.py` (`T1` adapter snapshot normalization, `T2` findings->inspection highlight mapping, `T4` line type coercion tolerance)- Not executed locally per release policy: - JetBrains IDE adapter unit/component checks will run in Linux unified validation stage.## 39. Execution Log (2026-04-30, P2-11)- Card: `P2-11 Linux Sharded Execution Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_unified_execution_gate.py` (adds deterministic shard slicing via `--shard-total` + `--shard-index`, shard-aware dry-run header, shard metadata in summary payload) - `README.md` (adds Linux shard-parallel invocation example) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-11 card and execution log)- Test scripts landed: - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds shard slicing contract, invalid shard argument guards, summary shard metadata contract)- Not executed locally per release policy: - P2-11 shard execution gate contract checks and full-suite execution will run in Linux validation stage.## 40. Execution Log (2026-04-30, P2-12)- Card: `P2-12 Linux Shard Aggregation Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_shard_aggregation_gate.py` (discovers shard summaries via direct path/glob/artifacts dir, validates summary schema, merges shard totals, emits merged summary and global pass/fail) - `scripts/run_linux_unified_gate.py` (adds P2-12 aggregation contract test to unified manifest) - `README.md` (adds Linux shard aggregation gate invocation examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-12 card and execution log)- Test scripts landed: - `tests/test_p2_linux_shard_aggregation_gate_runtime.py` (`T1` summary discovery dedupe, `T2` summary field validation, `T3` successful shard aggregation, `T4` missing-shard and total drift failure contract) - `tests/test_p2_linux_unified_execution_gate_runtime.py` (adds empty-shard summary payload contract used by aggregation stage)- Not executed locally per release policy: - P2-12 aggregation gate contract checks and Linux multi-shard recovery validation will run in Linux unified validation stage.## 41. Execution Log (2026-04-30, P2-13)- Card: `P2-13 Linux Final Report Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_report_publish_gate.py` (loads merged shard summary, validates publish contract, emits final_report.json + final_report.md, enforces computed-vs-reported status consistency) - `scripts/run_linux_unified_gate.py` (adds P2-13 publish gate contract test to unified manifest) - `README.md` (adds P2-13 final report publish gate command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-13 card and execution log)- Test scripts landed: - `tests/test_p2_linux_report_publish_gate_runtime.py` (`T1` merged summary required fields/types, `T2` happy-path payload contract, `T3` status mismatch failure contract, `T4` markdown report section contract)- Not executed locally per release policy: - P2-13 publish gate contract checks and Linux final-report recovery validation will run in Linux unified validation stage.## 42. Execution Log (2026-04-30, P2-14)- Card: `P2-14 Linux Unified Pipeline Orchestration Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_unified_pipeline_gate.py` (orchestrates execution->aggregation->publish chain, supports stage skip controls, dry-run/print-commands, fail-fast, and pytest args forwarding to execution stage) - `scripts/run_linux_unified_gate.py` (adds P2-14 pipeline contract test to unified manifest) - `README.md` (adds P2-14 one-command Linux pipeline examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-14 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_unified_pipeline_gate_runtime.py` (`T1` default 3-stage command plan with execution summary feed, `T2` skip-execution external summary passthrough, `T3` all-stage-skip empty plan contract)- Not executed locally per release policy: - P2-14 pipeline gate contract checks and Linux orchestrated recovery validation will run in Linux unified validation stage.## 43. Execution Log (2026-04-30, P2-15)- Card: `P2-15 Linux Shard Plan Generation Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_shard_plan_gate.py` (builds deterministic shard execution plan from unified manifest; emits per-shard report/summary/command payload; supports dry-run/print-commands/pytest args forwarding) - `scripts/run_linux_unified_gate.py` (adds P2-15 shard plan contract test to unified manifest) - `README.md` (adds P2-15 Linux shard plan command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-15 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_shard_plan_gate_runtime.py` (`T1` deterministic shard slicing contract, `T2` command+args forwarding contract, `T3` invalid shard_total rejection contract)- Not executed locally per release policy: - P2-15 shard plan gate contract checks and Linux fan-out orchestration validation will run in Linux unified validation stage.## 44. Execution Log (2026-04-30, P2-16)- Card: `P2-16 Linux CI Matrix Export Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_matrix_gate.py` (loads P2-15 shard plan, validates shard/total contract, emits CI-consumable matrix payload and summary-path collection, supports optional GitHub output export) - `scripts/run_linux_unified_gate.py` (adds P2-16 matrix gate contract test to unified manifest) - `README.md` (adds P2-16 CI matrix gate command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-16 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_matrix_gate_runtime.py` (`T1` matrix include order/field contract, `T2` empty-shard filter contract, `T3` GitHub output value contract, `T4` totals mismatch rejection contract)- Not executed locally per release policy: - P2-16 CI matrix export contract checks and Linux fan-out/fan-in orchestration validation will run in Linux unified validation stage.## 45. Execution Log (2026-04-30, P2-17)- Card: `P2-17 Linux CI Workflow Plan Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_plan_gate.py` (loads P2-16 ci_matrix, validates matrix/summary_paths contracts, emits CI-consumable workflow plan with fan-out matrix and fan-in aggregation/publish commands, supports optional GitHub output export) - `scripts/run_linux_unified_gate.py` (adds P2-17 workflow plan gate contract test to unified manifest) - `README.md` (adds P2-17 workflow plan gate command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-17 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_plan_gate_runtime.py` (`T1` fan-out/fan-in payload contract, `T2` GitHub output contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection)- Not executed locally per release policy: - P2-17 CI workflow plan contract checks and Linux fan-out/fan-in/final-publish orchestration validation will run in Linux unified validation stage.## 46. Execution Log (2026-04-30, P2-18)- Card: `P2-18 Linux CI Workflow YAML Render Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_yaml_gate.py` (loads P2-17 ci_workflow_plan, validates fan-out/fan-in contract, renders GitHub Actions workflow YAML, emits render metadata for audit traceability) - `scripts/run_linux_unified_gate.py` (adds P2-18 workflow YAML render contract test to unified manifest) - `README.md` (adds P2-18 workflow YAML gate command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-18 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py` (`T1` workflow YAML fan-out/fan-in render contract, `T2` render metadata contract, `T3` summary_paths order mismatch rejection, `T4` selected_shards mismatch rejection)- Not executed locally per release policy: - P2-18 workflow YAML render contract checks and Linux CI-end orchestration validation will run in Linux unified validation stage.## 47. Execution Log (2026-04-30, P2-19)- Card: `P2-19 Linux CI Workflow Drift Sync Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_sync_gate.py` (rebuilds expected workflow/metadata from P2-17 plan, validates drift against rendered artifacts, supports diff output and optional in-place sync write-back) - `scripts/run_linux_unified_gate.py` (adds P2-19 workflow sync contract test to unified manifest) - `README.md` (adds P2-19 drift check/sync command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-19 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_sync_gate_runtime.py` (`T1` missing artifact drift contract, `T2` workflow content drift+diff contract, `T3` generated_at relaxed-mode contract, `T4` strict generated_at mismatch rejection)- Not executed locally per release policy: - P2-19 workflow sync contract checks and Linux CI drift-governance validation will run in Linux unified validation stage.## 48. Execution Log (2026-04-30, P2-20)- Card: `P2-20 Linux CI Workflow Command Guard Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_command_guard_gate.py` (validates command string/parts integrity, script target + required flag contract, fan-out/fan-in path binding, optional canonical write-back) - `scripts/run_linux_unified_gate.py` (adds P2-20 command guard contract test to unified manifest) - `README.md` (adds P2-20 command guard gate invocation examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-20 card, dependency chain extension, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py` (`T1` valid payload pass contract, `T2` command string/parts mismatch rejection + normalization contract, `T3` aggregation summary order mismatch rejection, `T4` publish path mismatch rejection)- Not executed locally per release policy: - P2-20 command-integrity contract checks and Linux CI command-governance validation will run in Linux unified validation stage.## 49. Execution Log (2026-04-30, P2-21)- Card: `P2-21 Linux CI Workflow Governance Convergence Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_governance_gate.py` (aggregates P2-19 drift sync + P2-20 command guard + metadata lineage checks into one governance verdict and categorized failed_checks) - `scripts/run_linux_unified_gate.py` (adds P2-21 governance gate contract test to unified manifest) - `README.md` (adds P2-21 governance gate invocation examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-21 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_governance_gate_runtime.py` (`T1` full aligned pass contract, `T2` workflow drift failure category, `T3` command guard failure category, `T4` metadata lineage mismatch category)- Not executed locally per release policy: - P2-21 governance convergence contract checks and Linux CI final-governance validation will run in Linux unified validation stage.## 50. Execution Log (2026-04-30, P2-22)- Card: `P2-22 Linux CI Workflow Governance Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_governance_publish_gate.py` (publishes P2-21 governance report into CI decision payload, markdown report, and optional GitHub outputs) - `scripts/run_linux_unified_gate.py` (adds P2-22 governance publish contract test to unified manifest) - `README.md` (adds P2-22 governance publish gate invocation examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-22 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py` (`T1` aligned governance pass contract, `T2` failed-checks deny-execution contract, `T3` failed_checks mismatch structural-failure contract, `T4` github-output field contract)- Not executed locally per release policy: - P2-22 governance publish contract checks and Linux CI publish-stage validation will run in Linux unified validation stage.## 51. Execution Log (2026-04-30, P2-23)- Card: `P2-23 Linux CI Workflow Execution Decision Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_decision_gate.py` (converts P2-22 publish payload into execute/blocked decision contract with on-block exit policy, artifact-presence guard, markdown/json report, and optional GitHub outputs) - `scripts/run_linux_unified_gate.py` (adds P2-23 execution decision gate contract test to unified manifest) - `README.md` (adds P2-23 decision-gate usage examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-23 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_decision_gate_runtime.py` (`T1` aligned pass -> execute contract, `T2` failed-governance -> blocked contract, `T3` blocked+skip policy exit-code contract, `T4` github-output field contract)- Not executed locally per release policy: - P2-23 execution-decision contract checks and Linux CI handoff validation will run in Linux unified validation stage.## 52. Execution Log (2026-04-30, P2-24)- Card: `P2-24 Linux CI Workflow Dispatch Readiness Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_dispatch_gate.py` (converts P2-23 execution decision payload into dispatch-ready contract, canonical gh workflow dispatch command, markdown/json reports, and optional GitHub outputs) - `scripts/run_linux_unified_gate.py` (adds P2-24 dispatch gate contract test to unified manifest) - `README.md` (adds P2-24 dispatch gate usage examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-24 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py` (`T1` execute -> ready dispatch contract, `T2` blocked -> no-dispatch contract, `T3` decision contract mismatch rejection, `T4` github-output field contract)- Not executed locally per release policy: - P2-24 dispatch-readiness contract checks and Linux CI dispatch handoff validation will run in Linux unified validation stage.## 53. Execution Log (2026-04-30, P2-25)- Card: `P2-25 Linux CI Workflow Dispatch Execution Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py` (validates P2-24 dispatch contract, executes ready dispatch command with timeout handling, and emits execution JSON/markdown/github-output contract) - `scripts/run_linux_unified_gate.py` (adds P2-25 dispatch execution gate contract test to unified manifest) - `README.md` (adds P2-25 dispatch execution gate usage examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-25 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py` (`T1` ready dry-run contract, `T2` blocked pass-through contract, `T3` command contract mismatch rejection, `T4` dispatch failure reason code contract, `T5` github-output field contract)- Not executed locally per release policy: - P2-25 dispatch execution contract checks and Linux CI dispatch execution validation will run in Linux unified validation stage.## 54. Execution Log (2026-04-30, P2-26)- Card: `P2-26 Linux CI Workflow Full Pipeline Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (initially composed P2-17 -> P2-31 into single pipeline entry with stage skips, strategy passthrough, dry-run/print, fail-fast policy, terminal publish, release handoff, and release trigger stage) - `scripts/run_linux_unified_gate.py` (adds P2-26 pipeline gate contract test to unified manifest) - `README.md` (updates P2-26 one-command workflow pipeline usage examples to include terminal publish coverage) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-26 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (`T1` default stage-chain contract through release trigger, `T2` strategy passthrough contract, `T3` partial skip-stage chain contract, `T4` all-skip empty-plan contract, `T5` terminal-publish-only stage contract)- Not executed locally per release policy: - P2-26 pipeline orchestration contract checks (including release trigger stage) and Linux CI end-to-end workflow gate validation will run in Linux unified validation stage.## 55. Execution Log (2026-04-30, P2-27)- Card: `P2-27 Linux CI Workflow Dispatch Traceability Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py` (builds dispatch trace contract from P2-25 report, extracts run reference, emits poll command, supports optional --poll-now run status query and GitHub outputs) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain with P2-27 dispatch trace stage and new trace control flags) - `scripts/run_linux_unified_gate.py` (adds P2-27 dispatch trace contract test to unified manifest) - `README.md` (adds P2-27 gate usage and updated pipeline examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-27 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py` (`T1` dispatched run reference extraction + poll command contract, `T2` missing run reference failure contract, `T3` blocked pass-through no-poll contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-27 stage presence and trace flag passthrough contract coverage)- Not executed locally per release policy: - P2-27 dispatch trace contract checks and Linux CI run-tracking validation will run in Linux unified validation stage.## 56. Execution Log (2026-04-30, P2-28)- Card: `P2-28 Linux CI Workflow Dispatch Completion Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py` (converges P2-27 trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain with P2-28 completion stage and new completion control flags/skip switch) - `scripts/run_linux_unified_gate.py` (adds P2-28 completion contract test to unified manifest) - `README.md` (adds P2-28 gate usage and updates P2-26 full-chain examples to P2-28) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-28 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` trace poll command mismatch rejection, `T4` github-output contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-28 stage presence and completion flag passthrough contract coverage)- Not executed locally per release policy: - P2-28 dispatch completion contract checks and Linux CI terminal convergence validation will run in Linux unified validation stage.## 57. Execution Log (2026-04-30, P2-29)- Card: `P2-29 Linux CI Workflow Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py` (converts P2-28 completion artifact into terminal publish verdict with promote/hold decision, contract mismatch guard, markdown/json/github-output contracts) - `scripts/run_linux_unified_gate.py` (adds P2-29 terminal publish contract test to unified manifest) - `README.md` (adds P2-29 terminal publish gate command examples) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-29 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py` (`T1` success->publish/pass+promote contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract)- Not executed locally per release policy: - P2-29 terminal publish contract checks and Linux CI terminal-release handoff validation will run in Linux unified validation stage.## 58. Execution Log (2026-04-30, P2-30)- Card: `P2-30 Linux CI Workflow Release Handoff Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_handoff_gate.py` (converts P2-29 terminal publish artifact into release handoff contract with promote/hold/reject action, target environment + channel semantics, contract mismatch guards, markdown/json/github-output outputs) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends pipeline stage chain to compose P2-17 -> P2-30 and adds release handoff output flags + skip switch + environment/channel passthrough) - `scripts/run_linux_unified_gate.py` (adds P2-30 release handoff contract test to unified manifest) - `README.md` (adds P2-30 release handoff gate invocation examples and updates P2-26 full-chain examples to P2-30) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-30 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py` (`T1` passed->ready_for_release contract, `T2` in_progress->awaiting_completion contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-30 stage presence, environment/channel passthrough, and release-handoff-only stage contract coverage)- Not executed locally per release policy: - P2-30 release handoff contract checks and Linux CI release-gate handoff validation will run in Linux unified validation stage.## 59. Execution Log (2026-04-30, P2-31)- Card: `P2-31 Linux CI Workflow Release Trigger Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_trigger_gate.py` (loads P2-30 handoff contract, validates trigger readiness, executes/dry-runs release command, emits trigger JSON/markdown/github-output report) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose P2-17 -> P2-31 and adds release-trigger flags/outputs/skip switch) - `scripts/run_linux_unified_gate.py` (adds P2-31 release trigger contract test to unified manifest) - `README.md` (adds P2-31 gate usage and updates P2-26 full-chain examples to P2-31) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-31 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py` (`T1` ready_for_release dry-run contract, `T2` awaiting_completion pass-through contract, `T3` handoff contract mismatch rejection, `T4` trigger command failure convergence, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-31 stage presence, release-trigger flag passthrough, and release-trigger-only stage contract coverage)- Not executed locally per release policy: - P2-31 release trigger contract checks and Linux CI release dispatch execution validation will run in Linux unified validation stage.## 60. Execution Log (2026-04-30, P2-32)- Card: `P2-32 Linux CI Workflow Release Traceability Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_trace_gate.py` (loads P2-31 release trigger artifact, validates trigger contract, extracts release run reference, emits optional poll command, supports optional --poll-now status query, and outputs JSON/markdown/github-output report) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-32`, adds release-trace outputs/flags/skip switch) - `scripts/run_linux_unified_gate.py` (adds P2-32 release trace contract test to unified manifest) - `README.md` (adds P2-32 gate usage and updates P2-26 full-chain examples to P2-32) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-32 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py` (`T1` triggered->run reference extraction contract, `T2` missing run reference failure contract, `T3` trigger_failed passthrough contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-32 stage presence, release-trace flag passthrough, and release-trace-only stage contract coverage)- Not executed locally per release policy: - P2-32 release trace contract checks and Linux CI release run-tracking validation will run in Linux unified validation stage.## 61. Execution Log (2026-04-30, P2-33)- Card: `P2-33 Linux CI Workflow Release Completion Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_completion_gate.py` (converges P2-32 release trace into completion verdict; supports poll loop with interval/max/timeout, optional allow-in-progress policy, and GitHub outputs) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-33`, adds release-completion outputs/flags/skip switch) - `scripts/run_linux_unified_gate.py` (adds P2-33 release completion contract test to unified manifest) - `README.md` (adds P2-33 gate usage and updates P2-26 full-chain examples to P2-33) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-33 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py` (`T1` success convergence contract, `T2` in-progress timeout contract, `T3` release poll command mismatch rejection, `T4` github-output contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-33 stage presence, release-completion flag passthrough, and release-completion-only stage contract coverage)- Not executed locally per release policy: - P2-33 release completion contract checks and Linux CI release terminal convergence validation will run in Linux unified validation stage.## 62. Execution Log (2026-04-30, P2-34)- Card: `P2-34 Linux CI Workflow Release Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py` (converts P2-33 release completion artifact into release terminal publish verdict with finalize/hold decision, contract mismatch guard, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-34`, adds release-terminal-publish outputs and `--skip-release-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-34 release terminal publish contract test to unified manifest) - `README.md` (adds P2-34 gate usage and updates P2-26 full-chain examples to P2-34) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-34 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py` (`T1` success->passed+finalize contract, `T2` timeout+allow-in-progress->in_progress contract, `T3` success/exit mismatch->contract_failed contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-34 stage presence, skip flag coverage, and release-terminal-publish-only stage contract)- Verification performed without running tests: - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - P2-34 runtime contract checks and Linux CI release terminal publish validation will run in Linux unified validation stage.## 63. Execution Log (2026-04-30, P2-35)- Card: `P2-35 Linux CI Workflow Release Finalization Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_finalization_gate.py` (converts P2-34 release terminal publish artifact into final release closure contract with `finalize/hold/abort` decision, hold policy support, contract mismatch guard, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-35`, adds release-finalization outputs and `--skip-release-finalization` switch, plus `--on-release-hold` policy passthrough) - `scripts/run_linux_unified_gate.py` (adds P2-35 release finalization contract test to unified manifest) - `README.md` (adds P2-35 gate usage and updates P2-26 full-chain examples to P2-35) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-35 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py` (`T1` passed->finalized contract, `T2` hold-policy-fail contract, `T3` publish contract mismatch->contract_failed contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-35 stage presence, hold-policy passthrough, and release-finalization-only stage contract)- Not executed locally per release policy: - P2-35 runtime contract checks and Linux CI release finalization validation will run in Linux unified validation stage.## 64. Execution Log (2026-04-30, P2-36)- Card: `P2-36 Linux CI Workflow Release Closure Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_closure_gate.py` (converts P2-35 release finalization artifact into terminal release closure contract with `closed/pending/failed/contract_failed` status, `ship/hold/rollback` decision, consistency guard, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-36`, adds release-closure outputs and `--skip-release-closure` switch) - `scripts/run_linux_unified_gate.py` (adds P2-36 release closure contract test to unified manifest) - `README.md` (adds P2-36 gate usage and updates P2-26 full-chain examples to P2-36) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-36 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py` (`T1` finalized->closed contract, `T2` awaiting_release->pending contract, `T3` finalization mismatch->contract_failed contract, `T4` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-36 stage presence, skip flag coverage, and release-closure-only stage contract)- Not executed locally per release policy: - P2-36 runtime contract checks and Linux CI release closure publish validation will run in Linux unified validation stage.## 65. Execution Log (2026-04-30, P2-37)- Card: `P2-37 Linux CI Workflow Release Archive Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_archive_gate.py` (converts P2-36 release closure artifact into terminal release archive contract with `ready/pending/failed/contract_failed` status, `publish/hold/block` decision, evidence-manifest existence checks, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-37`, adds release-archive outputs and `--skip-release-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-37 release archive contract test to unified manifest) - `README.md` (adds P2-37 gate usage and updates P2-26 full-chain examples to P2-37) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-37 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py` (`T1` closed+evidence->ready contract, `T2` pending->hold contract, `T3` closure mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-37 stage presence and release-archive-only stage contract coverage)- Not executed locally per release policy: - P2-37 runtime contract checks and Linux CI release evidence archive validation will run in Linux unified validation stage.## 66. Execution Log (2026-04-30, P2-38)- Card: `P2-38 Linux CI Workflow Release Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_verdict_gate.py` (converts P2-37 release archive artifact into terminal release verdict contract with `published/awaiting_archive/blocked/contract_failed` status, `ship/hold/block` decision, evidence consistency guard, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-38`, adds release-verdict outputs and `--skip-release-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-38 release verdict contract test to unified manifest) - `README.md` (adds P2-38 gate usage and updates P2-26 full-chain examples to P2-38) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-38 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py` (`T1` ready->published contract, `T2` pending->awaiting_archive contract, `T3` archive mismatch->contract_failed contract, `T4` missing evidence->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-38 stage presence, skip flag coverage, and release-verdict-only stage contract)- Not executed locally per release policy: - P2-38 runtime contract checks and Linux CI final release verdict convergence validation will run in Linux unified validation stage.## 67. Execution Log (2026-04-30, Gate Contract Coverage Closure)- Card: `Phase 1 Gate Runtime Contract Closure (P1-01 ~ P1-06)`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_linux_unified_gate.py` (extends Linux unified manifest with P1 gate runtime contract tests) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds closure execution log)- Test scripts landed: - `tests/test_p1_control_plane_gate_runtime.py` (P1-01 manifest file existence + pytest command contract) - `tests/test_p1_event_journal_gate_runtime.py` (P1-02 manifest file existence + pytest command contract) - `tests/test_p1_sqlite_state_backend_gate_runtime.py` (P1-03 gate command chain + file existence contract) - `tests/test_p1_active_memory_gate_runtime.py` (P1-04 gate command chain + file existence contract) - `tests/test_p1_hook_permission_audit_gate_runtime.py` (P1-05 gate command chain + file existence contract) - `tests/test_p1_cli_thin_client_gate_runtime.py` (P1-06 gate command chain + file existence contract)- Verification performed without running tests: - `py -3 scripts/check_syntax.py --root tests --json` passed. - `py -3 scripts/check_syntax.py --root scripts --json` passed.- Not executed locally per release policy: - All new P1 gate runtime contract tests will be executed in Linux unified validation stage.## 68. Execution Log (2026-05-01, P2-39)- Card: `P2-39 Linux CI Workflow Release Incident Dispatch Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_incident_gate.py` (converts P2-38 release verdict artifact into terminal incident dispatch contract with `not_required/ready_dry_run/dispatched/dispatch_failed/contract_failed` status, verdict consistency guard, incident command execution fallback, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-39`, adds release-incident outputs, incident command flags, and `--skip-release-incident` switch) - `scripts/run_linux_unified_gate.py` (adds P2-39 release incident contract test to unified manifest) - `README.md` (adds P2-39 gate usage and updates P2-26 full-chain examples to P2-39) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-39 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py` (`T1` published->not_required contract, `T2` blocked->ready_dry_run contract, `T3` verdict mismatch->contract_failed contract, `T4` incident command failure convergence, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-39 stage presence, incident flag passthrough, and release-incident-only stage contract coverage)- Not executed locally per release policy: - P2-39 runtime contract checks and Linux CI release incident dispatch validation will run in Linux unified validation stage.## 69. Execution Log (2026-05-01, Gate Contract Coverage Closure II)- Card: `Gate Runtime Contract Closure II (Phase 0 + P2 gate manifest coverage)`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `tests/test_phase0_gate_runtime.py` (adds contract coverage for `scripts/run_phase0_gate.py` manifest + pytest command builder) - `scripts/run_linux_unified_gate.py` (extends unified manifest to include: - `tests/test_phase0_gate_runtime.py` - `tests/test_linux_unified_gate_runtime.py` - `tests/test_p2_multi_agent_supervisor_gate_runtime.py` - `tests/test_p2_artifact_bus_gate_runtime.py` - `tests/test_p2_ide_integration_gate_runtime.py` - `tests/test_p2_github_ci_workflow_gate_runtime.py` - `tests/test_p2_org_policy_audit_gate_runtime.py` - `tests/test_p2_agent_runtime_parity_gate_runtime.py` - `tests/test_p2_custom_agents_loader_gate_runtime.py` - `tests/test_p2_jetbrains_ide_integration_gate_runtime.py`)- Verification performed without running tests: - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed. - Gate contract scan passed: `NO_MISSING_GATE_TESTS`. - Unified manifest scan passed: `NO_MISSING_UNIFIED_MANIFEST_TESTS`.- Not executed locally per release policy: - All newly added gate runtime contract tests remain pending for Linux unified validation stage.## 70. Execution Log (2026-05-01, P2-40)- Card: `P2-40 Linux CI Workflow Release Terminal Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py` (converts P2-39 release incident artifact into terminal release verdict contract with `released/awaiting_archive/blocked_incident_ready_dry_run/blocked_incident_dispatched/blocked_incident_failed/blocked/contract_failed` status and `ship/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-40`, adds release-terminal-verdict outputs, stage wiring, and `--skip-release-terminal-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-40 release terminal verdict contract test to unified manifest) - `README.md` (adds P2-40 gate usage and updates P2-26 full-chain examples to P2-40) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-40 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py` (`T1` published->released contract, `T2` blocked+ready_dry_run->blocked_incident_ready_dry_run contract, `T3` verdict/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-40 stage presence, command path assertions, skip flag propagation, and release-terminal-verdict-only stage contract coverage)- Not executed locally per release policy: - P2-40 runtime contract checks and Linux CI final terminal verdict convergence validation will run in Linux unified validation stage.## 71. Execution Log (2026-05-01, P2-41)- Card: `P2-41 Linux CI Workflow Release Delivery Closure Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_delivery_gate.py` (converts P2-40 release terminal verdict artifact into final delivery contract with `shipped/pending_follow_up/blocked_incident/blocked_incident_failed/blocked/contract_failed` status and `deliver/hold/escalate/block` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-41`, adds release-delivery outputs, stage wiring, and `--skip-release-delivery` switch) - `scripts/run_linux_unified_gate.py` (adds P2-41 release delivery contract test to unified manifest) - `README.md` (adds P2-41 gate usage and updates P2-26 full-chain examples to P2-41) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-41 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py` (`T1` released->shipped contract, `T2` blocked_incident_ready_dry_run->blocked_incident contract, `T3` terminal/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-41 stage presence, command path assertions, skip flag propagation, and release-delivery-only stage contract coverage)- Not executed locally per release policy: - P2-41 runtime contract checks and Linux CI final delivery closure convergence validation will run in Linux unified validation stage.## 72. Execution Log (2026-05-01, P2-42)- Card: `P2-42 Linux CI Workflow Release Delivery Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py` (converts P2-41 release delivery artifact into terminal publish contract with `published/pending_follow_up/blocked/contract_failed` status and `announce_release/announce_hold/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-42`, adds release-delivery-terminal-publish outputs, stage wiring, and `--skip-release-delivery-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-42 release delivery terminal publish contract test to unified manifest) - `README.md` (adds P2-42 gate usage and updates P2-26 full-chain examples to P2-42) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-42 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py` (`T1` shipped->published contract, `T2` blocked_incident->blocked contract, `T3` delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-42 stage presence, command path assertions, skip flag propagation, and release-delivery-terminal-publish-only stage contract coverage)- Not executed locally per release policy: - P2-42 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage.## 73. Execution Log (2026-05-01, P2-43)- Card: `P2-43 Linux CI Workflow Release Delivery Final Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py` (converts P2-42 release delivery terminal publish artifact into final verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_release/open_follow_up/escalate_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-43`, adds release-delivery-final-verdict outputs, stage wiring, and `--skip-release-delivery-final-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-43 release delivery final verdict contract test to unified manifest) - `README.md` (adds P2-43 gate usage and updates P2-26 full-chain examples to P2-43) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-43 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py` (`T1` published->completed contract, `T2` pending_follow_up->requires_follow_up contract, `T3` terminal/delivery/incident mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-43 stage presence, command path assertions, skip flag propagation, and release-delivery-final-verdict-only stage contract coverage)- Not executed locally per release policy: - P2-43 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage.## 74. Execution Log (2026-05-01, P2-44)- Card: `P2-44 Linux CI Workflow Release Follow-Up Dispatch Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py` (converts P2-43 release delivery final verdict artifact into follow-up dispatch contract with `closed/follow_up_required/escalated/contract_failed` status and `no_action/dispatch_follow_up/dispatch_escalation/abort_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-44`, adds release-follow-up-dispatch outputs, stage wiring, and `--skip-release-follow-up-dispatch` switch) - `scripts/run_linux_unified_gate.py` (adds P2-44 release follow-up dispatch contract test to unified manifest) - `README.md` (adds P2-44 gate usage and updates P2-26 full-chain examples to P2-44) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-44 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py` (`T1` completed->closed contract, `T2` requires_follow_up->follow_up_required contract, `T3` final-verdict mismatch->contract_failed contract, `T4` evidence missing->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-44 stage presence, command path assertions, skip flag propagation, and release-follow-up-dispatch-only stage contract coverage)- Not executed locally per release policy: - P2-44 runtime contract checks and Linux CI follow-up dispatch convergence validation will run in Linux unified validation stage.## 75. Execution Log (2026-05-01, P2-45)- Card: `P2-45 Linux CI Workflow Release Follow-Up Closure Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py` (converts P2-44 release follow-up dispatch artifact into follow-up closure contract with `closed/queued_dry_run/queued/queue_failed/contract_failed` status and `no_action/queue_follow_up/queue_escalation/abort_queue` decision, plus consistency guards, optional follow-up queue command execution, markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-45`, adds release-follow-up-closure outputs, stage wiring, follow-up command flags, and `--skip-release-follow-up-closure` switch) - `scripts/run_linux_unified_gate.py` (adds P2-45 release follow-up closure contract test to unified manifest) - `README.md` (adds P2-45 gate usage and updates P2-26 full-chain examples to P2-45) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-45 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py` (`T1` closed->no_action contract, `T2` follow_up_required->queued_dry_run contract, `T3` dispatch mismatch->contract_failed contract, `T4` follow-up queue command failure->queue_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-45 stage presence, command path assertions, follow-up flag propagation, and release-follow-up-closure-only stage contract coverage)- Not executed locally per release policy: - P2-45 runtime contract checks and Linux CI follow-up closure convergence validation will run in Linux unified validation stage.## 76. Execution Log (2026-05-01, P2-46)- Card: `P2-46 Linux CI Workflow Release Follow-Up Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py` (converts P2-45 release follow-up closure artifact into terminal publish contract with `published/pending_queue/queue_failed/contract_failed` status and `announce_closed/announce_queued/announce_pending_queue/announce_queue_failure/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-46`, adds release-follow-up-terminal-publish outputs, stage wiring, and `--skip-release-follow-up-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-46 release follow-up terminal publish contract test to unified manifest) - `README.md` (adds P2-46 gate usage and updates P2-26 full-chain examples to P2-46) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-46 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py` (`T1` closed->published contract, `T2` queued_dry_run->pending_queue contract, `T3` closure mismatch->contract_failed contract, `T4` queue_failed->queue_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-46 stage presence, command path assertions, skip flag propagation, and release-follow-up-terminal-publish-only stage contract coverage)- Not executed locally per release policy: - P2-46 runtime contract checks and Linux CI follow-up terminal publish convergence validation will run in Linux unified validation stage.## 77. Execution Log (2026-05-01, P2-47)- Card: `P2-47 Linux CI Workflow Release Follow-Up Final Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py` (converts P2-46 release follow-up terminal publish artifact into final follow-up verdict contract with `completed/requires_follow_up/blocked/contract_failed` status and `close_follow_up/keep_follow_up_open/escalate_queue_failure/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-47`, adds release-follow-up-final-verdict outputs, stage wiring, and `--skip-release-follow-up-final-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-47 release follow-up final verdict contract test to unified manifest) - `README.md` (adds P2-47 gate usage and updates P2-26 full-chain examples to P2-47) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-47 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py` (`T1` published+announce_closed->completed contract, `T2` pending_queue->requires_follow_up contract, `T3` queue_failed->blocked contract, `T4` contract mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-47 stage presence, command path assertions, skip flag propagation, and release-follow-up-final-verdict-only stage contract coverage)- Not executed locally per release policy: - P2-47 runtime contract checks and Linux CI follow-up final verdict convergence validation will run in Linux unified validation stage.## 78. Execution Log (2026-05-01, P2-48)- Card: `P2-48 Linux CI Workflow Release Final Outcome Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py` (converges P2-43 + P2-47 final verdict artifacts into one terminal release outcome contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_and_close/ship_with_follow_up_open/escalate_blocker/abort_outcome` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-48`, adds release-final-outcome outputs, stage wiring, and `--skip-release-final-outcome` switch) - `scripts/run_linux_unified_gate.py` (adds P2-48 release final outcome contract test to unified manifest) - `README.md` (adds P2-48 gate usage and updates P2-26 full-chain examples to P2-48) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-48 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py` (`T1` released contract, `T2` released_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-48 stage presence, command path assertions, skip flag propagation, and release-final-outcome-only stage contract coverage)- Not executed locally per release policy: - P2-48 runtime contract checks and Linux CI final release outcome convergence validation will run in Linux unified validation stage.## 79. Execution Log (2026-05-01, P2-49)- Card: `P2-49 Linux CI Workflow Release Final Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py` (converges P2-48 final outcome artifact into terminal publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closure/announce_release_with_follow_up/announce_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-49`, adds release-final-terminal-publish outputs, stage wiring, and `--skip-release-final-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-49 release final terminal publish contract test to unified manifest) - `README.md` (adds P2-49 gate usage and updates P2-26 full-chain examples to P2-49) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-49 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-49 stage presence, command path assertions, skip flag propagation, and release-final-terminal-publish-only stage contract coverage)- Not executed locally per release policy: - P2-49 runtime contract checks and Linux CI final terminal publish convergence validation will run in Linux unified validation stage.## 80. Execution Log (2026-05-01, P2-50)- Card: `P2-50 Linux CI Workflow Release Final Handoff Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py` (converges P2-49 final terminal publish artifact into release handoff contract with `completed/completed_with_follow_up/blocked/contract_failed` status and `handoff_release_closure/handoff_release_with_follow_up/handoff_blocker/abort_handoff` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-50`, adds release-final-handoff outputs, stage wiring, and `--skip-release-final-handoff` switch) - `scripts/run_linux_unified_gate.py` (adds P2-50 release final handoff contract test to unified manifest) - `README.md` (adds P2-50 gate usage and updates P2-26 full-chain examples to P2-50) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-50 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py` (`T1` published->completed contract, `T2` published_with_follow_up->completed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-50 stage presence, command path assertions, skip flag propagation, and release-final-handoff-only stage contract coverage)- Not executed locally per release policy: - P2-50 runtime contract checks and Linux CI final handoff convergence validation will run in Linux unified validation stage.## 81. Execution Log (2026-05-01, P2-51)- Card: `P2-51 Linux CI Workflow Release Final Closure Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py` (converges P2-50 final handoff artifact into release closure contract with `closed/closed_with_follow_up/blocked/contract_failed` status and `close_release/close_with_follow_up/close_blocker/abort_close` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-51`, adds release-final-closure outputs, stage wiring, and `--skip-release-final-closure` switch) - `scripts/run_linux_unified_gate.py` (adds P2-51 release final closure contract test to unified manifest) - `README.md` (adds P2-51 gate usage and updates P2-26 full-chain examples to P2-51) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-51 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py` (`T1` completed->closed contract, `T2` completed_with_follow_up->closed_with_follow_up contract, `T3` blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-51 stage presence, command path assertions, skip flag propagation, and release-final-closure-only stage contract coverage)- Not executed locally per release policy: - P2-51 runtime contract checks and Linux CI final closure convergence validation will run in Linux unified validation stage.## 82. Execution Log (2026-05-01, P2-52)- Card: `P2-52 Linux CI Workflow Release Final Closure Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py` (converges P2-51 release final closure artifact into final closure publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_closed/announce_release_closed_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-52`, adds release-final-closure-publish outputs, stage wiring, and `--skip-release-final-closure-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-52 release final closure publish contract test to unified manifest) - `README.md` (adds P2-52 gate usage and updates P2-26 full-chain examples to P2-52) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-52 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py` (`T1` closed->published contract, `T2` closed_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-52 stage presence, command path assertions, skip flag propagation, and release-final-closure-publish-only stage contract coverage)- Not executed locally per release policy: - P2-52 runtime contract checks and Linux CI final closure publish convergence validation will run in Linux unified validation stage.## 83. Execution Log (2026-05-01, P2-53)- Card: `P2-53 Linux CI Workflow Release Final Archive Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py` (converges P2-52 release final closure publish artifact into final archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_closed/archive_release_closed_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-53`, adds release-final-archive outputs, stage wiring, and `--skip-release-final-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-53 release final archive contract test to unified manifest) - `README.md` (adds P2-53 gate usage and updates P2-26 full-chain examples to P2-53) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-53 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-53 stage presence, command path assertions, skip flag propagation, and release-final-archive-only stage contract coverage)- Not executed locally per release policy: - P2-53 runtime contract checks and Linux CI final archive convergence validation will run in Linux unified validation stage.## 84. Execution Log (2026-05-01, P2-54)- Card: `P2-54 Linux CI Workflow Release Final Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py` (converges P2-53 final archive artifact into final verdict contract with `released/released_with_follow_up/blocked/contract_failed` status and `ship_release/ship_release_with_follow_up/escalate_release_blocker/abort_verdict` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-54`, adds release-final-verdict outputs, stage wiring, and `--skip-release-final-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-54 release final verdict contract test to unified manifest) - `README.md` (adds P2-54 gate usage and updates P2-26 full-chain examples to P2-54) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-54 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py` (`T1` archived->released contract, `T2` archived_with_follow_up->released_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-54 stage presence, command path assertions, skip flag propagation, and release-final-verdict-only stage contract coverage)- Not executed locally per release policy: - P2-54 runtime contract checks and Linux CI final verdict convergence validation will run in Linux unified validation stage.## 85. Execution Log (2026-05-01, P2-55)- Card: `P2-55 Linux CI Workflow Release Final Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py` (converges P2-54 final verdict artifact into final verdict publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_release_shipped/announce_release_shipped_with_follow_up/announce_release_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-55`, adds release-final-verdict-publish outputs, stage wiring, and `--skip-release-final-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-55 release final verdict publish contract test to unified manifest) - `README.md` (adds P2-55 gate usage and updates P2-26 full-chain examples to P2-55) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency chain extension, P2-55 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py` (`T1` released->published contract, `T2` released_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-55 stage presence, command path assertions, skip flag propagation, and release-final-verdict-publish-only stage contract coverage)- Not executed locally per release policy: - P2-55 runtime contract checks and Linux CI final verdict publish convergence validation will run in Linux unified validation stage.## 86. Execution Log (2026-05-01, P2-56)- Card: `P2-56 Linux CI Workflow Release Final Publish Archive Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py` (converges P2-55 final verdict publish artifact into final publish archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-56`, adds release-final-publish-archive outputs, stage wiring, and `--skip-release-final-publish-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-56 release final publish archive contract test to unified manifest) - `README.md` (adds P2-56 gate usage and updates P2-26 full-chain examples to P2-56) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-56 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-56 stage presence, command path assertions, skip flag propagation, and release-final-publish-archive-only stage contract coverage)- Not executed locally per release policy: - P2-56 runtime contract checks and Linux CI final publish archive convergence validation will run in Linux unified validation stage.## 87. Execution Log (2026-05-01, P2-57)- Card: `P2-57 Linux Gate Manifest Drift Closure Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_gate_manifest_drift_gate.py` (adds closure guard that scans `run_*_gate.py` -> `test_*_gate_runtime.py` mapping and validates Linux unified manifest coverage + orphan manifest entries, emits JSON/Markdown + GitHub outputs) - `scripts/run_linux_unified_gate.py` (adds P2-57 runtime contract test to Linux unified manifest) - `README.md` (adds P2-57 standalone usage while keeping P2-26 full-chain wording as `P2-17 -> P2-56`) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-57 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_gate_manifest_drift_gate_runtime.py` (`T1` gate scan include/exclude contract, `T2` runtime-test mapping contract, `T3` baseline drift report passed contract, `T4` github-output field contract)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - P2-57 runtime contract checks and Linux gate drift closure convergence validation will run in Linux unified validation stage.## 88. Execution Log (2026-05-01, P2-58)- Card: `P2-58 Linux CI Workflow Pipeline Closure to P2-57`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain description to `P2-17 -> P2-57`, wires `workflow_gate_manifest_drift` stage to call `scripts/run_p2_linux_gate_manifest_drift_gate.py`, adds `--gate-manifest-drift-json-output` / `--gate-manifest-drift-markdown-output`, and adds `--skip-gate-manifest-drift`) - `README.md` (updates P2-26 full-chain wording to `P2-17 -> P2-57` and adds P2-57-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds P2-58 execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-57 stage presence in default/partial chain, command assertions for drift-stage output paths, skip-flag propagation, and P2-57-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - P2-58 runtime contract checks and Linux CI workflow pipeline closure convergence validation will run in Linux unified validation stage.## 89. Execution Log (2026-05-01, P2-59)- Card: `P2-59 Linux CI Workflow Terminal Verdict Closure Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py` (converges P2-56 release final publish archive report and P2-57 gate manifest drift report into one terminal verdict contract with `ready_for_linux_validation/ready_with_follow_up_for_linux_validation/blocked/contract_failed` status and `proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-59`, adds terminal-verdict outputs, stage wiring, and `--skip-terminal-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-59 terminal verdict contract test to unified manifest) - `README.md` (adds P2-59 standalone usage, updates P2-26 full-chain examples to `P2-17 -> P2-59`, and adds P2-59-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-59 card, and execution log)- Test scripts landed: - `tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py` (`T1` archived+drift-passed->ready contract, `T2` archived-with-follow-up+drift-passed->ready-with-follow-up contract, `T3` blocked+drift-passed->blocked contract, `T4` archive mismatch or drift-failed->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-59 stage presence in default/partial chain, command assertions for terminal-verdict report paths, compatibility propagation for `skip_terminal_verdict`, and P2-59-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - P2-59 runtime contract checks and Linux CI terminal verdict closure convergence validation will run in Linux unified validation stage.## 90. Execution Log (2026-05-01, P2-60)- Card: `P2-60 Linux CI Workflow Linux Validation Dispatch Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_dispatch_gate.py` (converges P2-59 terminal verdict into Linux validation dispatch contract with `ready_dry_run/ready_with_follow_up_dry_run/dispatched/dispatch_failed/blocked/contract_failed` status and `dispatch_linux_validation/dispatch_linux_validation_with_follow_up/hold_linux_validation_blocker/abort_linux_validation_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-60`, adds Linux validation dispatch outputs, stage wiring, timeout flag, and `--skip-linux-validation-dispatch` switch) - `scripts/run_linux_unified_gate.py` (adds P2-60 Linux validation dispatch contract test to unified manifest) - `README.md` (adds P2-60 standalone usage, updates full-chain examples to `P2-17 -> P2-60`, and adds P2-60-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-60 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_dispatch_rt.py` (`T1` ready->ready_dry_run contract, `T2` ready_with_follow_up->ready_with_follow_up_dry_run contract, `T3` blocked->blocked contract, `T4` mismatch/drift/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-60 stage presence in default/partial chain, command assertions for Linux validation dispatch paths/timeout, and P2-60-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 91. Execution Log (2026-05-01, P2-61)- Card: `P2-61 Linux CI Workflow Linux Validation Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_verdict_gate.py` (converges P2-60 Linux validation dispatch artifact into one final Linux validation verdict contract with `validated/validated_with_follow_up/validation_failed/blocked/contract_failed` status and `accept_linux_validation/accept_linux_validation_with_follow_up/escalate_linux_validation_failure/hold_linux_validation_blocker/abort_linux_validation_verdict` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-61`, adds Linux validation verdict outputs, stage wiring, and `--skip-linux-validation-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-61 Linux validation verdict contract test to unified manifest) - `README.md` (adds P2-61 standalone usage, updates full-chain examples to `P2-17 -> P2-61`, and adds P2-61-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-61 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_verdict_rt.py` (`T1` ready_dry_run->validated contract, `T2` ready_with_follow_up_dry_run->validated_with_follow_up contract, `T3` dispatch_failed->validation_failed contract, `T4` dispatch/evidence mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-61 stage presence in default/partial chain, command assertions for Linux validation verdict paths, skip propagation for `skip_linux_validation_verdict`, and P2-61-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 92. Execution Log (2026-05-02, P2-62)- Card: `P2-62 Linux CI Workflow Linux Validation Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_verdict_publish_gate.py` (converges P2-61 Linux validation verdict artifact into one publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_linux_validation_passed/announce_linux_validation_passed_with_follow_up/announce_linux_validation_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-62`, adds Linux validation verdict publish outputs, stage wiring, and `--skip-linux-validation-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-62 Linux validation verdict publish contract test to unified manifest) - `README.md` (adds P2-62 standalone usage, updates full-chain examples to `P2-17 -> P2-62`, and adds P2-62-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-62 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_verdict_publish_rt.py` (`T1` validated->published contract, `T2` validated_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-62 stage presence in default/partial chain, command assertions for Linux validation verdict publish paths, skip propagation for `skip_linux_validation_verdict_publish`, and P2-62-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 93. Execution Log (2026-05-02, P2-63)- Card: `P2-63 Linux CI Workflow Linux Validation Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_terminal_publish_gate.py` (converges P2-62 Linux validation verdict publish artifact into one terminal publish contract with `terminal_published/terminal_published_with_follow_up/terminal_blocked/terminal_contract_failed` status and `announce_linux_validation_terminal_passed/announce_linux_validation_terminal_passed_with_follow_up/announce_linux_validation_terminal_blocker/abort_terminal_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-63`, adds Linux validation terminal publish outputs, stage wiring, and `--skip-linux-validation-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-63 Linux validation terminal publish contract test to unified manifest) - `README.md` (adds P2-63 standalone usage, updates full-chain examples to `P2-17 -> P2-63`, and adds P2-63-only pipeline command)- Test scripts landed: - `tests/test_p2_lv_terminal_publish_rt.py` (`T1` published->terminal_published contract, `T2` published_with_follow_up->terminal_published_with_follow_up contract, `T3` blocked->terminal_blocked contract, `T4` publish contract mismatch/evidence issue->terminal_contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-63 stage presence in default/partial chain, command assertions for Linux validation terminal publish paths, skip propagation for `skip_linux_validation_terminal_publish`, and P2-63-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 94. Execution Log (2026-05-02, P2-64)- Card: `P2-64 Linux CI Workflow Linux Validation Final Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_final_verdict_gate.py` (converges P2-63 Linux validation terminal publish artifact into final verdict contract with `validated/validated_with_follow_up/blocked/contract_failed` status and `accept_linux_validation_terminal/accept_linux_validation_terminal_with_follow_up/escalate_linux_validation_terminal_blocker/abort_linux_validation_terminal_verdict` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-64`, adds Linux validation final verdict outputs, stage wiring, and `--skip-linux-validation-final-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-64 Linux validation final verdict contract test to unified manifest) - `README.md` (adds P2-64 standalone usage, updates full-chain examples to `P2-17 -> P2-64`, and adds P2-64-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-64 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_final_verdict_rt.py` (`T1` terminal_published->validated contract, `T2` terminal_published_with_follow_up->validated_with_follow_up contract, `T3` terminal_blocked->blocked contract, `T4` terminal contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-64 stage presence in default/partial chain, command assertions for Linux validation final verdict paths, skip propagation for `skip_linux_validation_final_verdict`, and P2-64-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 95. Execution Log (2026-05-02, P2-65)- Card: `P2-65 Linux CI Workflow Linux Validation Final Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_final_verdict_publish_gate.py` (converges P2-64 Linux validation final verdict artifact into one publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_linux_validation_final_validated/announce_linux_validation_final_validated_with_follow_up/announce_linux_validation_final_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-65`, adds Linux validation final verdict publish outputs, stage wiring, and `--skip-linux-validation-final-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-65 Linux validation final verdict publish contract test to unified manifest) - `README.md` (adds P2-65 standalone usage, updates full-chain examples to `P2-17 -> P2-65`, and adds P2-65-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-65 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_final_verdict_publish_rt.py` (`T1` validated->published contract, `T2` validated_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` final verdict contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-65 stage presence in default/partial chain, command assertions for Linux validation final verdict publish paths, skip propagation for `skip_linux_validation_final_verdict_publish`, and P2-65-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 96. Execution Log (2026-05-02, P2-66)- Card: `P2-66 Linux CI Workflow Linux Validation Final Publish Archive Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_final_publish_archive_gate.py` (converges P2-65 Linux validation final verdict publish artifact into one final archive contract with `archived/archived_with_follow_up/blocked/contract_failed` status and `archive_release_shipped/archive_release_shipped_with_follow_up/archive_release_blocker/abort_archive` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-66`, adds Linux validation final publish archive outputs, stage wiring, and `--skip-linux-validation-final-publish-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-66 Linux validation final publish archive contract test to unified manifest) - `README.md` (adds P2-66 standalone usage, updates full-chain examples to `P2-17 -> P2-66`, and adds P2-66-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-66 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_final_publish_archive_rt.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` blocked->blocked contract, `T4` publish contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-66 stage presence in default/partial chain, command assertions for Linux validation final publish archive paths, skip propagation for `skip_linux_validation_final_publish_archive`, and P2-66-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 97. Execution Log (2026-05-02, P2-67)- Card: `P2-67 Linux CI Workflow Linux Validation Terminal Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_terminal_verdict_gate.py` (converges P2-66 Linux validation final publish archive artifact into one terminal verdict contract with `ready_for_linux_validation/ready_with_follow_up_for_linux_validation/blocked/contract_failed` status and `proceed_linux_validation/proceed_linux_validation_with_follow_up/halt_linux_validation_blocker/abort_linux_validation` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-67`, adds Linux validation terminal verdict outputs, stage wiring, and `--skip-linux-validation-terminal-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-67 Linux validation terminal verdict contract test to unified manifest) - `README.md` (adds P2-67 standalone usage, updates full-chain examples to `P2-17 -> P2-67`, and adds P2-67-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-67 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_terminal_verdict_rt.py` (`T1` archived->ready_for_linux_validation contract, `T2` archived_with_follow_up->ready_with_follow_up_for_linux_validation contract, `T3` blocked->blocked contract, `T4` archive contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-67 stage presence in default/partial chain, command assertions for Linux validation terminal verdict paths, skip propagation for `skip_linux_validation_terminal_verdict`, and P2-67-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 98. Execution Log (2026-05-02, P2-68)- Card: `P2-68 Linux CI Workflow Linux Validation Terminal Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_terminal_verdict_publish_gate.py` (converges P2-67 Linux validation terminal verdict artifact into one publish contract with `published/published_with_follow_up/blocked/contract_failed` status and `announce_linux_validation_terminal_ready/announce_linux_validation_terminal_ready_with_follow_up/announce_linux_validation_terminal_blocker/abort_publish` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-68`, adds Linux validation terminal verdict publish outputs, stage wiring, and `--skip-linux-validation-terminal-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-68 Linux validation terminal verdict publish contract test to unified manifest) - `README.md` (adds P2-68 standalone usage, updates full-chain examples to `P2-17 -> P2-68`, and adds P2-68-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-68 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_terminal_verdict_publish_rt.py` (`T1` ready_for_linux_validation->published contract, `T2` ready_with_follow_up_for_linux_validation->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` terminal verdict contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-68 stage presence in default/partial chain, command assertions for Linux validation terminal verdict publish paths, skip propagation for `skip_linux_validation_terminal_verdict_publish`, and P2-68-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 99. Execution Log (2026-05-02, P2-69)- Card: `P2-69 Linux CI Workflow Linux Validation Terminal Dispatch Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_dispatch_gate.py` (converges P2-68 Linux validation terminal verdict publish artifact into one terminal dispatch contract with `ready_dry_run/ready_with_follow_up_dry_run/blocked/contract_failed` status and `dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch` decision, plus consistency guards and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-69`, adds Linux validation terminal dispatch outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch` switch) - `scripts/run_linux_unified_gate.py` (adds P2-69 Linux validation terminal dispatch contract test to unified manifest) - `README.md` (adds P2-69 standalone usage, updates full-chain examples to `P2-17 -> P2-69`, and adds P2-69-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-69 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_dispatch_rt.py` (`T1` published->ready_dry_run contract, `T2` published_with_follow_up->ready_with_follow_up_dry_run contract, `T3` blocked->blocked contract, `T4` publish contract mismatch/evidence issue->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-69 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch paths, skip propagation for `skip_linux_validation_terminal_dispatch`, and P2-69-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 100. Execution Log (2026-05-02, P2-70)- Card: `P2-70 Linux CI Workflow Linux Validation Terminal Dispatch Execution Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_execution_gate.py` (executes P2-69 Linux validation terminal dispatch artifact into one terminal dispatch execution contract with `ready_dry_run/ready_with_follow_up_dry_run/dispatched/dispatch_failed/blocked/contract_failed` status and `dispatch_linux_validation_terminal/dispatch_linux_validation_terminal_with_follow_up/hold_linux_validation_terminal_blocker/abort_linux_validation_terminal_dispatch` decision, plus timeout/CLI-error handling and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-70`, adds Linux validation terminal dispatch execution outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-execution` switch) - `scripts/run_linux_unified_gate.py` (adds P2-70 Linux validation terminal dispatch execution contract test to unified manifest) - `README.md` (adds P2-70 standalone usage, updates full-chain examples to `P2-17 -> P2-70`, and adds P2-70-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-70 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_execution_rt.py` (`T1` ready_dry_run->ready_dry_run contract, `T2` ready_with_follow_up_dry_run+command_success->dispatched contract, `T3` blocked->blocked contract, `T4` contract mismatch rejection + command failure path contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-70 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch execution paths and timeout wiring, skip propagation for `skip_linux_validation_terminal_dispatch_execution`, and P2-70-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 101. Execution Log (2026-05-02, P2-71)- Card: `P2-71 Linux CI Workflow Linux Validation Terminal Dispatch Trace Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_trace_gate.py` (converges P2-70 Linux validation terminal dispatch execution artifact into one trace contract with `run_tracking_ready/run_tracking_missing/run_poll_failed/run_in_progress/run_completed_success/run_completed_failure` states, optional poll-now execution, and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-71`, adds Linux validation terminal dispatch trace outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-trace` switch) - `scripts/run_linux_unified_gate.py` (adds P2-71 Linux validation terminal dispatch trace contract test to unified manifest) - `README.md` (adds P2-71 standalone usage, updates full-chain examples to `P2-17 -> P2-71`, and adds P2-71-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-71 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_trace_rt.py` (`T1` dispatched+run-url->run_tracking_ready contract, `T2` dispatched without run-url->run_tracking_missing contract, `T3` dispatch_failed passthrough contract, `T4` execution contract mismatch rejection, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-71 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch trace paths and timeout wiring, skip propagation for `skip_linux_validation_terminal_dispatch_trace`, and P2-71-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 102. Execution Log (2026-05-03, P2-72)- Card: `P2-72 Linux CI Workflow Linux Validation Terminal Dispatch Completion Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_completion_gate.py` (converges P2-71 Linux validation terminal dispatch trace artifact into one completion-await contract with `run_completed_success/run_completed_failure/run_in_progress/run_poll_failed/run_await_timeout` outcomes, optional await-loop execution, and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-72`, adds Linux validation terminal dispatch completion outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-completion` switch) - `scripts/run_linux_unified_gate.py` (adds P2-72 Linux validation terminal dispatch completion contract test to unified manifest) - `README.md` (adds P2-72 standalone usage, updates full-chain examples to `P2-17 -> P2-72`, and adds P2-72-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-72 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_completion_rt.py` (`T1` run-tracking-ready+completed-success->run_completed_success contract, `T2` repeated in-progress->run_await_timeout contract, `T3` non-polling terminal passthrough contract, `T4` poll command contract mismatch rejection, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-72 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch completion paths and poll wiring, skip propagation for `skip_linux_validation_terminal_dispatch_completion`, and P2-72-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 103. Execution Log (2026-05-03, P2-73)- Card: `P2-73 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_terminal_publish_gate.py` (converges P2-72 Linux validation terminal dispatch completion artifact into one terminal publish contract with `terminal_published/terminal_published_with_follow_up/terminal_in_progress/terminal_blocked/terminal_failed/terminal_contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-73`, adds Linux validation terminal dispatch terminal publish outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-terminal-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-73 Linux validation terminal dispatch terminal publish contract test to unified manifest) - `README.md` (adds P2-73 standalone usage, updates full-chain examples to `P2-17 -> P2-73`, and adds P2-73-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-73 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_terminal_publish_rt.py` (`T1` completion success -> terminal_published contract, `T2` allow-in-progress timeout -> terminal_in_progress contract, `T3` completion mismatch -> terminal_contract_failed contract, `T4` github-output field contract for follow-up channel) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-73 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch terminal publish paths, skip propagation for `skip_linux_validation_terminal_dispatch_terminal_publish`, and P2-73-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 104. Execution Log (2026-05-03, P2-74)- Card: `P2-74 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_final_verdict_gate.py` (converges P2-73 Linux validation terminal dispatch terminal publish artifact into one final verdict contract with `validated/validated_with_follow_up/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-74`, adds Linux validation terminal dispatch final verdict outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-final-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-74 Linux validation terminal dispatch final verdict contract test to unified manifest) - `README.md` (adds P2-74 standalone usage, updates full-chain examples to `P2-17 -> P2-74`, and adds P2-74-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-74 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_final_verdict_rt.py` (`T1` terminal published -> validated contract, `T2` follow-up terminal published -> validated_with_follow_up contract, `T3` terminal blocked -> blocked contract, `T4` terminal publish contract mismatch -> contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-74 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch final verdict paths, skip propagation for `skip_linux_validation_terminal_dispatch_final_verdict`, and P2-74-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 105. Execution Log (2026-05-03, P2-75)- Card: `P2-75 Linux CI Workflow Linux Validation Terminal Dispatch Final Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_final_verdict_publish_gate.py` (converges P2-74 Linux validation terminal dispatch final verdict artifact into one publish contract with `published/published_with_follow_up/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-75`, adds Linux validation terminal dispatch final verdict publish outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-final-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-75 Linux validation terminal dispatch final verdict publish contract test to unified manifest) - `README.md` (adds P2-75 standalone usage, updates full-chain examples to `P2-17 -> P2-75`, and adds P2-75-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-75 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_final_verdict_publish_rt.py` (`T1` validated->published contract, `T2` validated_with_follow_up->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` final verdict contract mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-75 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch final verdict publish paths, skip propagation for `skip_linux_validation_terminal_dispatch_final_verdict_publish`, and P2-75-only stage contract coverage)## 106. Execution Log (2026-05-03, P2-76)- Card: `P2-76 Linux CI Workflow Linux Validation Terminal Dispatch Final Publish Archive Gate`- Result: landed- Key changes: - `scripts/run_p2_lv_td_final_publish_archive_gate.py` (converges P2-75 Linux validation terminal dispatch final verdict publish artifact into one final archive contract with `archived/archived_with_follow_up/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-76`, adds Linux validation terminal dispatch final publish archive outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-final-publish-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-76 Linux validation terminal dispatch final publish archive contract test to unified manifest) - `README.md` (adds P2-76 standalone usage, updates full-chain examples to `P2-17 -> P2-76`, and adds P2-76-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-76 card, and execution log)- Tests added/updated: - `tests/test_p2_lv_td_final_publish_archive_rt.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` in_progress->in_progress contract, `T4` blocked/failed archive contracts, `T5` publish contract mismatch->contract_failed contract, `T6` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-76 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch final publish archive paths, skip propagation for `skip_linux_validation_terminal_dispatch_final_publish_archive`, and P2-76-only stage contract coverage)## 107. Execution Log (2026-05-03, P2-77)- Card: `P2-77 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_terminal_verdict_gate.py` (converges P2-76 Linux validation terminal dispatch final publish archive artifact into one terminal verdict contract with `ready_for_linux_validation_terminal_dispatch/ready_with_follow_up_for_linux_validation_terminal_dispatch/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-77`, adds Linux validation terminal dispatch terminal verdict outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-terminal-verdict` switch) - `scripts/run_linux_unified_gate.py` (adds P2-77 Linux validation terminal dispatch terminal verdict contract test to unified manifest) - `README.md` (adds P2-77 standalone usage, updates full-chain examples to `P2-17 -> P2-77`, and adds P2-77-only command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-77 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_terminal_verdict_rt.py` (`T1` archived->ready_for_linux_validation_terminal_dispatch contract, `T2` archived_with_follow_up->ready_with_follow_up_for_linux_validation_terminal_dispatch contract, `T3` in_progress->in_progress contract, `T4` blocked->blocked contract, `T5` final publish archive mismatch->contract_failed contract, `T6` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-77 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch terminal verdict paths, skip propagation for `skip_linux_validation_terminal_dispatch_terminal_verdict`, and P2-77-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` passed. - `py -3 scripts/check_syntax.py --root tests --json` passed.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 108. Execution Log (2026-05-03, P2-78)- Card: `P2-78 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_tverdict_publish_gate.py` (converges P2-77 Linux validation terminal dispatch terminal verdict artifact into one publish contract with `published/published_with_follow_up/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-78`, adds Linux validation terminal dispatch terminal verdict publish outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-terminal-verdict-publish` switch) - `scripts/run_linux_unified_gate.py` (adds P2-78 Linux validation terminal dispatch terminal verdict publish contract test to unified manifest) - `README.md` (adds P2-78 standalone usage, updates full-chain examples to `P2-17 -> P2-78`, and adds P2-78-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-78 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_tverdict_publish_rt.py` (`T1` ready_for_linux_validation_terminal_dispatch->published contract, `T2` ready_with_follow_up_for_linux_validation_terminal_dispatch->published_with_follow_up contract, `T3` blocked->blocked contract, `T4` terminal verdict contract mismatch->contract_failed contract, `T5` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-78 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch terminal verdict publish paths, skip propagation for `skip_linux_validation_terminal_dispatch_terminal_verdict_publish`, and P2-78-only stage contract coverage)- Verification performed (without running tests): - `py -3 scripts/check_syntax.py --root scripts --json` pending. - `py -3 scripts/check_syntax.py --root tests --json` pending.- Not executed locally per release policy: - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage.## 109. Execution Log (2026-05-03, P2-79)- Card: `P2-79 Linux CI Workflow Linux Validation Terminal Dispatch Terminal Verdict Publish Archive Gate`- Status: `implemented` (Linux full-suite verification pending)- Code landed: - `scripts/run_p2_lv_td_tverdict_publish_archive_gate.py` (converges P2-78 Linux validation terminal dispatch terminal verdict publish artifact into one archive contract with `archived/archived_with_follow_up/in_progress/blocked/failed/contract_failed` outcomes and markdown/json/github-output contracts) - `scripts/run_p2_linux_ci_workflow_pipeline_gate.py` (extends stage chain to compose `P2-17 -> P2-79`, adds Linux validation terminal dispatch terminal verdict publish archive outputs, stage wiring, skip propagation, and `--skip-linux-validation-terminal-dispatch-terminal-verdict-publish-archive` switch) - `scripts/run_linux_unified_gate.py` (adds P2-79 Linux validation terminal dispatch terminal verdict publish archive contract test to unified manifest) - `README.md` (adds P2-79 standalone usage, updates full-chain examples to `P2-17 -> P2-79`, and adds P2-79-only pipeline command) - `docs/current/architecture/EXECUTION_TASK_CARDS_2026-04-29.md` (adds dependency-chain extension, P2-79 card, and execution log)- Test scripts landed: - `tests/test_p2_lv_td_tverdict_publish_archive_rt.py` (`T1` published->archived contract, `T2` published_with_follow_up->archived_with_follow_up contract, `T3` in_progress->in_progress contract, `T4` blocked/failed archive contracts, `T5` publish contract mismatch->contract_failed contract, `T6` github-output field contract) - `tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py` (adds P2-79 stage presence in default/partial chain, command assertions for Linux validation terminal dispatch terminal verdict publish archive paths, skip propagation for `skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive`, and P2-79-only stage contract coverage)- Verification performed (without running tests): - No local pytest execution by policy for this run.- Not executed locally per release policy: - Runtime contract tests remain pending for Linux unified validation stage. +## 110. Execution Log (2026-05-05, P2-106) +- Card: P2-106 Linux CI Workflow Pipeline Closure to P2-105 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py (verifies P2-105 closure contract stability from P2-104 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-106 runtime contract test into Linux unified manifest) +- Test scripts landed: + - ests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py (T1 repo contract pass for P2-106->P2-105 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 111. Execution Log (2026-05-05, P2-108) +- Card: P2-108 Linux CI Workflow Pipeline Closure to P2-107 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py (verifies P2-107 closure contract stability from P2-106 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-108 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py (T1 repo contract pass for P2-108->P2-107 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 112. Execution Log (2026-05-05, P2-109) +- Card: P2-109 Linux CI Workflow Pipeline Closure to P2-108 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py (verifies P2-108 closure contract stability from P2-107 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-109 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py (T1 repo contract pass for P2-109->P2-108 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 113. Execution Log (2026-05-05, P2-110) +- Card: P2-110 Linux CI Workflow Pipeline Closure to P2-109 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py (verifies P2-109 closure contract stability from P2-108 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-110 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py (T1 repo contract pass for P2-110->P2-109 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + + +## 114. Execution Log (2026-05-05, P2-111) +- Card: P2-111 Linux CI Workflow Pipeline Closure to P2-110 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py (verifies P2-110 closure contract stability from P2-109 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-111 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py (T1 repo contract pass for P2-111->P2-110 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 115. Execution Log (2026-05-05, P2-112) +- Card: P2-112 Linux CI Workflow Pipeline Closure to P2-111 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py (verifies P2-111 closure contract stability from P2-110 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-112 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py (T1 repo contract pass for P2-112->P2-111 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 116. Execution Log (2026-05-05, P2-113) +- Card: P2-113 Linux CI Workflow Pipeline Closure to P2-112 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py (verifies P2-112 closure contract stability from P2-111 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-113 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py (T1 repo contract pass for P2-113->P2-112 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 117. Execution Log (2026-05-05, P2-114) +- Card: P2-114 Linux CI Workflow Pipeline Closure to P2-113 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py (verifies P2-113 closure contract stability from P2-112 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-114 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py (T1 repo contract pass for P2-114->P2-113 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 118. Execution Log (2026-05-05, P2-115) +- Card: P2-115 Linux CI Workflow Pipeline Closure to P2-114 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py (verifies P2-114 closure contract stability from P2-113 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-115 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py (T1 repo contract pass for P2-115->P2-114 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 119. Execution Log (2026-05-05, P2-116) +- Card: P2-116 Linux CI Workflow Pipeline Closure to P2-115 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py (verifies P2-115 closure contract stability from P2-114 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-116 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py (T1 repo contract pass for P2-116->P2-115 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 120. Execution Log (2026-05-06, P2-117) +- Card: P2-117 Linux CI Workflow Pipeline Closure to P2-116 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py (verifies P2-116 closure contract stability from P2-115 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-117 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py (T1 repo contract pass for P2-117->P2-116 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. + +## 121. Execution Log (2026-05-06, P2-118) +- Card: P2-118 Linux CI Workflow Pipeline Closure to P2-117 Gate +- Status: implemented (Linux full-suite verification pending) +- Code landed: + - scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_117_gate.py (verifies P2-117 closure contract stability from P2-116 artifacts, emits passed/failed decision contract with normalized JSON/Markdown/GitHub outputs) + - scripts/run_linux_unified_gate.py (adds P2-118 runtime contract test into Linux unified manifest) +- Test scripts landed: + - tests/test_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_runtime.py (T1 repo contract pass for P2-118->P2-117 closure, T2 missing-pattern failure contract, T3 missing-artifact failure contract, T4 GitHub output field contract) +- Verification performed (without running tests): + - py -3 scripts/check_syntax.py --root scripts --json passed. + - py -3 scripts/check_syntax.py --root tests --json passed. +- Not executed locally per release policy: + - No pytest execution; all new/updated runtime contract tests remain pending for Linux unified validation stage. diff --git a/docs/current/architecture/PYTHON_ENGINEERING_EVALUATION_2026-04-17.md b/docs/current/architecture/PYTHON_ENGINEERING_EVALUATION_2026-04-17.md new file mode 100644 index 0000000..a525f66 --- /dev/null +++ b/docs/current/architecture/PYTHON_ENGINEERING_EVALUATION_2026-04-17.md @@ -0,0 +1,676 @@ +# Claude Code Python Python 工程评估报告(2026-04-17) + +- 项目:`claude-code-python` +- 评估日期:`2026-04-17` +- 评估视角:`top-python-dev` + Python 工程质量 / 可维护性 / 扩展性 / 发布完整性 +- 关联基线文档: + - `docs/current/architecture/ARCHITECTURE_EVALUATION_2026-04-16.md` + - `docs/current/middleware/MIDDLEWARE_EVOLUTION_ASSESSMENT_2026-04-16.md` + - `docs/current/performance/PERFORMANCE_ONE_PAGER.md` + - `docs/current/performance/BASELINE_V1.md` + +--- + +## 1. 执行摘要 + +### 1.1 总体结论 + +当前仓库已经具备“核心 CLI 主链路可运行、测试可回归、任务中间件开始收敛”的基础能力,但从全球顶级 Python 工程标准看,**仍然明显停留在“工程可用基线”而非“可稳定演进平台”阶段**。 + +更具体地说: + +1. **主链路健康度不错**:语法检查通过,全量测试 `90 passed`,旧文档中已经闭环的 Query / permission / runtime 基线大体保持稳定。 +2. **发布与扩展面明显落后**:`pyproject.toml` 无法解析、`setup.py` 安装元数据有硬错误、MCP 客户端主链路并未真正打通、插件本地加载器只能读元数据不能加载执行实例。 +3. **配置契约与运行时契约不一致**:文档、`main.py`、`config.py`、命令层和 provider 层对“支持哪些 provider、model 如何生效、配置从哪里读取”给出了彼此冲突的答案。 +4. **迭代风险仍偏高**:代码体量已经接近 `4.8 万 LOC`,存在 `133` 处宽泛异常、`34` 处占位函数、多个 `700+ LOC` 的核心文件,说明后续任何跨模块改动都需要更强的契约测试和模块收敛。 + +### 1.2 结论评分 + +- 核心 CLI 主链路:`8.2 / 10` +- Python 工程完整度:`7.1 / 10` +- 发布/扩展面成熟度:`5.8 / 10` + +### 1.3 当前最值得优先处理的四条线 + +1. 修复**打包发布链路**,否则项目无法形成可靠分发闭环。 +2. 修复**MCP / 插件扩展面**,否则“支持 MCP/插件”目前更像文档承诺而不是可用能力。 +3. 收敛**配置 -> 命令 -> runtime -> provider** 的单一事实来源。 +4. 收敛**应用生命周期 / 调度 / 工作目录 / 子代理执行上下文** 的行为一致性。 + +--- + +## 2. 评估范围与验证方法 + +### 2.1 覆盖范围 + +本次评估覆盖: + +1. `claude_code/` 主代码目录 +2. `tests/` 当前测试体系 +3. `README.md`、`pyproject.toml`、`setup.py`、`requirements.txt` +4. `docs/current/` 下当前架构 / 中间件 / 性能文档 + +### 2.2 已执行验证 + +1. 语法检查: + - 命令:`D:\code_environment\anaconda_all_css\py311\python.exe scripts/check_syntax.py --root claude_code` + - 结果:`208` 个文件,`0` 个语法错误 +2. 全量测试: + - 命令:`D:\code_environment\anaconda_all_css\py311\python.exe -m pytest -q -c pytest.ini` + - 结果:`90 passed, 3 warnings` +3. 补充事实验证: + - `pyproject.toml` 用 `tomllib` 解析,结果 `TOMLDecodeError` + - `Config(api_provider="ollama")` / `vllm` / `deepseek` 直接触发 `ValidationError` + - `packaging.requirements.Requirement("boto3>=1.35.0 # AWS Bedrock")` 解析失败 + - `ClaudeMdLoader` 在父目录存在 `CLAUDE.md` 时,`find_claude_md_files()` 能找到文件,但 `load_content()` 返回 `None` + +### 2.3 本次无法完成的检查 + +1. `ruff` 与 `mypy` 在给定解释器环境中不可用: + - `No module named ruff` + - `No module named mypy` +2. 因此本次静态质量结论主要来自: + - 手工代码审查 + - AST/文本扫描 + - 当前测试与定点复现 + +--- + +## 3. 量化快照 + +基于本地 AST / 文本扫描得到的当前快照如下: + +| 指标 | 数值 | +|---|---:| +| Python 文件数 | `208` | +| 代码总行数(约) | `47,864` | +| 函数 / 方法总数 | `2,237` | +| `async def` 数量 | `461` | +| 类数量 | `550` | +| 宽泛异常(`except:` / `except Exception`) | `133` | +| 占位函数(`pass` / 空壳接口) | `34` | +| 缺失返回类型标注 | `29` | +| 缺失参数类型标注 | `41` | +| 含 `Any` 的函数源码片段 | `537` | + +### 3.1 复杂度热点 + +文件体量 Top 6: + +1. `claude_code/skills/builtin.py`:`1040` 行 +2. `claude_code/services/mcp/client.py`:`887` 行 +3. `claude_code/ui/rendering.py`:`784` 行 +4. `claude_code/tasks/manager.py`:`777` 行 +5. `claude_code/engine/query.py`:`744` 行 +6. `claude_code/api/client.py`:`645` 行 + +长函数 Top 6: + +1. `claude_code/agents/builtin.py:create_builtin_agents`:`209` 行 +2. `claude_code/tools/registry.py:create_default_registry`:`170` 行 +3. `claude_code/engine/query.py:query`:`142` 行 +4. `claude_code/main.py:main`:`140` 行 +5. `claude_code/tools/builtin/bash.py:execute`:`109` 行 +6. `claude_code/hooks/registry.py:_execute_hook`:`102` 行 + +结论:**当前仓库最主要的工程风险已经不再是“能不能跑”,而是“局部复杂度过高 + 契约边界不清 + 扩展面未收敛”。** + +--- + +## 4. 与旧文档的关系 + +### 4.1 旧文档中已关闭、且本次复核仍然成立的事项 + +以下事项本次未发现回滚: + +1. Query 主链路基础可运行。 +2. 权限系统已从“多套并行语义”收敛到可工作的 canonical 路径。 +3. 任务仓储/队列抽象已具备雏形,`tasks.json` 也已有 schema 兼容层。 +4. 全量测试已从旧文档的 `68 passed` 增长到 `90 passed`。 + +### 4.2 旧文档覆盖不足、但本次发现仍然重要的方向 + +旧文档较少覆盖以下内容,而这些内容会直接影响后续迭代开发: + +1. Python 打包/发布完整性 +2. MCP 客户端是否真的可用 +3. 插件本地加载器是否真的能加载代码 +4. 配置系统与命令系统是否驱动真实 runtime +5. 工作目录 / 子代理上下文是否真的被正确传播 + +--- + +## 5. 关键问题清单(按严重级别) + +## P0:必须优先处理,否则会直接阻断后续迭代 + +### P0-1:打包发布链路当前不可作为可靠分发入口 + +证据: + +1. `pyproject.toml:1-39` 不是 TOML,而是 JSON 风格对象;本地 `tomllib` 解析直接失败。 +2. `pyproject.toml:9` 与 `pyproject.toml:14` 出现重复 `rich` 依赖。 +3. `setup.py:7-11` 直接把 `requirements.txt` 每行 `line.strip()` 放进 `install_requires`,而 `requirements.txt:16-18` 包含内联注释,生成的 requirement 字符串不符合 PEP 508。 +4. `setup.py:39-40` 注册了 `claude_code.gui:main`,但仓库中不存在 `claude_code/gui.py`。 +5. `setup.py:17`、`setup.py:21` 仍是占位元数据(`example@example.com` / `your-repo`)。 + +影响: + +1. 无法把仓库稳定发布为可安装 Python 包。 +2. 本地开发“从源码运行”与“通过包安装运行”是两条完全不同的路径。 +3. 后续若接 CI 发布、wheel 构建、editable install、插件分发,会被这条链路卡死。 + +建议: + +1. 先在单个 PR 中收敛为**唯一打包事实源**,推荐标准 TOML。 +2. `requirements.txt` 不要直接喂给 `install_requires`;可拆成 runtime / optional / dev 三层。 +3. 删除或补齐 `claude_code.gui` 入口,不能再保留悬空 entry point。 + +### P0-2:`claude_code.services.mcp.client` 主链路未真正打通 + +证据: + +1. `claude_code/services/mcp/client.py:407-427` + - `MCPProtocol.send_request()` 只创建 `future` 和 `message`,**没有把消息发送到任何 transport**,最后直接 `return await future`。 +2. `claude_code/services/mcp/client.py:565`、`590`、`609`、`634`、`649` + - `MCPClient` 多处调用 `self.protocol.send_request(...)`,因此这些路径理论上都会悬挂。 +3. `claude_code/services/mcp/client.py:136-140` + - `MCPStdIOTransport.send()` 对 `stdin.write()` / `stdin.flush()` 使用 `await`,但这些接口不是 awaitable;这里是明显的 asyncio API 使用错误。 +4. `claude_code/services/mcp/client.py:532-536` + - HTTP/SSE transport 仍然是 `NotImplementedError`。 +5. `claude_code/services/mcp/client.py:289-292` + - `MCPWebSocketTransport.connect()` 创建 `aiohttp.ClientSession()` 后没有持久保存 session 句柄,也没有关闭。 +6. `claude_code/services/mcp/client.py:322-341` + - `_reader_loop()` 内部直接引用 `aiohttp.WSMsgType`,但该名字不在方法作用域中,运行时会 `NameError`。 + +影响: + +1. MCP 客户端目前更像“接口草图”,不是可依赖能力。 +2. 后续若围绕 MCP 接入生态扩展,这部分会持续制造误判。 + +建议: + +1. 把 `MCPProtocol` 与 transport 真正绑定,先打通一个可工作的 `STDIO` 路径。 +2. HTTP/SSE / WebSocket 不要提前宣称支持,未打通前应显式 feature-flag 或 fail-fast。 +3. 为 `MCPClient.connect -> initialize -> list tools/resources -> call tool` 补一条契约测试主链路。 + +### P0-3:本地插件加载器只能读元数据,无法把插件变成可执行实例 + +证据: + +1. `claude_code/plugins/__init__.py:252-273` + - `LocalPluginLoader.load()` 只读取 `plugin.json` 并返回 `Plugin(...)`,没有导入插件模块,也没有设置 `_instance`。 +2. `claude_code/plugins/__init__.py:186-209` + - `get_all_tools()` / `get_all_commands()` / `get_all_hooks()` 都依赖 `plugin.instance` 实现 `ToolPlugin` / `CommandPlugin` / `HookPlugin`。 + +结果: + +1. Builtin loader 能拿到 `module.get_plugin()` 实例(`claude_code/plugins/__init__.py:234-235`)。 +2. Local loader 只能返回 metadata,导致本地插件处于“登记了,但不能提供功能”的状态。 + +影响: + +1. 插件系统对本地开发者基本不可用。 +2. 文档/架构上宣称的 plugin extensibility 与真实行为不一致。 + +建议: + +1. 为 local plugin 明确入口约定,例如 `plugin.py:get_plugin()`。 +2. 给 local / builtin / marketplace 三类插件做统一 loader 抽象,不要只在 builtin 路径加载实例。 + +### P0-4:Cron 体系出现双文件、双 schema、双实现的分裂 + +证据: + +1. `claude_code/tools/cron/create.py:214`、`268` + - `CronCreateTool` 使用 `.claude/scheduled_tasks.json`。 +2. `claude_code/tools/cron/__init__.py:113`、`195`、`285` + - `ScheduleCronTool` / `CronListTool` / `CronDeleteTool` 使用 `.claude/schedules.json`。 +3. `claude_code/tools/cron/create.py:171`、`232` + - `CronCreateTool` 还区分 `durable` 与 `session-only`。 +4. `claude_code/tools/cron/__init__.py:117-129`、`200-201`、`298-299` + - 老的 cron tools 直接 `open/json.load/json.dump`,没有复用新路径的原子写/会话语义。 + +影响: + +1. `cron_create` 创建的内容,`cron_list` / `cron_delete` 不一定能看到。 +2. 对调度系统做任何迭代时,都会先踩到数据分裂问题。 + +测试现状: + +1. 当前只有 `tests/test_tasks_middleware_runtime.py` 覆盖了 `CronCreateTool` 的 `durable` 行为。 +2. 没有看到针对 `ScheduleCronTool` / `CronListTool` / `CronDeleteTool` 与 `CronCreateTool` 一致性的回归测试。 + +建议: + +1. 只保留一套 cron 数据模型。 +2. 统一文件名、schema、会话语义、原子写策略。 +3. 旧路径若保留,必须降级为兼容层,不能继续并行演进。 + +--- + +## P1:高优先级问题,会持续侵蚀后续开发效率 + +### P1-1:配置系统、命令系统、运行时对 provider/model 的定义并不一致 + +证据: + +1. `claude_code/config.py:40` + - `VALID_PROVIDERS` 只允许 `anthropic/openai/bedrock/vertex/azure`。 +2. `claude_code/main.py:56-69`、`77-88` + - `main.py` 明确支持 `ollama/vllm/deepseek`。 +3. `README.md:12`、`56-64` + - README 也明确把 `ollama/vllm/deepseek` 作为当前能力对外暴露。 +4. 本地复现: + - `Config(api_provider="ollama")` / `vllm` / `deepseek` 均直接 `ValidationError`。 +5. `claude_code/config.py:337-342` + - `update_from_env()` 只识别 `CLAUDE_API_PROVIDER` 属于 `VALID_PROVIDERS` 的情况,因此本地 provider 无法通过 config 正常进入配置对象。 + +额外不一致: + +1. `azure` 在 `config.py` 中是合法 provider,但 `main.py` 与 `api/client.py` 当前都 fail-fast 禁用。 +2. 这意味着配置层允许的 provider,不等于 runtime 真正能启动的 provider。 + +建议: + +1. 定义一份**单一 provider 能力矩阵**。 +2. `config.py`、`README`、`main.py`、`api/client.py` 必须共享同一个 truth source。 +3. 对“已声明支持但尚未可靠”的 provider,统一改成 feature-flag + fail-fast 描述。 + +### P1-2:`/model` 与持久化配置不会真实驱动 runtime + +证据: + +1. `claude_code/commands/model/__init__.py:51-53` + - `/model` 会把 `config.model` 写入配置并 `save_config()`。 +2. `claude_code/main.py:177-182` + - `create_engine()` 的 model 来源是参数或 `CLAUDE_MODEL` 环境变量,然后只把 `get_config()` 用于 permission 传播。 +3. `claude_code/main.py:69-116` + - `setup_api_client()` 也直接从环境变量读取 provider/api key,而不是使用 `Config`。 + +影响: + +1. 用户通过 `/model` 或配置文件改的值,不一定影响真正执行的 engine。 +2. 项目看起来有“完整配置系统”,实际上主链路仍是“环境变量优先且近乎唯一生效”。 + +建议: + +1. 明确配置优先级:CLI args > local config > user config > env vars > defaults。 +2. 让 `create_engine()` / `setup_api_client()` 统一从 `Config` 读取,而不是各自重新拼装。 + +### P1-3:`working_dir` 只影响 system prompt,不影响工具真实执行目录 + +证据: + +1. `claude_code/main.py:162-188` + - `create_engine(working_dir=...)` 只把 `working_dir` 传给 `build_system_prompt()`。 +2. `claude_code/engine/query.py:567-576` + - 工具执行时的 `ToolContext.working_directory` 直接写死为 `os.getcwd()`。 + +影响: + +1. 上下文显示的工作目录与工具实际操作目录可能不同。 +2. 后续若做 worktree、多仓、多根目录、远程执行,这会变成严重的一致性问题。 + +建议: + +1. `QueryConfig` 或 `QueryEngine` 应持有 canonical working directory。 +2. 所有 tool / task / agent / context builder 都从该统一字段取值。 + +### P1-4:`AgentTool` 的后台路径忽略了调用方上下文 + +证据: + +1. `claude_code/tools/agent/__init__.py:184-199` + - `effective_cwd` 被计算并传入 `_run_async_agent()`。 +2. `claude_code/tools/agent/__init__.py:296-313` + - 后台路径里重新构造 `APIClient(APIClientConfig())` 与 `QueryEngine(api_client=api_client)`,没有把 `cwd`、`tool_registry`、permission、provider 传进去。 +3. `claude_code/tools/agent/__init__.py:308-311` + - background agent 的 `engine.config.system_prompt` 只用 `agent_def.prompt`,不带父上下文拼接后的 cwd 信息。 + +结果: + +1. 后台子代理不能稳定继承父 agent 的 provider / model / working directory / 权限上下文。 +2. `run_in_background=True` 与同步执行路径的行为语义已经分叉。 + +建议: + +1. 把子代理构造收敛到统一 builder。 +2. 同步 / 后台 / worktree 三条路径共享同一份上下文构建逻辑。 + +### P1-5:`ClaudeMdLoader` 会静默丢失父目录中的 `CLAUDE.md` + +证据: + +1. `claude_code/engine/context.py:244-283` + - `find_claude_md_files()` 会向上查找父目录 `CLAUDE.md`。 +2. `claude_code/engine/context.py:283` + - `filepath.relative_to(self._working_dir)` 对父目录文件会抛 `ValueError`。 +3. 由于外围 `except Exception: continue`,最终内容会被静默跳过。 + +本地复现结果: + +1. `find_claude_md_files()` 返回父目录文件。 +2. `load_content()` 最终返回 `None`。 + +影响: + +1. 这是一个真实的上下文丢失 bug,不是代码风格问题。 +2. 上游目录里的团队规范无法稳定注入 prompt。 + +建议: + +1. 用相对于共同祖先目录或绝对路径展示文件名。 +2. 不要用宽泛异常把这类路径错误静默吞掉。 + +### P1-6:应用生命周期实现存在隐藏的副作用和一致性问题 + +证据: + +1. `claude_code/app.py:200-204` + - `Application._setup()` 里先创建并启动 `ShutdownManager`,然后又调用自己的 `_register_signal_handlers()`。 +2. `claude_code/services/shutdown.py:31`、`67` + - `force_after` 与 `_force_task` 已定义,但未真正实现强制关闭逻辑。 +3. `claude_code/app.py:220-225` + - `_teardown()` 会对 `cleanup=None` 的服务执行 `cleanup()`,只能靠 `try/except` 吞掉错误。 +4. `claude_code/app.py:289-291` + - `register_factory()` 为了推断类型直接执行 `factory()`,会提前触发副作用。 + +影响: + +1. 生命周期行为难以预测。 +2. 注册阶段与运行阶段边界不清。 +3. 后续接入真实资源(DB 连接、网络客户端、线程池)时会放大问题。 + +建议: + +1. 只保留一套 signal ownership。 +2. `register_factory()` 明确传入 service type,禁止为推断类型执行 factory。 +3. `_services` 不要再把 cleanup 和 service 元数据混放。 + +### P1-7:工具注册表的“lazy”只做到了延迟实例化,没有做到延迟导入 + +证据: + +1. `claude_code/tools/registry.py:154-188` + - `create_default_registry()` 内部会一次性 import 大量工具模块。 +2. `claude_code/tools/registry.py:157-158` + - 注释宣称“显著提升启动时间”,但真实行为只是延迟对象创建,不是延迟模块导入。 + +影响: + +1. 冷启动成本仍然偏高。 +2. 模块导入副作用依旧会在注册阶段触发。 + +补充问题: + +1. `claude_code/tools/registry.py:139-142` + - `get_definitions()` 只返回已经实例化的工具定义,不是全部注册工具。 +2. `claude_code/tools/registry.py:151` + - `get_names()` 把 `_tools.keys()` 与 `_lazy_factories.keys()` 直接拼接,存在重复项。 + +建议: + +1. 把“模块发现”和“实例化”都做成真正的 lazy。 +2. 对 registry API 建立清晰契约:`get_names()`、`get_definitions()`、`list_all()` 各自保证什么。 + +### P1-8:Provider 支持矩阵被高估,Vertex 路径大概率仍不完整 + +证据: + +1. `claude_code/api/client.py:249-259` + - `_setup_vertex()` 会调用 `google.auth.default()` 获取凭证,但后续只是构造 `AsyncAnthropic(base_url=...)`。 +2. 代码中没有看到把 Google credentials 注入 bearer token / transport / request signing 的过程。 + +判断: + +1. 这是**基于代码的推断**,不是执行后的黑盒结论。 +2. 但从当前实现看,Vertex 路径极可能仍处于“看起来支持,实际并未真正接好认证”的状态。 + +建议: + +1. provider 文档必须区分“已实测可用”和“仅接口占位”。 +2. 给 Vertex / Bedrock / OpenAI-compatible 每条路径都补最小契约测试。 + +--- + +## P2:中优先级问题,会持续拉低可维护性 + +### P2-1:测试是绿色的,但测试盲区依然明显 + +已知现状: + +1. 当前测试覆盖 Query / permissions / runtime tasks / middleware backend contract 基础路径,这一点是好的。 +2. 但没有看到以下方向的有效回归保护: + - Python 打包安装链路 + - `pyproject.toml` / `setup.py` 合法性 + - `PluginManager` / `LocalPluginLoader` + - `MCPClient` / `MCPManager` + - `Application.register_factory()` / 生命周期副作用 + - `ScheduleCronTool` / `CronListTool` / `CronDeleteTool` 与 `CronCreateTool` 的一致性 + - provider config chain(`Config` / `/model` / `main.py` / `APIClient`) + +结论: + +`90 passed` 很有价值,但当前更准确的表达应是:**核心回归保护不错,扩展面与分发面几乎没有得到同等级别验证。** + +### P2-2:宽泛异常过多,错误被静默吞掉的概率偏高 + +量化结果: + +1. 全仓库共有 `133` 处宽泛异常。 +2. 热点文件包括: + - `claude_code/engine/context.py` + - `claude_code/api/client.py` + - `claude_code/tasks/manager.py` + - `claude_code/tools/mcp/__init__.py` + - `claude_code/app.py` + +风险: + +1. 这会让“功能降级”与“功能损坏”在表面上都表现为“静默继续运行”。 +2. 在多 provider、多工具、多 transport 场景下,排障成本会非常高。 + +建议: + +1. 只在明确容错点使用宽泛异常。 +2. 对 fallback、degrade、skip 的位置补结构化日志。 + +### P2-3:类型系统债务已经开始影响可读性和重构安全 + +量化结果: + +1. 缺失返回类型标注:`29` +2. 缺失参数类型标注:`41` +3. 含 `Any` 的函数源码片段:`537` + +热点文件: + +1. `claude_code/remote.py` +2. `claude_code/plugins/__init__.py` +3. `claude_code/config.py` +4. `claude_code/services/cache_service.py` +5. `claude_code/services/token_estimation.py` +6. `claude_code/tasks/repository.py` + +建议: + +1. 优先收敛核心边界对象: + - provider response + - plugin instance + - MCP transport / message + - task repository record +2. 先减少公共接口层的 `Any`,再向实现层推进。 + +### P2-4:核心文件体量已经超过舒适维护阈值 + +高风险大文件: + +1. `claude_code/services/mcp/client.py` +2. `claude_code/tasks/manager.py` +3. `claude_code/engine/query.py` +4. `claude_code/api/client.py` +5. `claude_code/config.py` + +高风险长函数: + +1. `create_default_registry` +2. `QueryEngine.query` +3. `main` +4. `BashTool.execute` + +建议: + +1. 先按“稳定边界”拆,不要按“代码行数平均分”拆。 +2. 最优先拆分方向: + - `MCP transport / protocol / auth / manager` + - `QueryEngine loop / tool execution / response normalization` + - `config loading / env overlay / provider config` + +### P2-5:警告预算并未清零 + +本次测试警告: + +1. `claude_code/utils/__init__.py:129` + - 仍从 deprecated 的 `claude_code.utils.features` 导入。 +2. `claude_code/app.py:58` + - 仍导入 deprecated 的 `claude_code.utils.logging_system`。 + +这不是阻塞问题,但说明“弃用中的旧路径”仍然在主代码中存活,会持续增加认知成本。 + +### P2-6:`QueryEngine` 仍有未完工公共接口和细节一致性问题 + +证据: + +1. `claude_code/engine/query.py:676` + - `load_skill()` 仍是占位。 +2. `claude_code/engine/query.py:741` + - `resume_session()` 仍是占位。 +3. `claude_code/engine/query.py:605` + - 参数校验失败时 `duration_ms` 返回的是秒,不是毫秒。 + +结论: + +这部分说明 `QueryEngine` 既承担主链路,又继续暴露未落地 API,后续需要尽快清理边界。 + +--- + +## P3:低优先级,但建议纳入后续治理 + +### P3-1:README、配置、代码对“支持的能力”描述不完全同步 + +表现包括: + +1. provider 列表不一致 +2. Azure 有的地方允许、有的地方直接禁用 +3. plugin / MCP / cron 的对外心智模型比真实实现更完整 + +建议: + +1. 建立“文档只写已实测可用能力”的约束。 +2. 对实验性能力单独加 `experimental` 标记。 + +### P3-2:部分 API 仍然带有“演示性质”的元数据或注释 + +例如: + +1. `setup.py` 中的 placeholder email / repo URL +2. 部分模块注释宣称“top standards”,但实现中仍保留 placeholder / deprecated / TODO 风格接口 + +这不会直接造成运行错误,但会削弱代码库的可信度。 + +--- + +## 6. 迭代建议(按收益排序) + +### Iteration 1:先修“外部承诺层” + +目标:让仓库的对外接口与实际能力一致。 + +1. 修复 `pyproject.toml` / `setup.py` / `requirements` 三件套。 +2. 清理悬空 entry point。 +3. 收敛 provider 支持矩阵。 +4. 明确 README 中哪些能力是 stable,哪些是 experimental。 + +### Iteration 2:修复扩展面真实可用性 + +目标:让 MCP / plugin / cron 至少有一条稳定路径。 + +1. 打通一个最小可工作的 `MCP STDIO` 主链路。 +2. 修复 `LocalPluginLoader`,支持真正加载本地插件实例。 +3. 把 cron 收敛成单文件、单 schema、单工具集。 + +### Iteration 3:收敛配置与上下文传播 + +目标:让运行行为真正由统一配置驱动。 + +1. `Config` 成为 provider / model / permission / working_dir 的唯一事实源。 +2. `/model`、`main.py`、`APIClient`、`QueryEngine` 统一走同一条配置解析链。 +3. 修复 `working_dir`、`CLAUDE.md`、sub-agent cwd 的传播。 + +### Iteration 4:降低重构风险 + +目标:为中期持续演进打基础。 + +1. 收缩 `except Exception` +2. 提升公共接口的类型标注 +3. 拆分 `services/mcp/client.py`、`engine/query.py`、`api/client.py`、`config.py` +4. 补齐扩展面契约测试 + +--- + +## 7. 推荐的下一轮任务拆分 + +建议把下一轮开发拆成以下 6 个独立工作包: + +1. `packaging-hardening` + - 修 `pyproject.toml` + - 修 `setup.py` + - 建安装/构建 smoke test +2. `provider-contract-unification` + - 收敛 provider 列表 + - 统一 config -> runtime 映射 +3. `mcp-minimal-viable-client` + - 只打通 STDIO + - 先不承诺 HTTP/SSE/WebSocket +4. `plugin-loader-fix` + - 本地插件入口约定 + - loader / manager / tests +5. `cron-consolidation` + - 合并 `scheduled_tasks.json` 与 `schedules.json` +6. `execution-context-hardening` + - working_dir + - CLAUDE.md 祖先加载 + - agent 上下文继承 + +--- + +## 8. 最终判断 + +如果把本仓库定位为“单机 CLI 的持续演进版原型”,当前状态是可接受的;如果把它定位为“可稳定扩展、可可靠分发、可承载多 provider / MCP / plugin / task platform 的 Python 工程”,那么当前最主要的问题已经不是功能缺失,而是: + +1. **契约不一致** +2. **扩展面未闭环** +3. **发布面未达标** +4. **复杂度开始累积** + +因此,下一阶段最合理的策略不是继续横向堆新功能,而是先把: + +- 打包发布 +- MCP / plugin / cron +- 配置到运行时映射 +- 工作目录与上下文传播 + +这四条线收敛干净。否则后续每加一个新能力,都会把“局部可跑”继续放大成“整体难以维护”。 + +--- + +## 9. 附:本次评估的关键事实 + +1. 语法检查:通过(`208` 文件,`0` 错误) +2. 全量测试:通过(`90 passed, 3 warnings`) +3. `pyproject.toml`:当前不可被 `tomllib` 解析 +4. `Config(api_provider="ollama")`:当前直接失败 +5. `setup.py` + `requirements.txt`:当前不满足稳定打包要求 +6. `ClaudeMdLoader` 父目录 `CLAUDE.md`:当前已复现丢失 + diff --git a/pyproject.toml b/pyproject.toml index c20ba0b..26334a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,39 +1,43 @@ -{ - "name": "claude-code-python", - "version": "1.0.0", - "description": "AI Programming Assistant CLI - Python implementation", - "requires-python": ">=3.10", - "dependencies": { - "anthropic": ">=0.40.0", - "httpx": ">=0.28.0", - "rich": ">=13.9.0", - "readline": "*", - "click": ">=8.1.0", - "pydantic": ">=2.9.0", - "python-dotenv": ">=1.0.0", - "rich": ">=13.0.0", - "tenacity": ">=8.3.0", - "aiofiles": ">=24.0.0", - "pathspec": ">=2.6.0", - "watchdog": ">=4.0.0", - "pyyaml": ">=6.0.0", - "keyring": ">=25.0.0" - }, - "dev_dependencies": { - "pytest": ">=8.0.0", - "pytest-asyncio": ">=0.24.0", - "ruff": ">=0.6.0", - "mypy": ">=1.13.0" - }, - "scripts": { - "dev": "python -m claude_code.main", - "test": "pytest", - "lint": "ruff check .", - "format": "ruff format ." - }, - "entry_points": { - "console_scripts": [ - "claude=claude_code.main:main" - ] - } -} +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "claude-code-python" +version = "1.0.0" +description = "AI Programming Assistant CLI - Python implementation" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "anthropic>=0.40.0", + "httpx>=0.28.0", + "rich>=13.9.0", + "click>=8.1.0", + "pydantic>=2.9.0", + "python-dotenv>=1.0.0", + "tenacity>=8.3.0", + "aiofiles>=24.0.0", + "pathspec>=2.6.0", + "watchdog>=4.0.0", + "pyyaml>=6.0.0", + "keyring>=25.0.0", +] + +[project.optional-dependencies] +providers = [ + "boto3>=1.35.0", + "google-auth>=2.35.0", + "openai>=1.50.0", +] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.24.0", + "ruff>=0.6.0", + "mypy>=1.13.0", +] + +[project.scripts] +claude = "claude_code.main:main" + +[tool.setuptools.packages.find] +exclude = ["tests", "examples"] diff --git a/pytest.ini b/pytest.ini index 923a29b..c34d374 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,3 +7,4 @@ asyncio_mode = auto addopts = -v --tb=short markers = perf: performance smoke tests and micro-benchmark guardrails + phase0_gate: phase 0 runtime regression gate contract and curated entry checks diff --git a/scripts/run_linux_unified_gate.py b/scripts/run_linux_unified_gate.py new file mode 100644 index 0000000..67605e4 --- /dev/null +++ b/scripts/run_linux_unified_gate.py @@ -0,0 +1,195 @@ +"""Linux-stage unified verification gate for Phase 0 -> Phase 2 cards. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +LINUX_UNIFIED_TEST_FILES: tuple[str, ...] = ( + # Phase 0 + "tests/test_runtime_bootstrap.py", + "tests/test_main_runtime.py", + "tests/test_resume_runtime.py", + "tests/test_commands_auth_runtime.py", + "tests/test_features_runtime.py", + "tests/test_review_command_runtime.py", + "tests/test_session_history_memory_runtime.py", + "tests/test_context_builder_runtime.py", + "tests/test_hooks_runtime.py", + "tests/test_doctor_runtime.py", + "tests/test_plugins_runtime.py", + "tests/test_phase0_regression_contract.py", + "tests/test_phase0_gate_runtime.py", + # Phase 1 + "tests/test_server_runtime.py", + "tests/test_event_journal_runtime.py", + "tests/test_tasks_backend_contract.py", + "tests/test_tasks_factory_runtime.py", + "tests/test_tasks_manager_runtime.py", + "tests/test_tasks_middleware_runtime.py", + "tests/test_sqlite_runtime_state_backend.py", + "tests/test_memory_scope_runtime.py", + "tests/test_active_memory_runtime.py", + "tests/test_permissions_unified.py", + "tests/test_query_engine_runtime.py", + "tests/test_services_mcp_runtime.py", + "tests/test_hook_permission_audit_runtime.py", + "tests/test_cli_daemon_thin_client_runtime.py", + "tests/test_p1_control_plane_gate_runtime.py", + "tests/test_p1_event_journal_gate_runtime.py", + "tests/test_p1_sqlite_state_backend_gate_runtime.py", + "tests/test_p1_active_memory_gate_runtime.py", + "tests/test_p1_hook_permission_audit_gate_runtime.py", + "tests/test_p1_cli_thin_client_gate_runtime.py", + # Phase 2 + "tests/test_multi_agent_supervisor_runtime.py", + "tests/test_artifact_bus_runtime.py", + "tests/test_ide_integration_runtime.py", + "tests/test_github_ci_workflow_runtime.py", + "tests/test_org_policy_audit_runtime.py", + "tests/test_tools_registry_runtime.py", + "tests/test_agent_background_context_runtime.py", + "tests/test_custom_agents_loader_runtime.py", + "tests/test_p2_jetbrains_ide_integration_runtime.py", + "tests/test_p2_linux_unified_execution_gate_runtime.py", + "tests/test_p2_linux_shard_aggregation_gate_runtime.py", + "tests/test_p2_linux_report_publish_gate_runtime.py", + "tests/test_p2_linux_unified_pipeline_gate_runtime.py", + "tests/test_p2_linux_shard_plan_gate_runtime.py", + "tests/test_p2_linux_ci_matrix_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_plan_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_sync_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_governance_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_decision_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py", + # gate contract coverage closure + "tests/test_linux_unified_gate_runtime.py", + "tests/test_p2_linux_gate_manifest_drift_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py", + "tests/test_p2_lv_dispatch_rt.py", + "tests/test_p2_lv_verdict_rt.py", + "tests/test_p2_lv_verdict_publish_rt.py", + "tests/test_p2_lv_terminal_publish_rt.py", + "tests/test_p2_lv_final_verdict_rt.py", + "tests/test_p2_lv_final_verdict_publish_rt.py", + "tests/test_p2_lv_final_publish_archive_rt.py", + "tests/test_p2_lv_terminal_verdict_rt.py", + "tests/test_p2_lv_terminal_verdict_publish_rt.py", + "tests/test_p2_lv_td_dispatch_rt.py", + "tests/test_p2_lv_td_execution_rt.py", + "tests/test_p2_lv_td_trace_rt.py", + "tests/test_p2_lv_td_completion_rt.py", + "tests/test_p2_lv_td_terminal_publish_rt.py", + "tests/test_p2_lv_td_final_verdict_rt.py", + "tests/test_p2_lv_td_final_verdict_publish_rt.py", + "tests/test_p2_lv_td_final_publish_archive_rt.py", + "tests/test_p2_lv_td_terminal_verdict_rt.py", + "tests/test_p2_lv_td_tverdict_publish_rt.py", + "tests/test_p2_lv_td_tverdict_publish_archive_rt.py", + "tests/test_p2_lv_td_tverdict_closure_rt.py", + "tests/test_p2_lv_td_tverdict_closure_publish_rt.py", + "tests/test_p2_lv_td_tverdict_closure_publish_archive_rt.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py", + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_runtime.py", + "tests/test_p2_multi_agent_supervisor_gate_runtime.py", + "tests/test_p2_artifact_bus_gate_runtime.py", + "tests/test_p2_ide_integration_gate_runtime.py", + "tests/test_p2_github_ci_workflow_gate_runtime.py", + "tests/test_p2_org_policy_audit_gate_runtime.py", + "tests/test_p2_agent_runtime_parity_gate_runtime.py", + "tests/test_p2_custom_agents_loader_gate_runtime.py", + "tests/test_p2_jetbrains_ide_integration_gate_runtime.py", +) + + +def build_gate_commands(project_root: Path) -> list[str]: + _ = project_root + return [ + f"python -m pytest -q -c pytest.ini {test_file}" + for test_file in LINUX_UNIFIED_TEST_FILES + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("Phase 0-2 Linux unified gate commands (execution in Linux stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p1_active_memory_gate.py b/scripts/run_p1_active_memory_gate.py new file mode 100644 index 0000000..779d14b --- /dev/null +++ b/scripts/run_p1_active_memory_gate.py @@ -0,0 +1,32 @@ +"""Phase 1 card P1-04 gate runner for active memory runtime integration. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_memory_scope_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_active_memory_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_context_builder_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_session_history_memory_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P1-04 active memory gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p1_cli_thin_client_gate.py b/scripts/run_p1_cli_thin_client_gate.py new file mode 100644 index 0000000..acb6e9f --- /dev/null +++ b/scripts/run_p1_cli_thin_client_gate.py @@ -0,0 +1,31 @@ +"""Phase 1 card P1-06 gate runner for CLI thin-client migration. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_cli_daemon_thin_client_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P1-06 CLI thin-client gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p1_control_plane_gate.py b/scripts/run_p1_control_plane_gate.py new file mode 100644 index 0000000..5e44560 --- /dev/null +++ b/scripts/run_p1_control_plane_gate.py @@ -0,0 +1,103 @@ +"""Phase 1 control-plane regression gate entrypoint. + +Runs the curated P1-01 daemon/API runtime tests with one command. +""" + +from __future__ import annotations + +import argparse +import shlex +import subprocess +import sys +from pathlib import Path +from typing import Sequence + + +P1_CONTROL_PLANE_TEST_FILES: tuple[str, ...] = ( + "tests/test_main_runtime.py", + "tests/test_server_runtime.py", +) + + +def build_p1_control_plane_pytest_command( + *, + python_executable: str | None = None, + extra_pytest_args: Sequence[str] | None = None, +) -> list[str]: + python_bin = python_executable or sys.executable + command: list[str] = [ + python_bin, + "-m", + "pytest", + "-c", + "pytest.ini", + *P1_CONTROL_PLANE_TEST_FILES, + ] + if extra_pytest_args: + command.extend(extra_pytest_args) + return command + + +def _validate_manifest() -> list[str]: + missing: list[str] = [] + for test_file in P1_CONTROL_PLANE_TEST_FILES: + if not Path(test_file).is_file(): + missing.append(test_file) + return missing + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run Phase 1 control-plane gate tests") + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use (default: current interpreter)", + ) + parser.add_argument( + "--print-command", + action="store_true", + help="Print the underlying pytest command and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate manifest and show command without executing tests", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to pytest after '--'", + ) + args = parser.parse_args() + + missing = _validate_manifest() + if missing: + for path in missing: + print(f"[p1-control-plane-gate] missing test file: {path}", file=sys.stderr) + return 2 + + forwarded = list(args.pytest_args) + if forwarded and forwarded[0] == "--": + forwarded = forwarded[1:] + + command = build_p1_control_plane_pytest_command( + python_executable=args.python_executable, + extra_pytest_args=forwarded, + ) + + if args.print_command or args.dry_run: + print(_format_shell_command(command)) + if args.print_command and not args.dry_run: + return 0 + if args.dry_run: + return 0 + + return subprocess.call(command) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p1_event_journal_gate.py b/scripts/run_p1_event_journal_gate.py new file mode 100644 index 0000000..40cdf99 --- /dev/null +++ b/scripts/run_p1_event_journal_gate.py @@ -0,0 +1,103 @@ +"""Phase 1 event journal regression gate entrypoint. + +Runs the curated P1-02 event-journal runtime tests with one command. +""" + +from __future__ import annotations + +import argparse +import shlex +import subprocess +import sys +from pathlib import Path +from typing import Sequence + + +P1_EVENT_JOURNAL_TEST_FILES: tuple[str, ...] = ( + "tests/test_event_journal_runtime.py", + "tests/test_server_runtime.py", +) + + +def build_p1_event_journal_pytest_command( + *, + python_executable: str | None = None, + extra_pytest_args: Sequence[str] | None = None, +) -> list[str]: + python_bin = python_executable or sys.executable + command: list[str] = [ + python_bin, + "-m", + "pytest", + "-c", + "pytest.ini", + *P1_EVENT_JOURNAL_TEST_FILES, + ] + if extra_pytest_args: + command.extend(extra_pytest_args) + return command + + +def _validate_manifest() -> list[str]: + missing: list[str] = [] + for test_file in P1_EVENT_JOURNAL_TEST_FILES: + if not Path(test_file).is_file(): + missing.append(test_file) + return missing + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run Phase 1 event-journal gate tests") + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use (default: current interpreter)", + ) + parser.add_argument( + "--print-command", + action="store_true", + help="Print the underlying pytest command and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate manifest and show command without executing tests", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to pytest after '--'", + ) + args = parser.parse_args() + + missing = _validate_manifest() + if missing: + for path in missing: + print(f"[p1-event-journal-gate] missing test file: {path}", file=sys.stderr) + return 2 + + forwarded = list(args.pytest_args) + if forwarded and forwarded[0] == "--": + forwarded = forwarded[1:] + + command = build_p1_event_journal_pytest_command( + python_executable=args.python_executable, + extra_pytest_args=forwarded, + ) + + if args.print_command or args.dry_run: + print(_format_shell_command(command)) + if args.print_command and not args.dry_run: + return 0 + if args.dry_run: + return 0 + + return subprocess.call(command) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p1_hook_permission_audit_gate.py b/scripts/run_p1_hook_permission_audit_gate.py new file mode 100644 index 0000000..1d2fc13 --- /dev/null +++ b/scripts/run_p1_hook_permission_audit_gate.py @@ -0,0 +1,33 @@ +"""Phase 1 card P1-05 gate runner for hook/permission/audit convergence. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_permissions_unified.py", + "python -m pytest -q -c pytest.ini tests/test_hooks_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_event_journal_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_hook_permission_audit_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P1-05 hook/permission/audit gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p1_sqlite_state_backend_gate.py b/scripts/run_p1_sqlite_state_backend_gate.py new file mode 100644 index 0000000..258a06d --- /dev/null +++ b/scripts/run_p1_sqlite_state_backend_gate.py @@ -0,0 +1,34 @@ +"""Phase 1 card P1-03 gate runner for SQLite runtime state backend. + +This script only defines the Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_tasks_backend_contract.py", + "python -m pytest -q -c pytest.ini tests/test_tasks_factory_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_tasks_manager_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_event_journal_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_sqlite_runtime_state_backend.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_session_history_memory_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P1-03 SQLite runtime state backend gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_agent_runtime_parity_gate.py b/scripts/run_p2_agent_runtime_parity_gate.py new file mode 100644 index 0000000..eb43b4e --- /dev/null +++ b/scripts/run_p2_agent_runtime_parity_gate.py @@ -0,0 +1,31 @@ +"""Phase 2 card P2-07 gate runner for AgentTool runtime parity. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_agent_background_context_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_multi_agent_supervisor_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_tasks_manager_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-07 agent runtime parity gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_artifact_bus_gate.py b/scripts/run_p2_artifact_bus_gate.py new file mode 100644 index 0000000..be68f06 --- /dev/null +++ b/scripts/run_p2_artifact_bus_gate.py @@ -0,0 +1,30 @@ +"""Phase 2 card P2-02 gate runner for artifact bus and conflict convergence. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_artifact_bus_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_multi_agent_supervisor_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_tools_registry_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-02 artifact bus gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_custom_agents_loader_gate.py b/scripts/run_p2_custom_agents_loader_gate.py new file mode 100644 index 0000000..4353b74 --- /dev/null +++ b/scripts/run_p2_custom_agents_loader_gate.py @@ -0,0 +1,31 @@ +"""Phase 2 card P2-08 gate runner for custom agents loader/runtime chain. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + _ = project_root + return [ + "python -m pytest -q -c pytest.ini tests/test_custom_agents_loader_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_agent_background_context_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-08 custom agents loader gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_github_ci_workflow_gate.py b/scripts/run_p2_github_ci_workflow_gate.py new file mode 100644 index 0000000..881b47b --- /dev/null +++ b/scripts/run_p2_github_ci_workflow_gate.py @@ -0,0 +1,30 @@ +"""Phase 2 card P2-04 gate runner for GitHub/CI workflow agent. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_github_ci_workflow_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_ide_integration_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-04 GitHub/CI workflow gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_ide_integration_gate.py b/scripts/run_p2_ide_integration_gate.py new file mode 100644 index 0000000..f84f587 --- /dev/null +++ b/scripts/run_p2_ide_integration_gate.py @@ -0,0 +1,30 @@ +"""Phase 2 card P2-03 gate runner for IDE integration (VS Code first). + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_ide_integration_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_cli_daemon_thin_client_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-03 IDE integration gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_jetbrains_ide_integration_gate.py b/scripts/run_p2_jetbrains_ide_integration_gate.py new file mode 100644 index 0000000..dfc09ef --- /dev/null +++ b/scripts/run_p2_jetbrains_ide_integration_gate.py @@ -0,0 +1,31 @@ +"""Phase 2 card P2-10 gate runner for JetBrains IDE integration. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_p2_jetbrains_ide_integration_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_ide_integration_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-10 JetBrains IDE integration gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_matrix_gate.py b/scripts/run_p2_linux_ci_matrix_gate.py new file mode 100644 index 0000000..4d8ae77 --- /dev/null +++ b/scripts/run_p2_linux_ci_matrix_gate.py @@ -0,0 +1,280 @@ +"""Phase 2 card P2-16 gate for Linux CI matrix generation. + +This script converts shard plan artifacts from P2-15 into a CI-consumable +matrix payload for fan-out jobs and optional GitHub Actions outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path}: field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def load_shard_plan(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: shard plan file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: shard plan payload must be object") + + required_fields = ("manifest_total_tests", "shard_total", "shards") + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + manifest_total_tests = _coerce_int( + payload["manifest_total_tests"], + field="manifest_total_tests", + path=path, + ) + shard_total = _coerce_int(payload["shard_total"], field="shard_total", path=path) + shards = payload["shards"] + if not isinstance(shards, list): + raise ValueError(f"{path}: field 'shards' must be list") + + if manifest_total_tests < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + + normalized_shards: list[dict[str, Any]] = [] + seen_indexes: set[int] = set() + for index, shard in enumerate(shards, start=1): + if not isinstance(shard, dict): + raise ValueError(f"{path}: shards[{index - 1}] must be object") + + for required in ("shard_index", "total_tests", "report_dir", "summary_path", "command", "command_parts"): + if required not in shard: + raise ValueError(f"{path}: shards[{index - 1}] missing field '{required}'") + + shard_index = _coerce_int( + shard["shard_index"], + field=f"shards[{index - 1}].shard_index", + path=path, + ) + total_tests = _coerce_int( + shard["total_tests"], + field=f"shards[{index - 1}].total_tests", + path=path, + ) + report_dir = _coerce_str( + shard["report_dir"], + field=f"shards[{index - 1}].report_dir", + path=path, + ) + summary_path = _coerce_str( + shard["summary_path"], + field=f"shards[{index - 1}].summary_path", + path=path, + ) + command = _coerce_str( + shard["command"], + field=f"shards[{index - 1}].command", + path=path, + ) + command_parts = _coerce_str_list( + shard["command_parts"], + field=f"shards[{index - 1}].command_parts", + path=path, + ) + test_files = shard.get("test_files", []) + if not isinstance(test_files, list) or not all(isinstance(item, str) for item in test_files): + raise ValueError(f"{path}: field 'shards[{index - 1}].test_files' must be string list") + + nested_shard_total = shard.get("shard_total", shard_total) + if nested_shard_total != shard_total: + raise ValueError( + f"{path}: shards[{index - 1}].shard_total ({nested_shard_total}) " + f"!= top-level shard_total ({shard_total})" + ) + if shard_index < 1 or shard_index > shard_total: + raise ValueError(f"{path}: shards[{index - 1}].shard_index out of range") + if shard_index in seen_indexes: + raise ValueError(f"{path}: duplicate shard_index {shard_index}") + if total_tests < 0: + raise ValueError(f"{path}: shards[{index - 1}].total_tests must be >= 0") + + seen_indexes.add(shard_index) + normalized_shards.append( + { + "shard_index": shard_index, + "shard_total": shard_total, + "total_tests": total_tests, + "test_files": list(test_files), + "report_dir": report_dir, + "summary_path": summary_path, + "command": command, + "command_parts": command_parts, + } + ) + + expected_indexes = set(range(1, shard_total + 1)) + if seen_indexes != expected_indexes: + missing = sorted(expected_indexes - seen_indexes) + extra = sorted(seen_indexes - expected_indexes) + raise ValueError( + f"{path}: shard indexes mismatch (missing={missing}, unexpected={extra})" + ) + + total_tests = sum(int(item["total_tests"]) for item in normalized_shards) + if total_tests != manifest_total_tests: + raise ValueError( + f"{path}: shard total_tests sum ({total_tests}) " + f"!= manifest_total_tests ({manifest_total_tests})" + ) + + return { + "manifest_total_tests": manifest_total_tests, + "shard_total": shard_total, + "shards": sorted(normalized_shards, key=lambda item: int(item["shard_index"])), + } + + +def build_ci_matrix_payload( + shard_plan: dict[str, Any], + *, + source_path: Path, + skip_empty_shards: bool, +) -> dict[str, Any]: + include: list[dict[str, Any]] = [] + summary_paths: list[str] = [] + for shard in shard_plan["shards"]: + total_tests = int(shard["total_tests"]) + if skip_empty_shards and total_tests == 0: + continue + include_entry = { + "shard_index": int(shard["shard_index"]), + "shard_total": int(shard["shard_total"]), + "total_tests": total_tests, + "report_dir": str(shard["report_dir"]), + "summary_path": str(shard["summary_path"]), + "command": str(shard["command"]), + "command_parts": list(shard["command_parts"]), + } + include.append(include_entry) + summary_paths.append(include_entry["summary_path"]) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_shard_plan": str(source_path), + "shard_total": int(shard_plan["shard_total"]), + "manifest_total_tests": int(shard_plan["manifest_total_tests"]), + "selected_shards": len(include), + "skipped_empty_shards": bool(skip_empty_shards), + "matrix": {"include": include}, + "summary_paths": summary_paths, + } + + +def build_github_output_values(payload: dict[str, Any]) -> dict[str, str]: + return { + "matrix": json.dumps(payload["matrix"], ensure_ascii=False, separators=(",", ":")), + "summary_paths": json.dumps(payload["summary_paths"], ensure_ascii=False, separators=(",", ":")), + "selected_shards": str(payload["selected_shards"]), + "shard_total": str(payload["shard_total"]), + "manifest_total_tests": str(payload["manifest_total_tests"]), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate Linux CI matrix from P2 shard plan" + ) + parser.add_argument( + "--shard-plan", + default=".claude/reports/linux_unified_gate/shard_plan.json", + help="Shard plan JSON produced by run_p2_linux_shard_plan_gate.py", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/ci_matrix.json", + help="Output CI matrix JSON path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path (appends matrix/summary_paths fields)", + ) + parser.add_argument( + "--skip-empty-shards", + action="store_true", + help="Skip shards with zero assigned tests", + ) + parser.add_argument( + "--print-matrix", + action="store_true", + help="Print generated matrix payload JSON", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Generate and print summary without writing output file", + ) + args = parser.parse_args() + + shard_plan_path = Path(args.shard_plan) + try: + shard_plan = load_shard_plan(shard_plan_path) + payload = build_ci_matrix_payload( + shard_plan, + source_path=shard_plan_path.resolve(), + skip_empty_shards=args.skip_empty_shards, + ) + except ValueError as exc: + print(f"[p2-linux-ci-matrix-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"ci matrix: {output_path}") + + if args.github_output: + write_github_output(Path(args.github_output), build_github_output_values(payload)) + print(f"github output: {args.github_output}") + + print( + "matrix summary: " + f"{payload['selected_shards']} shards selected " + f"(manifest_total_tests={payload['manifest_total_tests']})" + ) + if args.print_matrix or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_command_guard_gate.py b/scripts/run_p2_linux_ci_workflow_command_guard_gate.py new file mode 100644 index 0000000..3904e7e --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_command_guard_gate.py @@ -0,0 +1,721 @@ +"""Phase 2 card P2-20 gate for Linux CI workflow command integrity. + +This script validates command-level integrity for the P2-17 workflow plan: +1) command string <-> command_parts consistency, +2) expected script-target and required-flag contracts, +3) fan-out/fan-in path binding correctness. + +It can optionally rewrite command strings to canonical shell-quoted form. +""" + +from __future__ import annotations + +import argparse +import copy +import json +import shlex +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path}: field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ValueError(f"{path}: field '{field}' must be non-empty string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _looks_like_python_executable(value: str) -> bool: + name = Path(value).name.lower() + return name.startswith("python") + + +def _extract_single_flag_value(parts: list[str], flag: str) -> str | None: + for index in range(len(parts) - 1): + if parts[index] == flag: + return parts[index + 1] + return None + + +def _extract_repeated_flag_values(parts: list[str], flag: str) -> list[str] | None: + values: list[str] = [] + index = 0 + while index < len(parts): + token = parts[index] + if token != flag: + index += 1 + continue + if index + 1 >= len(parts): + return None + values.append(parts[index + 1]) + index += 2 + return values + + +def _add_issue( + issues: list[dict[str, Any]], + *, + scope: str, + code: str, + message: str, + index: int | None = None, +) -> None: + issue: dict[str, Any] = { + "scope": scope, + "code": code, + "message": message, + } + if index is not None: + issue["index"] = index + issues.append(issue) + + +def load_ci_workflow_plan(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: workflow plan file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: workflow plan payload must be object") + + for field in ("shard_total", "manifest_total_tests", "selected_shards", "fan_out_matrix", "fan_in"): + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + shard_total = _coerce_int(payload["shard_total"], field="shard_total", path=path) + manifest_total_tests = _coerce_int( + payload["manifest_total_tests"], + field="manifest_total_tests", + path=path, + ) + selected_shards = _coerce_int(payload["selected_shards"], field="selected_shards", path=path) + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + if manifest_total_tests < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if selected_shards < 0: + raise ValueError(f"{path}: selected_shards must be >= 0") + + fan_out_matrix = payload["fan_out_matrix"] + if not isinstance(fan_out_matrix, dict): + raise ValueError(f"{path}: field 'fan_out_matrix' must be object") + include = fan_out_matrix.get("include") + if not isinstance(include, list): + raise ValueError(f"{path}: field 'fan_out_matrix.include' must be list") + + normalized_include: list[dict[str, Any]] = [] + include_summary_paths: list[str] = [] + seen_indexes: set[int] = set() + include_total_tests = 0 + for index, shard in enumerate(include, start=1): + if not isinstance(shard, dict): + raise ValueError(f"{path}: fan_out_matrix.include[{index - 1}] must be object") + + for required in ( + "job_id", + "shard_index", + "shard_total", + "total_tests", + "report_dir", + "summary_path", + "command", + "command_parts", + ): + if required not in shard: + raise ValueError( + f"{path}: fan_out_matrix.include[{index - 1}] missing field '{required}'" + ) + + job_id = _coerce_str( + shard["job_id"], + field=f"fan_out_matrix.include[{index - 1}].job_id", + path=path, + ) + shard_index = _coerce_int( + shard["shard_index"], + field=f"fan_out_matrix.include[{index - 1}].shard_index", + path=path, + ) + nested_total = _coerce_int( + shard["shard_total"], + field=f"fan_out_matrix.include[{index - 1}].shard_total", + path=path, + ) + total_tests = _coerce_int( + shard["total_tests"], + field=f"fan_out_matrix.include[{index - 1}].total_tests", + path=path, + ) + report_dir = _coerce_str( + shard["report_dir"], + field=f"fan_out_matrix.include[{index - 1}].report_dir", + path=path, + ) + summary_path = _coerce_str( + shard["summary_path"], + field=f"fan_out_matrix.include[{index - 1}].summary_path", + path=path, + ) + command = _coerce_str( + shard["command"], + field=f"fan_out_matrix.include[{index - 1}].command", + path=path, + ) + command_parts = _coerce_str_list( + shard["command_parts"], + field=f"fan_out_matrix.include[{index - 1}].command_parts", + path=path, + ) + + if shard_index < 1 or shard_index > shard_total: + raise ValueError(f"{path}: fan_out_matrix.include[{index - 1}].shard_index out of range") + if nested_total != shard_total: + raise ValueError( + f"{path}: fan_out_matrix.include[{index - 1}].shard_total ({nested_total}) " + f"!= top-level shard_total ({shard_total})" + ) + if total_tests < 0: + raise ValueError( + f"{path}: fan_out_matrix.include[{index - 1}].total_tests must be >= 0" + ) + if shard_index in seen_indexes: + raise ValueError(f"{path}: duplicate shard_index {shard_index} in fan_out_matrix.include") + + seen_indexes.add(shard_index) + include_total_tests += total_tests + include_summary_paths.append(summary_path) + normalized_include.append( + { + "job_id": job_id, + "shard_index": shard_index, + "shard_total": nested_total, + "total_tests": total_tests, + "report_dir": report_dir, + "summary_path": summary_path, + "command": command, + "command_parts": command_parts, + } + ) + + fan_in = payload["fan_in"] + if not isinstance(fan_in, dict): + raise ValueError(f"{path}: field 'fan_in' must be object") + for required in ( + "summary_paths", + "merged_summary_path", + "aggregation_command", + "aggregation_command_parts", + "publish_command", + "publish_command_parts", + "final_report_json", + "final_report_markdown", + ): + if required not in fan_in: + raise ValueError(f"{path}: fan_in missing field '{required}'") + + summary_paths = _coerce_str_list(fan_in["summary_paths"], field="fan_in.summary_paths", path=path) + merged_summary_path = _coerce_str( + fan_in["merged_summary_path"], + field="fan_in.merged_summary_path", + path=path, + ) + aggregation_command = _coerce_str( + fan_in["aggregation_command"], + field="fan_in.aggregation_command", + path=path, + ) + aggregation_command_parts = _coerce_str_list( + fan_in["aggregation_command_parts"], + field="fan_in.aggregation_command_parts", + path=path, + ) + publish_command = _coerce_str( + fan_in["publish_command"], + field="fan_in.publish_command", + path=path, + ) + publish_command_parts = _coerce_str_list( + fan_in["publish_command_parts"], + field="fan_in.publish_command_parts", + path=path, + ) + final_report_json = _coerce_str( + fan_in["final_report_json"], + field="fan_in.final_report_json", + path=path, + ) + final_report_markdown = _coerce_str( + fan_in["final_report_markdown"], + field="fan_in.final_report_markdown", + path=path, + ) + + if selected_shards != len(normalized_include): + raise ValueError( + f"{path}: selected_shards ({selected_shards}) " + f"!= fan_out_matrix.include count ({len(normalized_include)})" + ) + if summary_paths != include_summary_paths: + raise ValueError( + f"{path}: fan_in.summary_paths must match fan_out_matrix.include[*].summary_path order" + ) + if include_total_tests > manifest_total_tests: + raise ValueError( + f"{path}: fan_out_matrix.include total_tests ({include_total_tests}) " + f"exceeds manifest_total_tests ({manifest_total_tests})" + ) + + normalized_fan_out_jobs: list[dict[str, Any]] | None = None + if "fan_out_jobs" in payload: + fan_out_jobs = payload["fan_out_jobs"] + if not isinstance(fan_out_jobs, list): + raise ValueError(f"{path}: field 'fan_out_jobs' must be list when present") + if len(fan_out_jobs) != len(normalized_include): + raise ValueError( + f"{path}: fan_out_jobs count ({len(fan_out_jobs)}) " + f"!= fan_out_matrix.include count ({len(normalized_include)})" + ) + normalized_fan_out_jobs = [] + for index, job in enumerate(fan_out_jobs, start=1): + if not isinstance(job, dict): + raise ValueError(f"{path}: fan_out_jobs[{index - 1}] must be object") + for required in ( + "job_id", + "shard_index", + "shard_total", + "total_tests", + "report_dir", + "summary_path", + "command", + "command_parts", + ): + if required not in job: + raise ValueError(f"{path}: fan_out_jobs[{index - 1}] missing field '{required}'") + normalized_fan_out_jobs.append( + { + "job_id": _coerce_str( + job["job_id"], + field=f"fan_out_jobs[{index - 1}].job_id", + path=path, + ), + "shard_index": _coerce_int( + job["shard_index"], + field=f"fan_out_jobs[{index - 1}].shard_index", + path=path, + ), + "shard_total": _coerce_int( + job["shard_total"], + field=f"fan_out_jobs[{index - 1}].shard_total", + path=path, + ), + "total_tests": _coerce_int( + job["total_tests"], + field=f"fan_out_jobs[{index - 1}].total_tests", + path=path, + ), + "report_dir": _coerce_str( + job["report_dir"], + field=f"fan_out_jobs[{index - 1}].report_dir", + path=path, + ), + "summary_path": _coerce_str( + job["summary_path"], + field=f"fan_out_jobs[{index - 1}].summary_path", + path=path, + ), + "command": _coerce_str( + job["command"], + field=f"fan_out_jobs[{index - 1}].command", + path=path, + ), + "command_parts": _coerce_str_list( + job["command_parts"], + field=f"fan_out_jobs[{index - 1}].command_parts", + path=path, + ), + } + ) + + return { + "generated_at": payload.get("generated_at"), + "source_ci_matrix": payload.get("source_ci_matrix"), + "shard_total": shard_total, + "manifest_total_tests": manifest_total_tests, + "selected_shards": selected_shards, + "skipped_empty_shards": bool(payload.get("skipped_empty_shards", False)), + "fan_out_matrix": {"include": normalized_include}, + "fan_out_jobs": normalized_fan_out_jobs, + "fan_in": { + "summary_paths": summary_paths, + "merged_summary_path": merged_summary_path, + "aggregation_command": aggregation_command, + "aggregation_command_parts": aggregation_command_parts, + "publish_command": publish_command, + "publish_command_parts": publish_command_parts, + "final_report_json": final_report_json, + "final_report_markdown": final_report_markdown, + }, + } + + +def _validate_command_contract( + *, + scope: str, + command: str, + command_parts: list[str], + expected_script: str, + required_flags: list[str], + issues: list[dict[str, Any]], + index: int | None = None, +) -> str: + try: + parsed_parts = shlex.split(command, posix=True) + except ValueError as exc: + _add_issue( + issues, + scope=scope, + index=index, + code="invalid_shell_command", + message=f"command parsing failed: {exc}", + ) + parsed_parts = [] + + if parsed_parts != command_parts: + _add_issue( + issues, + scope=scope, + index=index, + code="command_parts_mismatch", + message="command string does not match command_parts", + ) + + canonical_command = _format_shell_command(command_parts) + if command != canonical_command: + _add_issue( + issues, + scope=scope, + index=index, + code="non_canonical_command", + message="command string is not canonical shell-quoted form", + ) + + if len(command_parts) < 2: + _add_issue( + issues, + scope=scope, + index=index, + code="malformed_command_parts", + message="command_parts must include python executable and script path", + ) + return canonical_command + + if not _looks_like_python_executable(command_parts[0]): + _add_issue( + issues, + scope=scope, + index=index, + code="unexpected_python_executable", + message=f"command_parts[0] must point to python executable, got '{command_parts[0]}'", + ) + if command_parts[1] != expected_script: + _add_issue( + issues, + scope=scope, + index=index, + code="unexpected_script_target", + message=f"expected '{expected_script}', got '{command_parts[1]}'", + ) + + for flag in required_flags: + if flag not in command_parts: + _add_issue( + issues, + scope=scope, + index=index, + code="missing_required_flag", + message=f"required flag '{flag}' is missing in command_parts", + ) + + return canonical_command + + +def guard_ci_workflow_plan_commands(plan: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + normalized_plan = copy.deepcopy(plan) + issues: list[dict[str, Any]] = [] + command_scopes: list[str] = [] + + include = plan["fan_out_matrix"]["include"] + normalized_include = normalized_plan["fan_out_matrix"]["include"] + + for index, shard in enumerate(include, start=1): + scope = "fan_out" + command_scopes.append(f"{scope}:{index}") + canonical_command = _validate_command_contract( + scope=scope, + command=shard["command"], + command_parts=list(shard["command_parts"]), + expected_script="scripts/run_p2_linux_unified_execution_gate.py", + required_flags=["--report-dir", "--shard-total", "--shard-index"], + issues=issues, + index=index, + ) + normalized_include[index - 1]["command"] = canonical_command + + expected_report_dir = str(shard["report_dir"]) + expected_shard_total = str(shard["shard_total"]) + expected_shard_index = str(shard["shard_index"]) + report_dir_value = _extract_single_flag_value(shard["command_parts"], "--report-dir") + shard_total_value = _extract_single_flag_value(shard["command_parts"], "--shard-total") + shard_index_value = _extract_single_flag_value(shard["command_parts"], "--shard-index") + + if report_dir_value is not None and report_dir_value != expected_report_dir: + _add_issue( + issues, + scope=scope, + index=index, + code="report_dir_mismatch", + message=( + f"command '--report-dir' value '{report_dir_value}' " + f"!= shard report_dir '{expected_report_dir}'" + ), + ) + if shard_total_value is not None and shard_total_value != expected_shard_total: + _add_issue( + issues, + scope=scope, + index=index, + code="shard_total_mismatch", + message=( + f"command '--shard-total' value '{shard_total_value}' " + f"!= shard_total '{expected_shard_total}'" + ), + ) + if shard_index_value is not None and shard_index_value != expected_shard_index: + _add_issue( + issues, + scope=scope, + index=index, + code="shard_index_mismatch", + message=( + f"command '--shard-index' value '{shard_index_value}' " + f"!= shard_index '{expected_shard_index}'" + ), + ) + + if normalized_plan["fan_out_jobs"] is not None: + jobs = plan["fan_out_jobs"] + normalized_jobs = normalized_plan["fan_out_jobs"] + assert jobs is not None + assert normalized_jobs is not None + for index, job in enumerate(jobs, start=1): + matrix_shard = include[index - 1] + if ( + job["job_id"] != matrix_shard["job_id"] + or job["shard_index"] != matrix_shard["shard_index"] + or job["summary_path"] != matrix_shard["summary_path"] + ): + _add_issue( + issues, + scope="fan_out_jobs", + index=index, + code="fan_out_job_matrix_mismatch", + message="fan_out_jobs entry does not match fan_out_matrix.include at same index", + ) + + canonical_command = _validate_command_contract( + scope="fan_out_jobs", + command=job["command"], + command_parts=list(job["command_parts"]), + expected_script="scripts/run_p2_linux_unified_execution_gate.py", + required_flags=["--report-dir", "--shard-total", "--shard-index"], + issues=issues, + index=index, + ) + normalized_jobs[index - 1]["command"] = canonical_command + command_scopes.append(f"fan_out_jobs:{index}") + + fan_in = plan["fan_in"] + normalized_fan_in = normalized_plan["fan_in"] + + command_scopes.append("fan_in:aggregation") + normalized_fan_in["aggregation_command"] = _validate_command_contract( + scope="fan_in_aggregation", + command=fan_in["aggregation_command"], + command_parts=list(fan_in["aggregation_command_parts"]), + expected_script="scripts/run_p2_linux_shard_aggregation_gate.py", + required_flags=["--output", "--summary"], + issues=issues, + ) + command_scopes.append("fan_in:publish") + normalized_fan_in["publish_command"] = _validate_command_contract( + scope="fan_in_publish", + command=fan_in["publish_command"], + command_parts=list(fan_in["publish_command_parts"]), + expected_script="scripts/run_p2_linux_report_publish_gate.py", + required_flags=["--merged-summary", "--output-json", "--output-markdown"], + issues=issues, + ) + + summary_values = _extract_repeated_flag_values(fan_in["aggregation_command_parts"], "--summary") + if summary_values is None: + _add_issue( + issues, + scope="fan_in_aggregation", + code="malformed_summary_flags", + message="aggregation_command_parts has '--summary' without value", + ) + elif summary_values != fan_in["summary_paths"]: + _add_issue( + issues, + scope="fan_in_aggregation", + code="summary_paths_mismatch", + message="aggregation '--summary' values must match fan_in.summary_paths order", + ) + + output_value = _extract_single_flag_value(fan_in["aggregation_command_parts"], "--output") + if output_value is not None and output_value != fan_in["merged_summary_path"]: + _add_issue( + issues, + scope="fan_in_aggregation", + code="merged_summary_mismatch", + message=( + f"aggregation '--output' value '{output_value}' " + f"!= merged_summary_path '{fan_in['merged_summary_path']}'" + ), + ) + + merged_summary_value = _extract_single_flag_value(fan_in["publish_command_parts"], "--merged-summary") + if merged_summary_value is not None and merged_summary_value != fan_in["merged_summary_path"]: + _add_issue( + issues, + scope="fan_in_publish", + code="publish_merged_summary_mismatch", + message=( + f"publish '--merged-summary' value '{merged_summary_value}' " + f"!= merged_summary_path '{fan_in['merged_summary_path']}'" + ), + ) + output_json_value = _extract_single_flag_value(fan_in["publish_command_parts"], "--output-json") + if output_json_value is not None and output_json_value != fan_in["final_report_json"]: + _add_issue( + issues, + scope="fan_in_publish", + code="publish_output_json_mismatch", + message=( + f"publish '--output-json' value '{output_json_value}' " + f"!= final_report_json '{fan_in['final_report_json']}'" + ), + ) + output_markdown_value = _extract_single_flag_value(fan_in["publish_command_parts"], "--output-markdown") + if output_markdown_value is not None and output_markdown_value != fan_in["final_report_markdown"]: + _add_issue( + issues, + scope="fan_in_publish", + code="publish_output_markdown_mismatch", + message=( + f"publish '--output-markdown' value '{output_markdown_value}' " + f"!= final_report_markdown '{fan_in['final_report_markdown']}'" + ), + ) + + failed_scopes = set() + for issue in issues: + issue_scope = str(issue["scope"]) + issue_index = issue.get("index") + failed_scopes.add((issue_scope, issue_index)) + + report = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "overall_status": "failed" if issues else "passed", + "total_commands": len(command_scopes), + "failed_commands": len(failed_scopes), + "issues": issues, + } + return normalized_plan, report + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow plan command integrity contracts" + ) + parser.add_argument( + "--ci-workflow-plan", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 CI workflow plan JSON path", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/ci_workflow_command_guard.json", + help="Output guard report JSON path", + ) + parser.add_argument( + "--write", + action="store_true", + help="Rewrite workflow plan command strings to canonical shell-quoted form", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print guard report JSON", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write guard report file", + ) + args = parser.parse_args() + + plan_path = Path(args.ci_workflow_plan) + try: + plan = load_ci_workflow_plan(plan_path) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-command-guard-gate] {exc}", file=sys.stderr) + return 2 + + normalized_plan, report = guard_ci_workflow_plan_commands(plan) + report["source_ci_workflow_plan"] = str(plan_path) + + if args.write: + plan_path.parent.mkdir(parents=True, exist_ok=True) + plan_path.write_text(json.dumps(normalized_plan, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"workflow plan normalized: {plan_path}") + + if not args.dry_run: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"command guard report: {output_path}") + + print( + "command guard summary: " + f"status={report['overall_status']} " + f"failed_commands={report['failed_commands']} " + f"total_commands={report['total_commands']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(report, indent=2, ensure_ascii=False)) + + return 1 if report["overall_status"] == "failed" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_decision_gate.py b/scripts/run_p2_linux_ci_workflow_decision_gate.py new file mode 100644 index 0000000..e77c36c --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_decision_gate.py @@ -0,0 +1,364 @@ +"""Phase 2 card P2-23 gate for Linux CI workflow execution decision. + +This script turns the P2-22 governance publish payload into a single +execution decision contract for CI: +1) executable/blocked decision, +2) policy-aware exit code (fail/skip), +3) decision report artifacts + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_bool(value: Any, *, field: str) -> bool: + if not isinstance(value, bool): + raise ValueError(f"field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"field '{field}' must be int") + return value + + +def _coerce_str_list(value: Any, *, field: str) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ValueError(f"field '{field}' must be non-empty string list") + return list(value) + + +def load_governance_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: governance publish report file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: governance publish payload must be object") + + for required in ("overall_status", "should_execute_workflow", "failed_checks", "failed_check_count"): + if required not in payload: + raise ValueError(f"{path}: missing field '{required}'") + + overall_status = payload["overall_status"] + if overall_status not in {"passed", "failed"}: + raise ValueError(f"{path}: field 'overall_status' must be 'passed' or 'failed'") + + should_execute_workflow = _coerce_bool( + payload["should_execute_workflow"], + field="should_execute_workflow", + ) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks") + failed_check_count = _coerce_int(payload["failed_check_count"], field="failed_check_count") + if failed_check_count != len(failed_checks): + raise ValueError( + f"{path}: failed_check_count ({failed_check_count}) " + f"!= len(failed_checks) ({len(failed_checks)})" + ) + + structural_issues_raw = payload.get("structural_issues", []) + if not isinstance(structural_issues_raw, list) or not all( + isinstance(item, str) and item for item in structural_issues_raw + ): + raise ValueError(f"{path}: field 'structural_issues' must be non-empty string list") + + return { + "generated_at": payload.get("generated_at"), + "source_governance_report": payload.get("source_governance_report"), + "overall_status": overall_status, + "should_execute_workflow": should_execute_workflow, + "failed_checks": failed_checks, + "failed_check_count": failed_check_count, + "structural_issues": list(structural_issues_raw), + } + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def build_execution_decision_payload( + governance_publish_report: dict[str, Any], + *, + source_path: Path, + workflow_plan_path: Path, + workflow_path: Path, + on_block_policy: str, +) -> dict[str, Any]: + structural_issues = list(governance_publish_report["structural_issues"]) + + expected_should_execute = governance_publish_report["overall_status"] == "passed" + if expected_should_execute != bool(governance_publish_report["should_execute_workflow"]): + structural_issues.append("status_contract_mismatch") + + should_execute_workflow = bool(governance_publish_report["should_execute_workflow"]) and not structural_issues + missing_artifacts: list[str] = [] + if should_execute_workflow: + if not workflow_plan_path.is_file(): + missing_artifacts.append("workflow_plan_missing") + if not workflow_path.is_file(): + missing_artifacts.append("workflow_yaml_missing") + if missing_artifacts: + structural_issues.append("missing_execution_artifacts") + should_execute_workflow = False + + structural_issues = _unique(structural_issues) + failed_checks = list(governance_publish_report["failed_checks"]) + reason_codes = _unique(failed_checks + structural_issues) + + if should_execute_workflow: + decision = "execute" + decision_status = "approved" + reason_codes = ["governance_passed"] + else: + decision = "blocked" + decision_status = "blocked" + if not reason_codes: + reason_codes = ["governance_failed"] + + exit_code = 0 if (should_execute_workflow or on_block_policy == "skip") else 1 + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_governance_publish_report": str(source_path), + "workflow_plan_path": str(workflow_plan_path), + "workflow_path": str(workflow_path), + "governance_overall_status": str(governance_publish_report["overall_status"]), + "decision": decision, + "decision_status": decision_status, + "should_execute_workflow": should_execute_workflow, + "on_block_policy": on_block_policy, + "exit_code": exit_code, + "failed_checks": failed_checks, + "failed_check_count": len(failed_checks), + "structural_issues": structural_issues, + "structural_issue_count": len(structural_issues), + "missing_artifacts": missing_artifacts, + "reason_codes": reason_codes, + "primary_reason": reason_codes[0], + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Execution Decision Report", + "", + f"- Decision: **{str(payload['decision']).upper()}**", + f"- Decision Status: `{payload['decision_status']}`", + f"- Should Execute Workflow: `{payload['should_execute_workflow']}`", + f"- Exit Code: `{payload['exit_code']}`", + f"- On Block Policy: `{payload['on_block_policy']}`", + f"- Primary Reason: `{payload['primary_reason']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Governance Publish Report: `{payload['source_governance_publish_report']}`", + f"- Workflow Plan Path: `{payload['workflow_plan_path']}`", + f"- Workflow YAML Path: `{payload['workflow_path']}`", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend( + [ + "", + "## Failed Checks", + ] + ) + failed_checks = payload["failed_checks"] + if failed_checks: + lines.extend(f"- `{item}`" for item in failed_checks) + else: + lines.append("- none") + + lines.extend( + [ + "", + "## Structural Issues", + ] + ) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend( + [ + "", + "## Missing Artifacts", + ] + ) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_execution_decision": str(payload["decision"]), + "workflow_execution_status": str(payload["decision_status"]), + "workflow_execution_should_execute": "true" if payload["should_execute_workflow"] else "false", + "workflow_execution_exit_code": str(payload["exit_code"]), + "workflow_execution_on_block_policy": str(payload["on_block_policy"]), + "workflow_execution_failed_checks": json.dumps( + payload["failed_checks"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_execution_structural_issues": json.dumps( + payload["structural_issues"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_execution_report_json": str(output_json), + "workflow_execution_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Build Linux CI workflow execution decision from governance publish payload" + ) + parser.add_argument( + "--governance-publish", + default=".claude/reports/linux_unified_gate/ci_workflow_governance_publish.json", + help="P2-22 governance publish JSON path", + ) + parser.add_argument( + "--workflow-plan", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 workflow plan JSON path", + ) + parser.add_argument( + "--workflow-path", + default=".github/workflows/linux_unified_gate.yml", + help="Rendered workflow YAML path", + ) + parser.add_argument( + "--on-block", + choices=("fail", "skip"), + default="fail", + help="Exit policy when decision is blocked", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_execution_decision.json", + help="Output decision JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_execution_decision.md", + help="Output decision markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print decision JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write output files", + ) + args = parser.parse_args() + + governance_publish_path = Path(args.governance_publish) + workflow_plan_path = Path(args.workflow_plan) + workflow_path = Path(args.workflow_path) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + governance_report = load_governance_publish_report(governance_publish_path) + payload = build_execution_decision_payload( + governance_report, + source_path=governance_publish_path.resolve(), + workflow_plan_path=workflow_plan_path, + workflow_path=workflow_path, + on_block_policy=args.on_block, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-decision-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"execution decision json: {output_json_path}") + print(f"execution decision markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "execution decision summary: " + f"decision={payload['decision']} " + f"should_execute_workflow={payload['should_execute_workflow']} " + f"exit_code={payload['exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py b/scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py new file mode 100644 index 0000000..022c807 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py @@ -0,0 +1,707 @@ +"""Phase 2 card P2-28 gate for Linux CI workflow dispatch completion await. + +This script consumes the P2-27 dispatch trace artifact and converges to a +final completion verdict: +1) validate trace contract and polling command, +2) optionally poll workflow run status until terminal/timeout, +3) emit completion JSON/Markdown + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable + + +TERMINAL_TRACE_STATUSES: set[str] = { + "blocked", + "ready_dry_run", + "run_tracking_missing", + "run_completed_success", + "run_completed_failure", + "run_poll_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_dispatch_trace_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: dispatch trace report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: dispatch trace payload must be object") + + required_fields = ( + "execution_status", + "tracking_status", + "trace_exit_code", + "should_poll_workflow_run", + "run_id", + "run_url", + "repo_owner", + "repo_name", + "workflow_path", + "workflow_ref", + "poll_command", + "poll_command_parts", + "poll_attempted", + "poll_returncode", + "poll_status", + "poll_conclusion", + "poll_url", + "poll_error_type", + "poll_error_message", + "poll_stdout_tail", + "poll_stderr_tail", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + tracking_status = _coerce_str(payload["tracking_status"], field="tracking_status", path=path) + allowed_tracking_statuses = { + "blocked", + "ready_dry_run", + "run_tracking_missing", + "run_tracking_ready", + "run_in_progress", + "run_completed_success", + "run_completed_failure", + "run_poll_failed", + } + if tracking_status not in allowed_tracking_statuses: + raise ValueError( + f"{path}: field 'tracking_status' must be one of " + f"{sorted(allowed_tracking_statuses)}" + ) + + trace_exit_code = _coerce_int(payload["trace_exit_code"], field="trace_exit_code", path=path) + if trace_exit_code < 0: + raise ValueError(f"{path}: field 'trace_exit_code' must be >= 0") + + should_poll_workflow_run = _coerce_bool( + payload["should_poll_workflow_run"], + field="should_poll_workflow_run", + path=path, + ) + + run_id_raw = payload["run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="run_id", path=path) + if run_id < 1: + raise ValueError(f"{path}: field 'run_id' must be >= 1 when present") + + run_url = _coerce_str(payload["run_url"], field="run_url", path=path) + repo_owner = _coerce_str(payload["repo_owner"], field="repo_owner", path=path) + repo_name = _coerce_str(payload["repo_name"], field="repo_name", path=path) + workflow_path = _coerce_str(payload["workflow_path"], field="workflow_path", path=path) + workflow_ref = _coerce_str(payload["workflow_ref"], field="workflow_ref", path=path) + poll_command = _coerce_str(payload["poll_command"], field="poll_command", path=path) + poll_command_parts = _coerce_str_list( + payload["poll_command_parts"], + field="poll_command_parts", + path=path, + ) + poll_attempted = _coerce_bool(payload["poll_attempted"], field="poll_attempted", path=path) + poll_returncode_raw = payload["poll_returncode"] + if poll_returncode_raw is None: + poll_returncode = None + else: + poll_returncode = _coerce_int( + poll_returncode_raw, + field="poll_returncode", + path=path, + ) + poll_status = _coerce_str(payload["poll_status"], field="poll_status", path=path) + poll_conclusion = _coerce_str(payload["poll_conclusion"], field="poll_conclusion", path=path) + poll_url = _coerce_str(payload["poll_url"], field="poll_url", path=path) + poll_error_type = _coerce_str(payload["poll_error_type"], field="poll_error_type", path=path) + poll_error_message = _coerce_str( + payload["poll_error_message"], + field="poll_error_message", + path=path, + ) + poll_stdout_tail = _coerce_str(payload["poll_stdout_tail"], field="poll_stdout_tail", path=path) + poll_stderr_tail = _coerce_str(payload["poll_stderr_tail"], field="poll_stderr_tail", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + if should_poll_workflow_run: + if run_id is None: + raise ValueError(f"{path}: should_poll_workflow_run=true requires run_id") + if not poll_command_parts: + raise ValueError(f"{path}: should_poll_workflow_run=true requires poll_command_parts") + if len(poll_command_parts) < 3 or poll_command_parts[1:3] != ["run", "view"]: + raise ValueError( + f"{path}: poll_command_parts must include ' run view ...' contract" + ) + parsed_command = shlex.split(poll_command) + if parsed_command != poll_command_parts: + raise ValueError(f"{path}: poll_command and poll_command_parts mismatch") + else: + if run_id is not None: + raise ValueError(f"{path}: should_poll_workflow_run=false requires run_id=null") + + return { + "generated_at": payload.get("generated_at"), + "execution_status": _coerce_str(payload["execution_status"], field="execution_status", path=path), + "tracking_status": tracking_status, + "trace_exit_code": trace_exit_code, + "should_poll_workflow_run": should_poll_workflow_run, + "run_id": run_id, + "run_url": run_url, + "repo_owner": repo_owner, + "repo_name": repo_name, + "workflow_path": workflow_path, + "workflow_ref": workflow_ref, + "poll_command": poll_command, + "poll_command_parts": poll_command_parts, + "poll_attempted": poll_attempted, + "poll_returncode": poll_returncode, + "poll_status": poll_status, + "poll_conclusion": poll_conclusion, + "poll_url": poll_url, + "poll_error_type": poll_error_type, + "poll_error_message": poll_error_message, + "poll_stdout_tail": poll_stdout_tail, + "poll_stderr_tail": poll_stderr_tail, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def execute_run_poll_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def build_dispatch_completion_payload( + dispatch_trace_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + poll_interval_seconds: int, + max_polls: int, + poll_timeout_seconds: int, + dry_run: bool, + allow_in_progress: bool, + poll_executor: Callable[..., dict[str, Any]] = execute_run_poll_command, + sleep_fn: Callable[[float], None] = time.sleep, +) -> dict[str, Any]: + tracking_status = str(dispatch_trace_report["tracking_status"]) + should_poll_workflow_run = bool(dispatch_trace_report["should_poll_workflow_run"]) + + completion_status = tracking_status + completion_exit_code = int(dispatch_trace_report["trace_exit_code"]) + reason_codes = list(dispatch_trace_report["reason_codes"]) + + poll_iterations = 0 + poll_attempted = False + poll_returncode = dispatch_trace_report["poll_returncode"] + poll_status = str(dispatch_trace_report["poll_status"]) + poll_conclusion = str(dispatch_trace_report["poll_conclusion"]) + poll_url = str(dispatch_trace_report["poll_url"]) + poll_error_type = str(dispatch_trace_report["poll_error_type"]) + poll_error_message = str(dispatch_trace_report["poll_error_message"]) + poll_stdout_tail = str(dispatch_trace_report["poll_stdout_tail"]) + poll_stderr_tail = str(dispatch_trace_report["poll_stderr_tail"]) + + should_await = should_poll_workflow_run and tracking_status in { + "run_tracking_ready", + "run_in_progress", + } + + if should_await and not dry_run: + for attempt in range(1, max_polls + 1): + poll_iterations = attempt + poll_attempted = True + poll_result = poll_executor( + list(dispatch_trace_report["poll_command_parts"]), + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + poll_returncode = int(poll_result["returncode"]) + poll_stdout_tail = str(poll_result["stdout_tail"]) + poll_stderr_tail = str(poll_result["stderr_tail"]) + poll_error_type = str(poll_result["error_type"]) + poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if poll_returncode != 0: + completion_status = "run_poll_failed" + completion_exit_code = 1 + if poll_error_type == "command_not_found": + reason_codes.append("run_poll_cli_unavailable") + elif poll_error_type == "timeout": + reason_codes.append("run_poll_timeout") + else: + reason_codes.append("run_poll_command_failed") + break + + if poll_payload is None: + completion_status = "run_poll_failed" + completion_exit_code = 1 + reason_codes.append("run_poll_payload_invalid") + break + + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + poll_status = poll_status_value + else: + poll_status = "" + if isinstance(poll_conclusion_value, str): + poll_conclusion = poll_conclusion_value + else: + poll_conclusion = "" + if isinstance(poll_url_value, str) and poll_url_value: + poll_url = poll_url_value + + if poll_status == "completed": + if poll_conclusion == "success": + completion_status = "run_completed_success" + completion_exit_code = 0 + reason_codes = ["run_completed_success"] + else: + completion_status = "run_completed_failure" + completion_exit_code = 1 + reason_codes.append("run_completed_with_failure") + break + + completion_status = "run_in_progress" + completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append("run_still_in_progress") + if attempt < max_polls: + sleep_fn(float(poll_interval_seconds)) + else: + completion_status = "run_await_timeout" + completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append("run_await_timeout") + + elif should_await and dry_run: + completion_status = tracking_status + completion_exit_code = int(dispatch_trace_report["trace_exit_code"]) + reason_codes.append("run_completion_poll_skipped_dry_run") + else: + if tracking_status == "run_completed_success": + completion_exit_code = 0 + elif tracking_status in {"run_completed_failure", "run_tracking_missing", "run_poll_failed"}: + completion_exit_code = 1 + elif tracking_status in {"blocked", "ready_dry_run"}: + completion_exit_code = int(dispatch_trace_report["trace_exit_code"]) + + if completion_status in TERMINAL_TRACE_STATUSES and completion_status == "run_in_progress": + # Defensive fallback; run_in_progress should not be terminal. + completion_exit_code = 0 if allow_in_progress else 1 + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_dispatch_trace_report": str(source_path), + "project_root": str(project_root), + "execution_status": str(dispatch_trace_report["execution_status"]), + "tracking_status": tracking_status, + "completion_status": completion_status, + "completion_exit_code": int(completion_exit_code), + "dry_run": dry_run, + "allow_in_progress": allow_in_progress, + "should_poll_workflow_run": should_poll_workflow_run, + "run_id": dispatch_trace_report["run_id"], + "run_url": str(dispatch_trace_report["run_url"]), + "repo_owner": str(dispatch_trace_report["repo_owner"]), + "repo_name": str(dispatch_trace_report["repo_name"]), + "workflow_path": str(dispatch_trace_report["workflow_path"]), + "workflow_ref": str(dispatch_trace_report["workflow_ref"]), + "poll_command": str(dispatch_trace_report["poll_command"]), + "poll_command_parts": list(dispatch_trace_report["poll_command_parts"]), + "poll_interval_seconds": poll_interval_seconds, + "max_polls": max_polls, + "poll_timeout_seconds": poll_timeout_seconds, + "poll_iterations": poll_iterations, + "poll_attempted": poll_attempted, + "poll_returncode": poll_returncode, + "poll_status": poll_status, + "poll_conclusion": poll_conclusion, + "poll_url": poll_url, + "poll_error_type": poll_error_type, + "poll_error_message": poll_error_message, + "poll_stdout_tail": poll_stdout_tail, + "poll_stderr_tail": poll_stderr_tail, + "trace_reason_codes": list(dispatch_trace_report["reason_codes"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(dispatch_trace_report["failed_checks"]), + "structural_issues": list(dispatch_trace_report["structural_issues"]), + "missing_artifacts": list(dispatch_trace_report["missing_artifacts"]), + "source_dispatch_execution_report": str(dispatch_trace_report["source_dispatch_execution_report"]), + "source_dispatch_report": str(dispatch_trace_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Dispatch Completion Report", + "", + f"- Completion Status: **{str(payload['completion_status']).upper()}**", + f"- Completion Exit Code: `{payload['completion_exit_code']}`", + f"- Should Poll Workflow Run: `{payload['should_poll_workflow_run']}`", + f"- Poll Attempted: `{payload['poll_attempted']}`", + f"- Poll Iterations: `{payload['poll_iterations']}`", + f"- Run ID: `{payload['run_id']}`", + f"- Run URL: `{payload['run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Dispatch Trace Report: `{payload['source_dispatch_trace_report']}`", + f"- Workflow Path: `{payload['workflow_path']}`", + f"- Workflow Ref: `{payload['workflow_ref']}`", + "", + "## Poll Result", + "", + f"- Poll Return Code: `{payload['poll_returncode']}`", + f"- Poll Status: `{payload['poll_status']}`", + f"- Poll Conclusion: `{payload['poll_conclusion']}`", + f"- Poll URL: `{payload['poll_url']}`", + f"- Poll Error Type: `{payload['poll_error_type']}`", + ] + + if payload["poll_stdout_tail"]: + lines.append("- Poll stdout:") + lines.append("```text") + lines.append(str(payload["poll_stdout_tail"])) + lines.append("```") + if payload["poll_stderr_tail"]: + lines.append("- Poll stderr:") + lines.append("```text") + lines.append(str(payload["poll_stderr_tail"])) + lines.append("```") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["run_id"] + return { + "workflow_dispatch_completion_status": str(payload["completion_status"]), + "workflow_dispatch_completion_exit_code": str(payload["completion_exit_code"]), + "workflow_dispatch_completion_should_poll": ( + "true" if payload["should_poll_workflow_run"] else "false" + ), + "workflow_dispatch_completion_poll_attempted": ( + "true" if payload["poll_attempted"] else "false" + ), + "workflow_dispatch_completion_poll_iterations": str(payload["poll_iterations"]), + "workflow_dispatch_completion_poll_status": str(payload["poll_status"]), + "workflow_dispatch_completion_poll_conclusion": str(payload["poll_conclusion"]), + "workflow_dispatch_completion_run_id": "" if run_id is None else str(run_id), + "workflow_dispatch_completion_run_url": str(payload["run_url"]), + "workflow_dispatch_completion_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_dispatch_completion_report_json": str(output_json), + "workflow_dispatch_completion_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Await Linux CI workflow dispatch completion from P2-27 dispatch trace report" + ) + parser.add_argument( + "--dispatch-trace-report", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json", + help="P2-27 dispatch trace report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root used for optional poll command execution", + ) + parser.add_argument( + "--poll-interval-seconds", + type=int, + default=30, + help="Sleep interval between polls when awaiting completion", + ) + parser.add_argument( + "--max-polls", + type=int, + default=20, + help="Maximum poll attempts when awaiting completion", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Timeout seconds for each poll command execution", + ) + parser.add_argument( + "--allow-in-progress", + action="store_true", + help="Return exit 0 when run remains in progress after max polls", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json", + help="Output completion JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md", + help="Output completion markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print completion JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_interval_seconds < 1: + print( + "[p2-linux-ci-workflow-dispatch-completion-gate] --poll-interval-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.max_polls < 1: + print( + "[p2-linux-ci-workflow-dispatch-completion-gate] --max-polls must be >= 1", + file=sys.stderr, + ) + return 2 + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-dispatch-completion-gate] --poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + dispatch_trace_report_path = Path(args.dispatch_trace_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + dispatch_trace_report = load_dispatch_trace_report(dispatch_trace_report_path) + payload = build_dispatch_completion_payload( + dispatch_trace_report, + source_path=dispatch_trace_report_path.resolve(), + project_root=project_root, + poll_interval_seconds=args.poll_interval_seconds, + max_polls=args.max_polls, + poll_timeout_seconds=args.poll_timeout_seconds, + dry_run=args.dry_run, + allow_in_progress=args.allow_in_progress, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-dispatch-completion-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"dispatch completion json: {output_json_path}") + print(f"dispatch completion markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "dispatch completion summary: " + f"completion_status={payload['completion_status']} " + f"poll_attempted={payload['poll_attempted']} " + f"completion_exit_code={payload['completion_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["completion_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py b/scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py new file mode 100644 index 0000000..dfdea6a --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py @@ -0,0 +1,549 @@ +"""Phase 2 card P2-25 gate for Linux CI workflow dispatch execution. + +This script executes (or dry-runs) the P2-24 dispatch contract: +1) validates dispatch-ready vs blocked payload contract, +2) executes canonical dispatch command when ready, +3) emits execution JSON/Markdown + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be string list") + if not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def load_dispatch_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: dispatch report file not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: dispatch report payload must be object") + + required_fields = ( + "decision", + "decision_status", + "dispatch_status", + "dispatch_mode", + "should_dispatch_workflow", + "exit_code", + "on_block_policy", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "dispatch_command", + "dispatch_command_parts", + "workflow_path", + "workflow_ref", + "workflow_plan_path", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + decision = payload["decision"] + if decision not in {"execute", "blocked"}: + raise ValueError(f"{path}: field 'decision' must be 'execute' or 'blocked'") + + decision_status = payload["decision_status"] + if decision_status not in {"approved", "blocked"}: + raise ValueError(f"{path}: field 'decision_status' must be 'approved' or 'blocked'") + + dispatch_status = payload["dispatch_status"] + if dispatch_status not in {"ready", "blocked"}: + raise ValueError(f"{path}: field 'dispatch_status' must be 'ready' or 'blocked'") + + dispatch_mode = payload["dispatch_mode"] + if dispatch_mode not in {"workflow_dispatch", "none"}: + raise ValueError(f"{path}: field 'dispatch_mode' must be 'workflow_dispatch' or 'none'") + + should_dispatch_workflow = _coerce_bool( + payload["should_dispatch_workflow"], + field="should_dispatch_workflow", + path=path, + ) + exit_code = _coerce_int(payload["exit_code"], field="exit_code", path=path) + if exit_code not in {0, 1}: + raise ValueError(f"{path}: field 'exit_code' must be 0 or 1") + + on_block_policy = payload["on_block_policy"] + if on_block_policy not in {"fail", "skip"}: + raise ValueError(f"{path}: field 'on_block_policy' must be 'fail' or 'skip'") + + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + dispatch_command = _coerce_str(payload["dispatch_command"], field="dispatch_command", path=path) + dispatch_command_parts = _coerce_str_list( + payload["dispatch_command_parts"], + field="dispatch_command_parts", + path=path, + ) + workflow_path = _coerce_str(payload["workflow_path"], field="workflow_path", path=path) + workflow_ref = _coerce_str(payload["workflow_ref"], field="workflow_ref", path=path) + workflow_plan_path = _coerce_str( + payload["workflow_plan_path"], + field="workflow_plan_path", + path=path, + ) + + if dispatch_status == "ready": + if decision != "execute" or decision_status != "approved": + raise ValueError(f"{path}: ready dispatch requires execute/approved decision") + if dispatch_mode != "workflow_dispatch": + raise ValueError(f"{path}: ready dispatch requires dispatch_mode='workflow_dispatch'") + if not should_dispatch_workflow: + raise ValueError(f"{path}: ready dispatch requires should_dispatch_workflow=true") + if not dispatch_command.strip(): + raise ValueError(f"{path}: ready dispatch requires non-empty dispatch_command") + if not dispatch_command_parts: + raise ValueError(f"{path}: ready dispatch requires non-empty dispatch_command_parts") + if len(dispatch_command_parts) < 3: + raise ValueError(f"{path}: ready dispatch command_parts must include gh workflow run") + if dispatch_command_parts[1:3] != ["workflow", "run"]: + raise ValueError(f"{path}: ready dispatch command_parts must contain 'workflow run'") + parsed_command = shlex.split(dispatch_command) + if parsed_command != dispatch_command_parts: + raise ValueError(f"{path}: dispatch_command and dispatch_command_parts mismatch") + else: + if decision != "blocked" or decision_status != "blocked": + raise ValueError(f"{path}: blocked dispatch requires blocked/blocked decision") + if dispatch_mode != "none": + raise ValueError(f"{path}: blocked dispatch requires dispatch_mode='none'") + if should_dispatch_workflow: + raise ValueError(f"{path}: blocked dispatch requires should_dispatch_workflow=false") + if dispatch_command.strip(): + raise ValueError(f"{path}: blocked dispatch requires empty dispatch_command") + if dispatch_command_parts: + raise ValueError(f"{path}: blocked dispatch requires empty dispatch_command_parts") + if on_block_policy == "fail" and exit_code != 1: + raise ValueError(f"{path}: blocked+fail policy requires exit_code=1") + if on_block_policy == "skip" and exit_code != 0: + raise ValueError(f"{path}: blocked+skip policy requires exit_code=0") + + return { + "generated_at": payload.get("generated_at"), + "source_execution_decision_report": payload.get("source_execution_decision_report"), + "source_governance_publish_report": payload.get("source_governance_publish_report"), + "decision": decision, + "decision_status": decision_status, + "dispatch_status": dispatch_status, + "dispatch_mode": dispatch_mode, + "should_dispatch_workflow": should_dispatch_workflow, + "exit_code": exit_code, + "on_block_policy": on_block_policy, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "dispatch_command": dispatch_command, + "dispatch_command_parts": dispatch_command_parts, + "workflow_path": workflow_path, + "workflow_ref": workflow_ref, + "workflow_plan_path": workflow_plan_path, + } + + +def execute_dispatch_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"dispatch command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_dispatch_execution_payload( + dispatch_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + should_dispatch = bool(dispatch_report["should_dispatch_workflow"]) + base_exit_code = int(dispatch_report["exit_code"]) + command = str(dispatch_report["dispatch_command"]) + command_parts = list(dispatch_report["dispatch_command_parts"]) + + execution_status: str + dispatch_attempted = False + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + execution_reason_codes = list(dispatch_report["reason_codes"]) + execution_error_type = "" + execution_error_message = "" + + if should_dispatch: + if dry_run: + execution_status = "ready_dry_run" + execution_exit_code = 0 + else: + dispatch_attempted = True + if command_result is None: + execution_status = "dispatch_failed" + execution_exit_code = 1 + execution_reason_codes.append("dispatch_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + execution_error_type = str(command_result.get("error_type", "")) + execution_error_message = str(command_result.get("error_message", "")) + if command_returncode == 0: + execution_status = "dispatched" + execution_exit_code = 0 + execution_reason_codes = ["dispatch_command_succeeded"] + else: + execution_status = "dispatch_failed" + execution_exit_code = 1 + if execution_error_type == "command_not_found": + execution_reason_codes.append("dispatch_cli_unavailable") + elif execution_error_type == "timeout": + execution_reason_codes.append("dispatch_timeout") + else: + execution_reason_codes.append("dispatch_command_failed") + else: + execution_status = "blocked" + execution_exit_code = base_exit_code + execution_reason_codes = list(dispatch_report["reason_codes"]) + + deduped_reason_codes: list[str] = [] + seen: set[str] = set() + for reason in execution_reason_codes: + if reason in seen: + continue + seen.add(reason) + deduped_reason_codes.append(reason) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_dispatch_report": str(source_path), + "project_root": str(project_root), + "decision": str(dispatch_report["decision"]), + "decision_status": str(dispatch_report["decision_status"]), + "dispatch_status": str(dispatch_report["dispatch_status"]), + "dispatch_mode": str(dispatch_report["dispatch_mode"]), + "should_dispatch_workflow": should_dispatch, + "dispatch_attempted": dispatch_attempted, + "dispatch_command": command, + "dispatch_command_parts": command_parts, + "dry_run": dry_run, + "execution_status": execution_status, + "execution_exit_code": int(execution_exit_code), + "command_returncode": command_returncode, + "command_stdout_tail": command_stdout_tail, + "command_stderr_tail": command_stderr_tail, + "execution_error_type": execution_error_type, + "execution_error_message": execution_error_message, + "on_block_policy": str(dispatch_report["on_block_policy"]), + "reason_codes": deduped_reason_codes, + "failed_checks": list(dispatch_report["failed_checks"]), + "structural_issues": list(dispatch_report["structural_issues"]), + "missing_artifacts": list(dispatch_report["missing_artifacts"]), + "workflow_path": str(dispatch_report["workflow_path"]), + "workflow_ref": str(dispatch_report["workflow_ref"]), + "workflow_plan_path": str(dispatch_report["workflow_plan_path"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Dispatch Execution Report", + "", + f"- Execution Status: **{str(payload['execution_status']).upper()}**", + f"- Should Dispatch Workflow: `{payload['should_dispatch_workflow']}`", + f"- Dispatch Attempted: `{payload['dispatch_attempted']}`", + f"- Dry Run: `{payload['dry_run']}`", + f"- Execution Exit Code: `{payload['execution_exit_code']}`", + f"- Command Return Code: `{payload['command_returncode']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Dispatch Report: `{payload['source_dispatch_report']}`", + f"- Project Root: `{payload['project_root']}`", + f"- Workflow Plan Path: `{payload['workflow_plan_path']}`", + f"- Workflow YAML Path: `{payload['workflow_path']}`", + "", + "## Dispatch Command", + ] + + if payload["dispatch_command"]: + lines.append(f"- `{payload['dispatch_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Command Output (tail)"]) + stdout_tail = str(payload["command_stdout_tail"]).strip() + stderr_tail = str(payload["command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + command_returncode = payload["command_returncode"] + return { + "workflow_dispatch_execution_status": str(payload["execution_status"]), + "workflow_dispatch_execution_should_dispatch": "true" + if payload["should_dispatch_workflow"] + else "false", + "workflow_dispatch_execution_attempted": "true" if payload["dispatch_attempted"] else "false", + "workflow_dispatch_execution_exit_code": str(payload["execution_exit_code"]), + "workflow_dispatch_execution_command_returncode": ( + "" if command_returncode is None else str(command_returncode) + ), + "workflow_dispatch_execution_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_dispatch_execution_report_json": str(output_json), + "workflow_dispatch_execution_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Execute Linux CI workflow dispatch contract and publish execution report" + ) + parser.add_argument( + "--dispatch-report", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch.json", + help="P2-24 dispatch report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for dispatch command execution", + ) + parser.add_argument( + "--dispatch-timeout-seconds", + type=int, + default=300, + help="Dispatch command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json", + help="Output execution JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md", + help="Output execution markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print execution JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate dispatch report and skip command execution", + ) + args = parser.parse_args() + + if args.dispatch_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-dispatch-execution-gate] " + "--dispatch-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + dispatch_report_path = Path(args.dispatch_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + dispatch_report = load_dispatch_report(dispatch_report_path) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-dispatch-execution-gate] {exc}", file=sys.stderr) + return 2 + + command_result: dict[str, Any] | None = None + if dispatch_report["should_dispatch_workflow"] and not args.dry_run: + command_result = execute_dispatch_command( + dispatch_report["dispatch_command_parts"], + cwd=project_root, + timeout_seconds=args.dispatch_timeout_seconds, + ) + + payload = build_dispatch_execution_payload( + dispatch_report, + source_path=dispatch_report_path.resolve(), + project_root=project_root.resolve(), + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"dispatch execution json: {output_json_path}") + print(f"dispatch execution markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "dispatch execution summary: " + f"execution_status={payload['execution_status']} " + f"dispatch_attempted={payload['dispatch_attempted']} " + f"execution_exit_code={payload['execution_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["execution_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_dispatch_gate.py b/scripts/run_p2_linux_ci_workflow_dispatch_gate.py new file mode 100644 index 0000000..7ea0165 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_dispatch_gate.py @@ -0,0 +1,382 @@ +"""Phase 2 card P2-24 gate for Linux CI workflow dispatch readiness. + +This script converts the P2-23 execution decision payload into a dispatch +contract that can be consumed by downstream CI jobs: +1) ready/blocked dispatch status, +2) canonical dispatch command parts, +3) report artifacts + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path}: field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ValueError(f"{path}: field '{field}' must be non-empty string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def load_execution_decision(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: execution decision file not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: execution decision payload must be object") + + required_fields = ( + "decision", + "decision_status", + "should_execute_workflow", + "on_block_policy", + "exit_code", + "workflow_path", + "workflow_plan_path", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + decision = payload["decision"] + if decision not in {"execute", "blocked"}: + raise ValueError(f"{path}: field 'decision' must be 'execute' or 'blocked'") + + decision_status = payload["decision_status"] + if decision_status not in {"approved", "blocked"}: + raise ValueError(f"{path}: field 'decision_status' must be 'approved' or 'blocked'") + + should_execute_workflow = _coerce_bool( + payload["should_execute_workflow"], + field="should_execute_workflow", + path=path, + ) + on_block_policy = payload["on_block_policy"] + if on_block_policy not in {"fail", "skip"}: + raise ValueError(f"{path}: field 'on_block_policy' must be 'fail' or 'skip'") + exit_code = _coerce_int(payload["exit_code"], field="exit_code", path=path) + if exit_code not in {0, 1}: + raise ValueError(f"{path}: field 'exit_code' must be 0 or 1") + + workflow_path = _coerce_str(payload["workflow_path"], field="workflow_path", path=path) + workflow_plan_path = _coerce_str( + payload["workflow_plan_path"], + field="workflow_plan_path", + path=path, + ) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + + structural_issues_raw = payload["structural_issues"] + if not isinstance(structural_issues_raw, list) or not all( + isinstance(item, str) and item for item in structural_issues_raw + ): + raise ValueError(f"{path}: field 'structural_issues' must be non-empty string list") + structural_issues = list(structural_issues_raw) + + missing_artifacts_raw = payload["missing_artifacts"] + if not isinstance(missing_artifacts_raw, list) or not all( + isinstance(item, str) and item for item in missing_artifacts_raw + ): + raise ValueError(f"{path}: field 'missing_artifacts' must be non-empty string list") + missing_artifacts = list(missing_artifacts_raw) + + if decision == "execute" and not should_execute_workflow: + raise ValueError(f"{path}: decision/should_execute_workflow contract mismatch") + if decision == "blocked" and should_execute_workflow: + raise ValueError(f"{path}: decision/should_execute_workflow contract mismatch") + if decision == "execute" and decision_status != "approved": + raise ValueError(f"{path}: execute decision requires decision_status='approved'") + if decision == "blocked" and decision_status != "blocked": + raise ValueError(f"{path}: blocked decision requires decision_status='blocked'") + if decision == "execute" and exit_code != 0: + raise ValueError(f"{path}: execute decision requires exit_code=0") + if decision == "blocked" and on_block_policy == "fail" and exit_code != 1: + raise ValueError(f"{path}: blocked+fail policy requires exit_code=1") + if decision == "blocked" and on_block_policy == "skip" and exit_code != 0: + raise ValueError(f"{path}: blocked+skip policy requires exit_code=0") + + return { + "generated_at": payload.get("generated_at"), + "source_governance_publish_report": payload.get("source_governance_publish_report"), + "decision": decision, + "decision_status": decision_status, + "should_execute_workflow": should_execute_workflow, + "on_block_policy": on_block_policy, + "exit_code": exit_code, + "workflow_path": workflow_path, + "workflow_plan_path": workflow_plan_path, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def build_dispatch_payload( + decision_report: dict[str, Any], + *, + source_path: Path, + workflow_ref: str, + gh_executable: str, +) -> dict[str, Any]: + should_dispatch = bool(decision_report["should_execute_workflow"]) + workflow_path = str(decision_report["workflow_path"]) + workflow_plan_path = str(decision_report["workflow_plan_path"]) + decision = str(decision_report["decision"]) + decision_status = str(decision_report["decision_status"]) + + command_parts: list[str] + command: str + dispatch_status: str + dispatch_mode: str + if should_dispatch: + command_parts = [ + gh_executable, + "workflow", + "run", + workflow_path, + "--ref", + workflow_ref, + "--raw-field", + f"source_decision_report={source_path}", + "--raw-field", + f"workflow_plan_path={workflow_plan_path}", + ] + command = _format_shell_command(command_parts) + dispatch_status = "ready" + dispatch_mode = "workflow_dispatch" + else: + command_parts = [] + command = "" + dispatch_status = "blocked" + dispatch_mode = "none" + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_execution_decision_report": str(source_path), + "source_governance_publish_report": decision_report["source_governance_publish_report"], + "workflow_path": workflow_path, + "workflow_plan_path": workflow_plan_path, + "workflow_ref": workflow_ref, + "decision": decision, + "decision_status": decision_status, + "dispatch_status": dispatch_status, + "dispatch_mode": dispatch_mode, + "should_dispatch_workflow": should_dispatch, + "exit_code": int(decision_report["exit_code"]), + "on_block_policy": str(decision_report["on_block_policy"]), + "reason_codes": list(decision_report["reason_codes"]), + "failed_checks": list(decision_report["failed_checks"]), + "structural_issues": list(decision_report["structural_issues"]), + "missing_artifacts": list(decision_report["missing_artifacts"]), + "dispatch_command": command, + "dispatch_command_parts": command_parts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Dispatch Readiness Report", + "", + f"- Dispatch Status: **{str(payload['dispatch_status']).upper()}**", + f"- Dispatch Mode: `{payload['dispatch_mode']}`", + f"- Should Dispatch Workflow: `{payload['should_dispatch_workflow']}`", + f"- Exit Code: `{payload['exit_code']}`", + f"- Workflow Ref: `{payload['workflow_ref']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Execution Decision Report: `{payload['source_execution_decision_report']}`", + f"- Workflow Plan Path: `{payload['workflow_plan_path']}`", + f"- Workflow YAML Path: `{payload['workflow_path']}`", + "", + "## Dispatch Command", + ] + + if payload["dispatch_command"]: + lines.append(f"- `{payload['dispatch_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_dispatch_status": str(payload["dispatch_status"]), + "workflow_dispatch_mode": str(payload["dispatch_mode"]), + "workflow_dispatch_should_dispatch": "true" if payload["should_dispatch_workflow"] else "false", + "workflow_dispatch_exit_code": str(payload["exit_code"]), + "workflow_dispatch_command": str(payload["dispatch_command"]), + "workflow_dispatch_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_dispatch_report_json": str(output_json), + "workflow_dispatch_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Build Linux CI workflow dispatch contract from execution decision report" + ) + parser.add_argument( + "--execution-decision", + default=".claude/reports/linux_unified_gate/ci_workflow_execution_decision.json", + help="P2-23 execution decision JSON path", + ) + parser.add_argument( + "--workflow-ref", + default="main", + help="Git ref used in workflow dispatch command", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable used in dispatch command", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch.json", + help="Output dispatch JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch.md", + help="Output dispatch markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print dispatch JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write output files", + ) + args = parser.parse_args() + + execution_decision_path = Path(args.execution_decision) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + decision_report = load_execution_decision(execution_decision_path) + payload = build_dispatch_payload( + decision_report, + source_path=execution_decision_path.resolve(), + workflow_ref=args.workflow_ref, + gh_executable=args.gh_executable, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-dispatch-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"workflow dispatch json: {output_json_path}") + print(f"workflow dispatch markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "dispatch summary: " + f"dispatch_status={payload['dispatch_status']} " + f"should_dispatch_workflow={payload['should_dispatch_workflow']} " + f"exit_code={payload['exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py b/scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py new file mode 100644 index 0000000..598a408 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py @@ -0,0 +1,657 @@ +"""Phase 2 card P2-27 gate for Linux CI workflow dispatch traceability. + +This script converts P2-25 dispatch execution artifacts into a trace contract: +1) extract workflow run reference (run_id/url) from dispatch execution output, +2) emit poll-ready command for downstream CI jobs, +3) optionally poll run status immediately and publish unified trace report. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +RUN_URL_PATTERN = re.compile( + r"https://github\.com/(?P[A-Za-z0-9_.-]+)/(?P[A-Za-z0-9_.-]+)/actions/runs/(?P\d+)" +) + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def load_dispatch_execution_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: dispatch execution report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: dispatch execution payload must be object") + + required_fields = ( + "execution_status", + "execution_exit_code", + "should_dispatch_workflow", + "dispatch_attempted", + "dispatch_command", + "dispatch_command_parts", + "command_returncode", + "command_stdout_tail", + "command_stderr_tail", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "workflow_path", + "workflow_ref", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + execution_status = payload["execution_status"] + if execution_status not in {"ready_dry_run", "blocked", "dispatched", "dispatch_failed"}: + raise ValueError( + f"{path}: field 'execution_status' must be one of " + "'ready_dry_run'|'blocked'|'dispatched'|'dispatch_failed'" + ) + + execution_exit_code = _coerce_int( + payload["execution_exit_code"], + field="execution_exit_code", + path=path, + ) + if execution_exit_code < 0: + raise ValueError(f"{path}: field 'execution_exit_code' must be >= 0") + + should_dispatch_workflow = _coerce_bool( + payload["should_dispatch_workflow"], + field="should_dispatch_workflow", + path=path, + ) + dispatch_attempted = _coerce_bool( + payload["dispatch_attempted"], + field="dispatch_attempted", + path=path, + ) + + dispatch_command = _coerce_str(payload["dispatch_command"], field="dispatch_command", path=path) + dispatch_command_parts = _coerce_str_list( + payload["dispatch_command_parts"], + field="dispatch_command_parts", + path=path, + ) + + command_returncode_raw = payload["command_returncode"] + if command_returncode_raw is not None: + command_returncode = _coerce_int( + command_returncode_raw, + field="command_returncode", + path=path, + ) + else: + command_returncode = None + + command_stdout_tail = _coerce_str( + payload["command_stdout_tail"], + field="command_stdout_tail", + path=path, + ) + command_stderr_tail = _coerce_str( + payload["command_stderr_tail"], + field="command_stderr_tail", + path=path, + ) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + workflow_path = _coerce_str(payload["workflow_path"], field="workflow_path", path=path) + workflow_ref = _coerce_str(payload["workflow_ref"], field="workflow_ref", path=path) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + if execution_status == "ready_dry_run": + if not should_dispatch_workflow or dispatch_attempted: + raise ValueError(f"{path}: ready_dry_run contract mismatch") + elif execution_status == "blocked": + if should_dispatch_workflow or dispatch_attempted: + raise ValueError(f"{path}: blocked contract mismatch") + elif execution_status == "dispatched": + if not should_dispatch_workflow or not dispatch_attempted: + raise ValueError(f"{path}: dispatched contract mismatch") + if command_returncode != 0: + raise ValueError(f"{path}: dispatched contract requires command_returncode=0") + else: + if not should_dispatch_workflow or not dispatch_attempted: + raise ValueError(f"{path}: dispatch_failed contract mismatch") + + return { + "generated_at": payload.get("generated_at"), + "execution_status": execution_status, + "execution_exit_code": execution_exit_code, + "should_dispatch_workflow": should_dispatch_workflow, + "dispatch_attempted": dispatch_attempted, + "dispatch_command": dispatch_command, + "dispatch_command_parts": dispatch_command_parts, + "command_returncode": command_returncode, + "command_stdout_tail": command_stdout_tail, + "command_stderr_tail": command_stderr_tail, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "workflow_path": workflow_path, + "workflow_ref": workflow_ref, + "source_dispatch_report": source_dispatch_report, + } + + +def extract_run_reference(*, stdout_tail: str, stderr_tail: str) -> dict[str, Any]: + merged = "\n".join(item for item in (stdout_tail, stderr_tail) if item) + match = RUN_URL_PATTERN.search(merged) + if not match: + return { + "run_url": "", + "run_id": None, + "repo_owner": "", + "repo_name": "", + } + run_url = match.group(0) + return { + "run_url": run_url, + "run_id": int(match.group("run_id")), + "repo_owner": match.group("owner"), + "repo_name": match.group("repo"), + } + + +def build_run_poll_command( + *, + gh_executable: str, + run_id: int, + repo_owner: str, + repo_name: str, +) -> list[str]: + command = [ + gh_executable, + "run", + "view", + str(run_id), + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + ] + if repo_owner and repo_name: + command.extend(["--repo", f"{repo_owner}/{repo_name}"]) + return command + + +def execute_run_poll_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def build_dispatch_trace_payload( + dispatch_execution_report: dict[str, Any], + *, + source_path: Path, + gh_executable: str, + poll_now: bool, + dry_run: bool, + project_root: Path, + poll_timeout_seconds: int, +) -> dict[str, Any]: + execution_status = str(dispatch_execution_report["execution_status"]) + run_ref = extract_run_reference( + stdout_tail=str(dispatch_execution_report["command_stdout_tail"]), + stderr_tail=str(dispatch_execution_report["command_stderr_tail"]), + ) + run_id = run_ref["run_id"] + run_url = str(run_ref["run_url"]) + repo_owner = str(run_ref["repo_owner"]) + repo_name = str(run_ref["repo_name"]) + + should_poll_workflow_run = execution_status == "dispatched" and run_id is not None + tracking_status = execution_status + trace_exit_code = int(dispatch_execution_report["execution_exit_code"]) + reason_codes = list(dispatch_execution_report["reason_codes"]) + poll_command_parts: list[str] = [] + poll_command = "" + poll_attempted = False + poll_returncode: int | None = None + poll_status = "" + poll_conclusion = "" + poll_url = run_url + poll_error_type = "" + poll_error_message = "" + poll_stdout_tail = "" + poll_stderr_tail = "" + + if execution_status == "dispatched": + if not should_poll_workflow_run: + tracking_status = "run_tracking_missing" + trace_exit_code = 1 + reason_codes.append("dispatch_run_reference_missing") + else: + poll_command_parts = build_run_poll_command( + gh_executable=gh_executable, + run_id=int(run_id), + repo_owner=repo_owner, + repo_name=repo_name, + ) + poll_command = _format_shell_command(poll_command_parts) + tracking_status = "run_tracking_ready" + + if poll_now and not dry_run: + poll_attempted = True + poll_result = execute_run_poll_command( + poll_command_parts, + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + poll_returncode = int(poll_result["returncode"]) + poll_stdout_tail = str(poll_result["stdout_tail"]) + poll_stderr_tail = str(poll_result["stderr_tail"]) + poll_error_type = str(poll_result["error_type"]) + poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if poll_returncode != 0: + tracking_status = "run_poll_failed" + trace_exit_code = 1 + if poll_error_type == "command_not_found": + reason_codes.append("run_poll_cli_unavailable") + elif poll_error_type == "timeout": + reason_codes.append("run_poll_timeout") + else: + reason_codes.append("run_poll_command_failed") + elif poll_payload is None: + tracking_status = "run_poll_failed" + trace_exit_code = 1 + reason_codes.append("run_poll_payload_invalid") + else: + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + poll_status = poll_status_value + if isinstance(poll_conclusion_value, str): + poll_conclusion = poll_conclusion_value + if isinstance(poll_url_value, str) and poll_url_value: + poll_url = poll_url_value + + if poll_status == "completed": + if poll_conclusion == "success": + tracking_status = "run_completed_success" + trace_exit_code = 0 + reason_codes = ["run_completed_success"] + else: + tracking_status = "run_completed_failure" + trace_exit_code = 1 + reason_codes.append("run_completed_with_failure") + else: + tracking_status = "run_in_progress" + trace_exit_code = 0 + reason_codes.append("run_still_in_progress") + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_dispatch_execution_report": str(source_path), + "project_root": str(project_root), + "execution_status": execution_status, + "tracking_status": tracking_status, + "trace_exit_code": int(trace_exit_code), + "poll_now": poll_now, + "dry_run": dry_run, + "should_poll_workflow_run": should_poll_workflow_run, + "run_id": run_id, + "run_url": run_url, + "repo_owner": repo_owner, + "repo_name": repo_name, + "workflow_path": str(dispatch_execution_report["workflow_path"]), + "workflow_ref": str(dispatch_execution_report["workflow_ref"]), + "poll_command": poll_command, + "poll_command_parts": poll_command_parts, + "poll_attempted": poll_attempted, + "poll_returncode": poll_returncode, + "poll_status": poll_status, + "poll_conclusion": poll_conclusion, + "poll_url": poll_url, + "poll_error_type": poll_error_type, + "poll_error_message": poll_error_message, + "poll_stdout_tail": poll_stdout_tail, + "poll_stderr_tail": poll_stderr_tail, + "dispatch_reason_codes": list(dispatch_execution_report["reason_codes"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(dispatch_execution_report["failed_checks"]), + "structural_issues": list(dispatch_execution_report["structural_issues"]), + "missing_artifacts": list(dispatch_execution_report["missing_artifacts"]), + "source_dispatch_report": str(dispatch_execution_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Dispatch Trace Report", + "", + f"- Tracking Status: **{str(payload['tracking_status']).upper()}**", + f"- Trace Exit Code: `{payload['trace_exit_code']}`", + f"- Should Poll Workflow Run: `{payload['should_poll_workflow_run']}`", + f"- Poll Attempted: `{payload['poll_attempted']}`", + f"- Run ID: `{payload['run_id']}`", + f"- Run URL: `{payload['run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Dispatch Execution Report: `{payload['source_dispatch_execution_report']}`", + f"- Workflow Path: `{payload['workflow_path']}`", + f"- Workflow Ref: `{payload['workflow_ref']}`", + "", + "## Poll Command", + ] + if payload["poll_command"]: + lines.append(f"- `{payload['poll_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Poll Result"]) + lines.append(f"- Poll Return Code: `{payload['poll_returncode']}`") + lines.append(f"- Poll Status: `{payload['poll_status']}`") + lines.append(f"- Poll Conclusion: `{payload['poll_conclusion']}`") + lines.append(f"- Poll URL: `{payload['poll_url']}`") + lines.append(f"- Poll Error Type: `{payload['poll_error_type']}`") + if payload["poll_stdout_tail"]: + lines.append("- Poll stdout:") + lines.append("```text") + lines.append(str(payload["poll_stdout_tail"])) + lines.append("```") + if payload["poll_stderr_tail"]: + lines.append("- Poll stderr:") + lines.append("```text") + lines.append(str(payload["poll_stderr_tail"])) + lines.append("```") + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["run_id"] + return { + "workflow_dispatch_trace_status": str(payload["tracking_status"]), + "workflow_dispatch_trace_should_poll": "true" if payload["should_poll_workflow_run"] else "false", + "workflow_dispatch_trace_run_id": "" if run_id is None else str(run_id), + "workflow_dispatch_trace_run_url": str(payload["run_url"]), + "workflow_dispatch_trace_poll_status": str(payload["poll_status"]), + "workflow_dispatch_trace_poll_conclusion": str(payload["poll_conclusion"]), + "workflow_dispatch_trace_exit_code": str(payload["trace_exit_code"]), + "workflow_dispatch_trace_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_dispatch_trace_report_json": str(output_json), + "workflow_dispatch_trace_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Build Linux CI workflow dispatch traceability report from dispatch execution artifact" + ) + parser.add_argument( + "--dispatch-execution-report", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json", + help="P2-25 dispatch execution report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for optional poll command execution", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable for poll command generation/execution", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Workflow run poll timeout in seconds when --poll-now is enabled", + ) + parser.add_argument( + "--poll-now", + action="store_true", + help="Execute workflow run poll command when run reference is available", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json", + help="Output dispatch trace JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md", + help="Output dispatch trace markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print trace JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-dispatch-trace-gate] --poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + dispatch_execution_report_path = Path(args.dispatch_execution_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + dispatch_execution_report = load_dispatch_execution_report(dispatch_execution_report_path) + payload = build_dispatch_trace_payload( + dispatch_execution_report, + source_path=dispatch_execution_report_path.resolve(), + gh_executable=args.gh_executable, + poll_now=args.poll_now, + dry_run=args.dry_run, + project_root=project_root, + poll_timeout_seconds=args.poll_timeout_seconds, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-dispatch-trace-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"dispatch trace json: {output_json_path}") + print(f"dispatch trace markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "dispatch trace summary: " + f"tracking_status={payload['tracking_status']} " + f"should_poll={payload['should_poll_workflow_run']} " + f"trace_exit_code={payload['trace_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["trace_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_governance_gate.py b/scripts/run_p2_linux_ci_workflow_governance_gate.py new file mode 100644 index 0000000..4c7ccb2 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_governance_gate.py @@ -0,0 +1,275 @@ +"""Phase 2 card P2-21 gate for Linux CI workflow governance convergence. + +This script aggregates the contracts from: +1) P2-19 workflow render drift sync gate, +2) P2-20 workflow command guard gate. + +It produces one governance verdict for CI, so Linux fan-out/fan-in workflow +artifacts are validated as a unified contract before execution. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _load_module(*, script_name: str, module_name: str): + script_path = Path(__file__).resolve().with_name(script_name) + spec = importlib.util.spec_from_file_location(module_name, script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _normalize_path(value: str) -> str: + return Path(value).resolve().as_posix().lower() + + +def _load_json_object(path: Path, *, label: str) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid {label} JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: {label} payload must be object") + return payload + + +def build_governance_report( + *, + ci_workflow_plan_path: Path, + workflow_path: Path, + metadata_path: Path, + workflow_name: str, + python_version: str, + artifact_prefix: str, + strict_generated_at: bool, +) -> dict[str, Any]: + sync_gate = _load_module( + script_name="run_p2_linux_ci_workflow_sync_gate.py", + module_name="p2_linux_ci_workflow_sync_gate_for_governance", + ) + command_guard_gate = _load_module( + script_name="run_p2_linux_ci_workflow_command_guard_gate.py", + module_name="p2_linux_ci_workflow_command_guard_gate_for_governance", + ) + + expected_workflow_yaml, expected_metadata = sync_gate.build_expected_artifacts( + plan_path=ci_workflow_plan_path, + workflow_path=workflow_path, + workflow_name=workflow_name, + python_version=python_version, + artifact_prefix=artifact_prefix, + ) + drift = sync_gate.check_render_drift( + expected_workflow_yaml=expected_workflow_yaml, + expected_metadata=expected_metadata, + workflow_path=workflow_path, + metadata_path=metadata_path, + strict_generated_at=strict_generated_at, + ) + + plan = command_guard_gate.load_ci_workflow_plan(ci_workflow_plan_path) + normalized_plan, command_guard = command_guard_gate.guard_ci_workflow_plan_commands(plan) + normalization_required = normalized_plan != plan + + expected_source_plan = str(ci_workflow_plan_path.resolve()) + expected_workflow_output = str(workflow_path.resolve()) + actual_source_plan: str | None = None + actual_workflow_output: str | None = None + source_match = False + workflow_output_match = False + lineage_issues: list[dict[str, str]] = [] + + if metadata_path.is_file(): + metadata_payload = _load_json_object(metadata_path, label="workflow metadata") + source_value = metadata_payload.get("source_ci_workflow_plan") + if isinstance(source_value, str) and source_value: + actual_source_plan = source_value + source_match = _normalize_path(source_value) == _normalize_path(expected_source_plan) + else: + lineage_issues.append( + { + "code": "missing_source_ci_workflow_plan", + "message": "workflow metadata missing non-empty source_ci_workflow_plan", + } + ) + + workflow_value = metadata_payload.get("workflow_output_path") + if isinstance(workflow_value, str) and workflow_value: + actual_workflow_output = workflow_value + workflow_output_match = _normalize_path(workflow_value) == _normalize_path( + expected_workflow_output + ) + else: + lineage_issues.append( + { + "code": "missing_workflow_output_path", + "message": "workflow metadata missing non-empty workflow_output_path", + } + ) + else: + lineage_issues.append( + { + "code": "missing_workflow_metadata", + "message": f"workflow metadata file not found: {metadata_path}", + } + ) + + if actual_source_plan is not None and not source_match: + lineage_issues.append( + { + "code": "source_ci_workflow_plan_mismatch", + "message": ( + f"metadata source_ci_workflow_plan '{actual_source_plan}' " + f"!= expected '{expected_source_plan}'" + ), + } + ) + + if actual_workflow_output is not None and not workflow_output_match: + lineage_issues.append( + { + "code": "workflow_output_path_mismatch", + "message": ( + f"metadata workflow_output_path '{actual_workflow_output}' " + f"!= expected '{expected_workflow_output}'" + ), + } + ) + + failed_checks: list[str] = [] + if drift["workflow_drift"] or drift["metadata_drift"]: + failed_checks.append("workflow_sync_drift") + if command_guard["overall_status"] != "passed": + failed_checks.append("command_guard_failed") + if lineage_issues: + failed_checks.append("lineage_mismatch") + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_ci_workflow_plan": str(ci_workflow_plan_path), + "workflow_path": str(workflow_path), + "metadata_path": str(metadata_path), + "strict_generated_at": strict_generated_at, + "overall_status": "failed" if failed_checks else "passed", + "failed_checks": failed_checks, + "workflow_sync": drift, + "command_guard": { + "overall_status": str(command_guard["overall_status"]), + "failed_commands": int(command_guard["failed_commands"]), + "total_commands": int(command_guard["total_commands"]), + "issue_count": len(command_guard["issues"]), + "normalization_required": normalization_required, + "issues": list(command_guard["issues"]), + }, + "lineage": { + "expected_source_ci_workflow_plan": expected_source_plan, + "actual_source_ci_workflow_plan": actual_source_plan, + "source_match": source_match, + "expected_workflow_output_path": expected_workflow_output, + "actual_workflow_output_path": actual_workflow_output, + "workflow_output_match": workflow_output_match, + "issues": lineage_issues, + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Converge Linux CI workflow governance contracts (P2-19 + P2-20)" + ) + parser.add_argument( + "--ci-workflow-plan", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 CI workflow plan JSON path", + ) + parser.add_argument( + "--workflow-path", + default=".github/workflows/linux_unified_gate.yml", + help="Rendered workflow YAML path", + ) + parser.add_argument( + "--metadata-path", + default=".claude/reports/linux_unified_gate/ci_workflow_render.json", + help="Rendered metadata JSON path", + ) + parser.add_argument( + "--workflow-name", + default="Linux Unified Gate", + help="Workflow name forwarded to render/sync contract builder", + ) + parser.add_argument( + "--python-version", + default="3.11", + help="Python version forwarded to render/sync contract builder", + ) + parser.add_argument( + "--artifact-prefix", + default="linux-unified-summary", + help="Artifact prefix forwarded to render/sync contract builder", + ) + parser.add_argument( + "--strict-generated-at", + action="store_true", + help="Treat metadata generated_at drift as governance failure", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/ci_workflow_governance.json", + help="Output governance report JSON path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print governance report JSON", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write report file", + ) + args = parser.parse_args() + + try: + report = build_governance_report( + ci_workflow_plan_path=Path(args.ci_workflow_plan), + workflow_path=Path(args.workflow_path), + metadata_path=Path(args.metadata_path), + workflow_name=args.workflow_name, + python_version=args.python_version, + artifact_prefix=args.artifact_prefix, + strict_generated_at=args.strict_generated_at, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-governance-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"governance report: {output_path}") + + print( + "governance summary: " + f"status={report['overall_status']} " + f"failed_checks={len(report['failed_checks'])} " + f"command_issues={report['command_guard']['issue_count']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(report, indent=2, ensure_ascii=False)) + + return 1 if report["overall_status"] == "failed" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_governance_publish_gate.py b/scripts/run_p2_linux_ci_workflow_governance_publish_gate.py new file mode 100644 index 0000000..1f48cbc --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_governance_publish_gate.py @@ -0,0 +1,395 @@ +"""Phase 2 card P2-22 gate for Linux CI workflow governance publishing. + +This script converts the P2-21 governance report into: +1) compact JSON payload for CI decisioning, +2) markdown report for quick human audit, +3) optional GitHub output key-values for downstream workflow jobs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"field '{field}' must be int") + return value + + +def _coerce_bool(value: Any, *, field: str) -> bool: + if not isinstance(value, bool): + raise ValueError(f"field '{field}' must be bool") + return value + + +def _coerce_str(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ValueError(f"field '{field}' must be non-empty string list") + return list(value) + + +def load_governance_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: governance report file not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: governance report payload must be object") + + required_fields = ("overall_status", "failed_checks", "workflow_sync", "command_guard", "lineage") + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + overall_status = payload["overall_status"] + if overall_status not in {"passed", "failed"}: + raise ValueError(f"{path}: field 'overall_status' must be 'passed' or 'failed'") + + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks") + + workflow_sync = payload["workflow_sync"] + if not isinstance(workflow_sync, dict): + raise ValueError(f"{path}: field 'workflow_sync' must be object") + workflow_drift = _coerce_bool( + workflow_sync.get("workflow_drift"), + field="workflow_sync.workflow_drift", + ) + metadata_drift = _coerce_bool( + workflow_sync.get("metadata_drift"), + field="workflow_sync.metadata_drift", + ) + + command_guard = payload["command_guard"] + if not isinstance(command_guard, dict): + raise ValueError(f"{path}: field 'command_guard' must be object") + command_guard_status = command_guard.get("overall_status") + if command_guard_status not in {"passed", "failed"}: + raise ValueError(f"{path}: field 'command_guard.overall_status' must be 'passed' or 'failed'") + failed_commands = _coerce_int( + command_guard.get("failed_commands"), + field="command_guard.failed_commands", + ) + total_commands = _coerce_int( + command_guard.get("total_commands"), + field="command_guard.total_commands", + ) + issue_count = _coerce_int(command_guard.get("issue_count"), field="command_guard.issue_count") + normalization_required = _coerce_bool( + command_guard.get("normalization_required"), + field="command_guard.normalization_required", + ) + if failed_commands < 0 or total_commands < 0 or issue_count < 0: + raise ValueError(f"{path}: command_guard counters must be >= 0") + if failed_commands > total_commands: + raise ValueError(f"{path}: command_guard.failed_commands exceeds total_commands") + + lineage = payload["lineage"] + if not isinstance(lineage, dict): + raise ValueError(f"{path}: field 'lineage' must be object") + source_match = _coerce_bool(lineage.get("source_match"), field="lineage.source_match") + workflow_output_match = _coerce_bool( + lineage.get("workflow_output_match"), + field="lineage.workflow_output_match", + ) + lineage_issues = lineage.get("issues") + if not isinstance(lineage_issues, list): + raise ValueError(f"{path}: field 'lineage.issues' must be list") + for index, item in enumerate(lineage_issues): + if not isinstance(item, dict): + raise ValueError(f"{path}: lineage.issues[{index}] must be object") + _coerce_str(item.get("code"), field=f"lineage.issues[{index}].code") + _coerce_str(item.get("message"), field=f"lineage.issues[{index}].message") + + return { + "generated_at": payload.get("generated_at"), + "source_ci_workflow_plan": payload.get("source_ci_workflow_plan"), + "workflow_path": payload.get("workflow_path"), + "metadata_path": payload.get("metadata_path"), + "strict_generated_at": bool(payload.get("strict_generated_at", False)), + "overall_status": overall_status, + "failed_checks": failed_checks, + "workflow_sync": { + "workflow_drift": workflow_drift, + "metadata_drift": metadata_drift, + }, + "command_guard": { + "overall_status": command_guard_status, + "failed_commands": failed_commands, + "total_commands": total_commands, + "issue_count": issue_count, + "normalization_required": normalization_required, + }, + "lineage": { + "source_match": source_match, + "workflow_output_match": workflow_output_match, + "issues": list(lineage_issues), + }, + } + + +def build_governance_publish_payload( + governance_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + failed_checks = list(governance_report["failed_checks"]) + workflow_sync = governance_report["workflow_sync"] + command_guard = governance_report["command_guard"] + lineage = governance_report["lineage"] + + structural_issues: list[str] = [] + notes: list[str] = [] + + computed_failed_checks: list[str] = [] + if workflow_sync["workflow_drift"] or workflow_sync["metadata_drift"]: + computed_failed_checks.append("workflow_sync_drift") + if command_guard["overall_status"] != "passed": + computed_failed_checks.append("command_guard_failed") + if lineage["issues"]: + computed_failed_checks.append("lineage_mismatch") + + computed_failed_checks = sorted(set(computed_failed_checks)) + reported_failed_checks = sorted(set(failed_checks)) + + if computed_failed_checks != reported_failed_checks: + structural_issues.append("failed_checks_mismatch") + notes.append( + "failed_checks mismatch between reported and recomputed contracts " + f"(reported={reported_failed_checks}, computed={computed_failed_checks})" + ) + + reported_status = str(governance_report["overall_status"]) + computed_status = "failed" if computed_failed_checks else "passed" + if reported_status != computed_status: + structural_issues.append("overall_status_mismatch") + notes.append( + "overall_status mismatch between reported and recomputed contracts " + f"(reported={reported_status}, computed={computed_status})" + ) + + final_status = "failed" if structural_issues else reported_status + should_execute_workflow = final_status == "passed" + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_governance_report": str(source_path), + "reported_status": reported_status, + "computed_status": computed_status, + "overall_status": final_status, + "should_execute_workflow": should_execute_workflow, + "failed_checks": reported_failed_checks, + "failed_check_count": len(reported_failed_checks), + "workflow_sync_summary": { + "workflow_drift": bool(workflow_sync["workflow_drift"]), + "metadata_drift": bool(workflow_sync["metadata_drift"]), + }, + "command_guard_summary": { + "overall_status": str(command_guard["overall_status"]), + "failed_commands": int(command_guard["failed_commands"]), + "total_commands": int(command_guard["total_commands"]), + "issue_count": int(command_guard["issue_count"]), + "normalization_required": bool(command_guard["normalization_required"]), + }, + "lineage_summary": { + "source_match": bool(lineage["source_match"]), + "workflow_output_match": bool(lineage["workflow_output_match"]), + "issue_count": len(lineage["issues"]), + }, + "lineage_issues": list(lineage["issues"]), + "structural_issues": structural_issues, + "notes": notes, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + status = payload["overall_status"] + lines = [ + "# Linux CI Workflow Governance Publish Report", + "", + f"- Status: **{status.upper()}**", + f"- Should Execute Workflow: `{payload['should_execute_workflow']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + f"- Source Governance Report: `{payload['source_governance_report']}`", + "", + "## Status Contract", + "", + f"- Reported Status: `{payload['reported_status']}`", + f"- Computed Status: `{payload['computed_status']}`", + f"- Failed Check Count: `{payload['failed_check_count']}`", + "", + "## Failed Checks", + ] + + failed_checks = payload["failed_checks"] + if failed_checks: + lines.extend(f"- `{item}`" for item in failed_checks) + else: + lines.append("- none") + + command_guard = payload["command_guard_summary"] + workflow_sync = payload["workflow_sync_summary"] + lineage = payload["lineage_summary"] + + lines.extend( + [ + "", + "## Contract Summaries", + "", + f"- Workflow Drift: `{workflow_sync['workflow_drift']}`", + f"- Metadata Drift: `{workflow_sync['metadata_drift']}`", + f"- Command Guard Status: `{command_guard['overall_status']}`", + f"- Command Guard Issues: `{command_guard['issue_count']}`", + f"- Command Guard Failed Commands: `{command_guard['failed_commands']}`", + f"- Command Guard Total Commands: `{command_guard['total_commands']}`", + f"- Lineage Source Match: `{lineage['source_match']}`", + f"- Lineage Workflow Output Match: `{lineage['workflow_output_match']}`", + f"- Lineage Issue Count: `{lineage['issue_count']}`", + "", + "## Structural Issues", + ] + ) + + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- {item}" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + lines.append("## Notes") + notes = payload["notes"] + if notes: + lines.extend(f"- {item}" for item in notes) + else: + lines.append("- none") + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "governance_status": str(payload["overall_status"]), + "governance_should_execute_workflow": "true" + if payload["should_execute_workflow"] + else "false", + "governance_failed_checks": json.dumps( + payload["failed_checks"], + ensure_ascii=False, + separators=(",", ":"), + ), + "governance_failed_check_count": str(payload["failed_check_count"]), + "governance_report_json": str(output_json), + "governance_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Publish Linux CI workflow governance report into CI decision payload" + ) + parser.add_argument( + "--governance-report", + default=".claude/reports/linux_unified_gate/ci_workflow_governance.json", + help="P2-21 governance report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_governance_publish.json", + help="Output publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_governance_publish.md", + help="Output publish markdown path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write output files", + ) + args = parser.parse_args() + + governance_report_path = Path(args.governance_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + governance_report = load_governance_report(governance_report_path) + payload = build_governance_publish_payload( + governance_report, + source_path=governance_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-governance-publish-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"governance publish json: {output_json_path}") + print(f"governance publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "governance publish summary: " + f"status={payload['overall_status']} " + f"should_execute_workflow={payload['should_execute_workflow']} " + f"failed_checks={payload['failed_check_count']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return 0 if payload["overall_status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py new file mode 100644 index 0000000..ce0862f --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py @@ -0,0 +1,358 @@ +"""Phase 2 card P2-101 gate for Linux CI workflow pipeline closure to P2-100. + +This script validates that the P2-100 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_100_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_100_missing": '"pipeline_closure_card_id": "P2-100"', + "pipeline_closure_target_card_id_p2_99_missing": '"pipeline_closure_target_card_id": "P2-99"', + "pipeline_closure_status_key_p2_99_missing": "pipeline_closure_to_p2_99_status", + "pipeline_closure_decision_verified_p2_99_missing": "pipeline_closure_to_p2_99_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_99_missing": "ci_workflow_pipeline_closure_p2_99.json", + "pipeline_closure_report_markdown_p2_99_missing": "ci_workflow_pipeline_closure_p2_99.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_100_payload( + *, + project_root: Path, + pipeline_closure_p2_99_gate_script: Path, + pipeline_closure_p2_99_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_99_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_99_gate_script)) + p2_100_closure_text = "" + else: + p2_100_closure_text = _read_text(pipeline_closure_p2_99_gate_script) + + if not pipeline_closure_p2_99_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_99_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_100_CLOSURE_PATTERNS.items(): + if pattern not in p2_100_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_100_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_100_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_100_verified", + "pipeline_closure_p2_100_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-101 " + f"pipeline_closure_to_p2_100_status={status} " + f"pipeline_closure_to_p2_100_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_99_gate_script": str( + pipeline_closure_p2_99_gate_script.resolve() + ), + "source_pipeline_closure_p2_99_runtime_test": str( + pipeline_closure_p2_99_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-101", + "pipeline_closure_target_card_id": "P2-100", + "pipeline_closure_to_p2_100_status": status, + "pipeline_closure_to_p2_100_decision": decision, + "pipeline_closure_to_p2_100_exit_code": exit_code, + "pipeline_closure_to_p2_100_should_proceed": should_proceed, + "pipeline_closure_to_p2_100_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_100_channel": channel, + "pipeline_closure_to_p2_100_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_100_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-100 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_100_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_100_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_100_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_100_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_100_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_100_channel']}`" + ), + ( + "- Source P2-100 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_99_gate_script']}`" + ), + ( + "- Source P2-100 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_99_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_100_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_100_status": str( + payload["pipeline_closure_to_p2_100_status"] + ), + "workflow_pipeline_closure_to_p2_100_decision": str( + payload["pipeline_closure_to_p2_100_decision"] + ), + "workflow_pipeline_closure_to_p2_100_exit_code": str( + payload["pipeline_closure_to_p2_100_exit_code"] + ), + "workflow_pipeline_closure_to_p2_100_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_100_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_100_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_100_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_100_channel": str( + payload["pipeline_closure_to_p2_100_channel"] + ), + "workflow_pipeline_closure_to_p2_100_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_100_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_100_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_100_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_100_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-100 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-99-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py", + help="P2-100 closure verifier script path (P2-99 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-99-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py", + help="P2-100 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_100.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_100.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_99_gate_script = project_root / Path( + args.pipeline_closure_p2_99_gate_script + ) + pipeline_closure_p2_99_runtime_test = project_root / Path( + args.pipeline_closure_p2_99_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_100_payload( + project_root=project_root, + pipeline_closure_p2_99_gate_script=pipeline_closure_p2_99_gate_script, + pipeline_closure_p2_99_runtime_test=pipeline_closure_p2_99_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-100-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_100_status={payload['pipeline_closure_to_p2_100_status']} " + f"pipeline_closure_to_p2_100_decision={payload['pipeline_closure_to_p2_100_decision']} " + f"pipeline_closure_to_p2_100_exit_code={payload['pipeline_closure_to_p2_100_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_100_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py new file mode 100644 index 0000000..59ba127 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py @@ -0,0 +1,360 @@ +"""Phase 2 card P2-102 gate for Linux CI workflow pipeline closure to P2-101. + +This script validates that the P2-101 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_101_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_101_missing": '"pipeline_closure_card_id": "P2-101"', + "pipeline_closure_target_card_id_p2_100_missing": '"pipeline_closure_target_card_id": "P2-100"', + "pipeline_closure_status_key_p2_100_missing": "pipeline_closure_to_p2_100_status", + "pipeline_closure_decision_verified_p2_100_missing": "pipeline_closure_to_p2_100_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_100_missing": "ci_workflow_pipeline_closure_p2_100.json", + "pipeline_closure_report_markdown_p2_100_missing": "ci_workflow_pipeline_closure_p2_100.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_101_payload( + *, + project_root: Path, + pipeline_closure_p2_100_gate_script: Path, + pipeline_closure_p2_100_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_100_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_100_gate_script)) + p2_101_closure_text = "" + else: + p2_101_closure_text = _read_text(pipeline_closure_p2_100_gate_script) + + if not pipeline_closure_p2_100_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_100_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_101_CLOSURE_PATTERNS.items(): + if pattern not in p2_101_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_101_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_101_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_101_verified", + "pipeline_closure_p2_101_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-102 " + f"pipeline_closure_to_p2_101_status={status} " + f"pipeline_closure_to_p2_101_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_100_gate_script": str( + pipeline_closure_p2_100_gate_script.resolve() + ), + "source_pipeline_closure_p2_100_runtime_test": str( + pipeline_closure_p2_100_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-102", + "pipeline_closure_target_card_id": "P2-101", + "pipeline_closure_to_p2_101_status": status, + "pipeline_closure_to_p2_101_decision": decision, + "pipeline_closure_to_p2_101_exit_code": exit_code, + "pipeline_closure_to_p2_101_should_proceed": should_proceed, + "pipeline_closure_to_p2_101_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_101_channel": channel, + "pipeline_closure_to_p2_101_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_101_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-101 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_101_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_101_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_101_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_101_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_101_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_101_channel']}`" + ), + ( + "- Source P2-101 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_100_gate_script']}`" + ), + ( + "- Source P2-101 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_100_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_101_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_101_status": str( + payload["pipeline_closure_to_p2_101_status"] + ), + "workflow_pipeline_closure_to_p2_101_decision": str( + payload["pipeline_closure_to_p2_101_decision"] + ), + "workflow_pipeline_closure_to_p2_101_exit_code": str( + payload["pipeline_closure_to_p2_101_exit_code"] + ), + "workflow_pipeline_closure_to_p2_101_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_101_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_101_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_101_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_101_channel": str( + payload["pipeline_closure_to_p2_101_channel"] + ), + "workflow_pipeline_closure_to_p2_101_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_101_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_101_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_101_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_101_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-101 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-100-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py", + help="P2-101 closure verifier script path (P2-100 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-100-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py", + help="P2-101 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_101.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_101.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_100_gate_script = project_root / Path( + args.pipeline_closure_p2_100_gate_script + ) + pipeline_closure_p2_100_runtime_test = project_root / Path( + args.pipeline_closure_p2_100_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_101_payload( + project_root=project_root, + pipeline_closure_p2_100_gate_script=pipeline_closure_p2_100_gate_script, + pipeline_closure_p2_100_runtime_test=pipeline_closure_p2_100_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-101-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_101_status={payload['pipeline_closure_to_p2_101_status']} " + f"pipeline_closure_to_p2_101_decision={payload['pipeline_closure_to_p2_101_decision']} " + f"pipeline_closure_to_p2_101_exit_code={payload['pipeline_closure_to_p2_101_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_101_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py new file mode 100644 index 0000000..f31ea21 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py @@ -0,0 +1,354 @@ +"""Phase 2 card P2-103 gate for Linux CI workflow pipeline closure to P2-102. + +This script validates that the P2-102 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_102_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_102_missing": '"pipeline_closure_card_id": "P2-102"', + "pipeline_closure_target_card_id_p2_101_missing": '"pipeline_closure_target_card_id": "P2-101"', + "pipeline_closure_status_key_p2_101_missing": "pipeline_closure_to_p2_101_status", + "pipeline_closure_decision_verified_p2_101_missing": "pipeline_closure_to_p2_101_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_101_missing": "ci_workflow_pipeline_closure_p2_101.json", + "pipeline_closure_report_markdown_p2_101_missing": "ci_workflow_pipeline_closure_p2_101.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_102_payload( + *, + project_root: Path, + pipeline_closure_p2_101_gate_script: Path, + pipeline_closure_p2_101_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_101_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_101_gate_script)) + p2_102_closure_text = "" + else: + p2_102_closure_text = _read_text(pipeline_closure_p2_101_gate_script) + + if not pipeline_closure_p2_101_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_101_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_102_CLOSURE_PATTERNS.items(): + if pattern not in p2_102_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_102_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_102_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_102_verified", + "pipeline_closure_p2_102_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-103 " + f"pipeline_closure_to_p2_102_status={status} " + f"pipeline_closure_to_p2_102_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_101_gate_script": str( + pipeline_closure_p2_101_gate_script.resolve() + ), + "source_pipeline_closure_p2_101_runtime_test": str( + pipeline_closure_p2_101_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-103", + "pipeline_closure_target_card_id": "P2-102", + "pipeline_closure_to_p2_102_status": status, + "pipeline_closure_to_p2_102_decision": decision, + "pipeline_closure_to_p2_102_exit_code": exit_code, + "pipeline_closure_to_p2_102_should_proceed": should_proceed, + "pipeline_closure_to_p2_102_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_102_channel": channel, + "pipeline_closure_to_p2_102_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_102_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-102 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_102_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_102_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_102_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_102_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_102_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_102_channel']}`" + ), + ( + "- Source P2-102 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_101_gate_script']}`" + ), + ( + "- Source P2-102 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_101_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_102_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_102_status": str( + payload["pipeline_closure_to_p2_102_status"] + ), + "workflow_pipeline_closure_to_p2_102_decision": str( + payload["pipeline_closure_to_p2_102_decision"] + ), + "workflow_pipeline_closure_to_p2_102_exit_code": str( + payload["pipeline_closure_to_p2_102_exit_code"] + ), + "workflow_pipeline_closure_to_p2_102_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_102_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_102_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_102_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_102_channel": str( + payload["pipeline_closure_to_p2_102_channel"] + ), + "workflow_pipeline_closure_to_p2_102_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_102_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_102_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_102_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_102_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-102 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-101-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py", + help="P2-102 closure verifier script path (P2-101 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-101-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py", + help="P2-102 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_102.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_102.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_101_gate_script = project_root / Path( + args.pipeline_closure_p2_101_gate_script + ) + pipeline_closure_p2_101_runtime_test = project_root / Path( + args.pipeline_closure_p2_101_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_102_payload( + project_root=project_root, + pipeline_closure_p2_101_gate_script=pipeline_closure_p2_101_gate_script, + pipeline_closure_p2_101_runtime_test=pipeline_closure_p2_101_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-102-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_102_status={payload['pipeline_closure_to_p2_102_status']} " + f"pipeline_closure_to_p2_102_decision={payload['pipeline_closure_to_p2_102_decision']} " + f"pipeline_closure_to_p2_102_exit_code={payload['pipeline_closure_to_p2_102_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_102_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py new file mode 100644 index 0000000..1abf9ef --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-104 gate for Linux CI workflow pipeline closure to P2-103. + +This script validates that the P2-103 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_103_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_103_missing": '"pipeline_closure_card_id": "P2-103"', + "pipeline_closure_target_card_id_p2_102_missing": '"pipeline_closure_target_card_id": "P2-102"', + "pipeline_closure_status_key_p2_102_missing": "pipeline_closure_to_p2_102_status", + "pipeline_closure_decision_verified_p2_102_missing": "pipeline_closure_to_p2_102_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_102_missing": "ci_workflow_pipeline_closure_p2_102.json", + "pipeline_closure_report_markdown_p2_102_missing": "ci_workflow_pipeline_closure_p2_102.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_103_payload( + *, + project_root: Path, + pipeline_closure_p2_102_gate_script: Path, + pipeline_closure_p2_102_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_102_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_102_gate_script)) + p2_103_closure_text = "" + else: + p2_103_closure_text = _read_text(pipeline_closure_p2_102_gate_script) + + if not pipeline_closure_p2_102_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_102_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_103_CLOSURE_PATTERNS.items(): + if pattern not in p2_103_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_103_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_103_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_103_verified", + "pipeline_closure_p2_103_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-103 " + f"pipeline_closure_to_p2_103_status={status} " + f"pipeline_closure_to_p2_103_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_102_gate_script": str( + pipeline_closure_p2_102_gate_script.resolve() + ), + "source_pipeline_closure_p2_102_runtime_test": str( + pipeline_closure_p2_102_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-104", + "pipeline_closure_target_card_id": "P2-103", + "pipeline_closure_to_p2_103_status": status, + "pipeline_closure_to_p2_103_decision": decision, + "pipeline_closure_to_p2_103_exit_code": exit_code, + "pipeline_closure_to_p2_103_should_proceed": should_proceed, + "pipeline_closure_to_p2_103_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_103_channel": channel, + "pipeline_closure_to_p2_103_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_103_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-103 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_103_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_103_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_103_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_103_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_103_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_103_channel']}`" + ), + ( + "- Source P2-103 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_102_gate_script']}`" + ), + ( + "- Source P2-103 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_102_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_103_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_103_status": str( + payload["pipeline_closure_to_p2_103_status"] + ), + "workflow_pipeline_closure_to_p2_103_decision": str( + payload["pipeline_closure_to_p2_103_decision"] + ), + "workflow_pipeline_closure_to_p2_103_exit_code": str( + payload["pipeline_closure_to_p2_103_exit_code"] + ), + "workflow_pipeline_closure_to_p2_103_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_103_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_103_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_103_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_103_channel": str( + payload["pipeline_closure_to_p2_103_channel"] + ), + "workflow_pipeline_closure_to_p2_103_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_103_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_103_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_103_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_103_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-103 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-102-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py", + help="P2-103 closure verifier script path (P2-102 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-102-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py", + help="P2-103 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_103.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_103.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_102_gate_script = project_root / Path( + args.pipeline_closure_p2_102_gate_script + ) + pipeline_closure_p2_102_runtime_test = project_root / Path( + args.pipeline_closure_p2_102_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_103_payload( + project_root=project_root, + pipeline_closure_p2_102_gate_script=pipeline_closure_p2_102_gate_script, + pipeline_closure_p2_102_runtime_test=pipeline_closure_p2_102_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-103-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_103_status={payload['pipeline_closure_to_p2_103_status']} " + f"pipeline_closure_to_p2_103_decision={payload['pipeline_closure_to_p2_103_decision']} " + f"pipeline_closure_to_p2_103_exit_code={payload['pipeline_closure_to_p2_103_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_103_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py new file mode 100644 index 0000000..26227cc --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-105 gate for Linux CI workflow pipeline closure to P2-104. + +This script validates that the P2-104 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_104_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_104_missing": '"pipeline_closure_card_id": "P2-104"', + "pipeline_closure_target_card_id_p2_103_missing": '"pipeline_closure_target_card_id": "P2-103"', + "pipeline_closure_status_key_p2_103_missing": "pipeline_closure_to_p2_103_status", + "pipeline_closure_decision_verified_p2_103_missing": "pipeline_closure_to_p2_103_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_103_missing": "ci_workflow_pipeline_closure_p2_103.json", + "pipeline_closure_report_markdown_p2_103_missing": "ci_workflow_pipeline_closure_p2_103.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_104_payload( + *, + project_root: Path, + pipeline_closure_p2_103_gate_script: Path, + pipeline_closure_p2_103_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_103_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_103_gate_script)) + p2_104_closure_text = "" + else: + p2_104_closure_text = _read_text(pipeline_closure_p2_103_gate_script) + + if not pipeline_closure_p2_103_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_103_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_104_CLOSURE_PATTERNS.items(): + if pattern not in p2_104_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_104_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_104_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_104_verified", + "pipeline_closure_p2_104_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-104 " + f"pipeline_closure_to_p2_104_status={status} " + f"pipeline_closure_to_p2_104_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_103_gate_script": str( + pipeline_closure_p2_103_gate_script.resolve() + ), + "source_pipeline_closure_p2_103_runtime_test": str( + pipeline_closure_p2_103_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-105", + "pipeline_closure_target_card_id": "P2-104", + "pipeline_closure_to_p2_104_status": status, + "pipeline_closure_to_p2_104_decision": decision, + "pipeline_closure_to_p2_104_exit_code": exit_code, + "pipeline_closure_to_p2_104_should_proceed": should_proceed, + "pipeline_closure_to_p2_104_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_104_channel": channel, + "pipeline_closure_to_p2_104_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_104_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-104 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_104_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_104_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_104_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_104_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_104_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_104_channel']}`" + ), + ( + "- Source P2-104 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_103_gate_script']}`" + ), + ( + "- Source P2-104 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_103_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_104_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_104_status": str( + payload["pipeline_closure_to_p2_104_status"] + ), + "workflow_pipeline_closure_to_p2_104_decision": str( + payload["pipeline_closure_to_p2_104_decision"] + ), + "workflow_pipeline_closure_to_p2_104_exit_code": str( + payload["pipeline_closure_to_p2_104_exit_code"] + ), + "workflow_pipeline_closure_to_p2_104_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_104_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_104_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_104_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_104_channel": str( + payload["pipeline_closure_to_p2_104_channel"] + ), + "workflow_pipeline_closure_to_p2_104_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_104_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_104_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_104_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_104_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-104 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-103-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py", + help="P2-104 closure verifier script path (P2-103 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-103-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py", + help="P2-104 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_104.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_104.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_103_gate_script = project_root / Path( + args.pipeline_closure_p2_103_gate_script + ) + pipeline_closure_p2_103_runtime_test = project_root / Path( + args.pipeline_closure_p2_103_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_104_payload( + project_root=project_root, + pipeline_closure_p2_103_gate_script=pipeline_closure_p2_103_gate_script, + pipeline_closure_p2_103_runtime_test=pipeline_closure_p2_103_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-104-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_104_status={payload['pipeline_closure_to_p2_104_status']} " + f"pipeline_closure_to_p2_104_decision={payload['pipeline_closure_to_p2_104_decision']} " + f"pipeline_closure_to_p2_104_exit_code={payload['pipeline_closure_to_p2_104_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_104_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py new file mode 100644 index 0000000..2297ecf --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py @@ -0,0 +1,357 @@ +"""Phase 2 card P2-106 gate for Linux CI workflow pipeline closure to P2-105. + +This script validates that the P2-105 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_105_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_105_missing": '"pipeline_closure_card_id": "P2-105"', + "pipeline_closure_target_card_id_p2_104_missing": '"pipeline_closure_target_card_id": "P2-104"', + "pipeline_closure_status_key_p2_104_missing": "pipeline_closure_to_p2_104_status", + "pipeline_closure_decision_verified_p2_104_missing": "pipeline_closure_to_p2_104_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_104_missing": "ci_workflow_pipeline_closure_p2_104.json", + "pipeline_closure_report_markdown_p2_104_missing": "ci_workflow_pipeline_closure_p2_104.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_105_payload( + *, + project_root: Path, + pipeline_closure_p2_104_gate_script: Path, + pipeline_closure_p2_104_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_104_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_104_gate_script)) + p2_105_closure_text = "" + else: + p2_105_closure_text = _read_text(pipeline_closure_p2_104_gate_script) + + if not pipeline_closure_p2_104_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_104_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_105_CLOSURE_PATTERNS.items(): + if pattern not in p2_105_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_105_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_105_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_105_verified", + "pipeline_closure_p2_105_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-105 " + f"pipeline_closure_to_p2_105_status={status} " + f"pipeline_closure_to_p2_105_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_104_gate_script": str( + pipeline_closure_p2_104_gate_script.resolve() + ), + "source_pipeline_closure_p2_104_runtime_test": str( + pipeline_closure_p2_104_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-106", + "pipeline_closure_target_card_id": "P2-105", + "pipeline_closure_to_p2_105_status": status, + "pipeline_closure_to_p2_105_decision": decision, + "pipeline_closure_to_p2_105_exit_code": exit_code, + "pipeline_closure_to_p2_105_should_proceed": should_proceed, + "pipeline_closure_to_p2_105_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_105_channel": channel, + "pipeline_closure_to_p2_105_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_105_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-105 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_105_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_105_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_105_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_105_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_105_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_105_channel']}`" + ), + ( + "- Source P2-105 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_104_gate_script']}`" + ), + ( + "- Source P2-105 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_104_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_105_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_105_status": str( + payload["pipeline_closure_to_p2_105_status"] + ), + "workflow_pipeline_closure_to_p2_105_decision": str( + payload["pipeline_closure_to_p2_105_decision"] + ), + "workflow_pipeline_closure_to_p2_105_exit_code": str( + payload["pipeline_closure_to_p2_105_exit_code"] + ), + "workflow_pipeline_closure_to_p2_105_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_105_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_105_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_105_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_105_channel": str( + payload["pipeline_closure_to_p2_105_channel"] + ), + "workflow_pipeline_closure_to_p2_105_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_105_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_105_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_105_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_105_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-105 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-104-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py", + help="P2-105 closure verifier script path (P2-104 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-104-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py", + help="P2-105 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_105.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_105.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_104_gate_script = project_root / Path( + args.pipeline_closure_p2_104_gate_script + ) + pipeline_closure_p2_104_runtime_test = project_root / Path( + args.pipeline_closure_p2_104_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_105_payload( + project_root=project_root, + pipeline_closure_p2_104_gate_script=pipeline_closure_p2_104_gate_script, + pipeline_closure_p2_104_runtime_test=pipeline_closure_p2_104_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-105-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_105_status={payload['pipeline_closure_to_p2_105_status']} " + f"pipeline_closure_to_p2_105_decision={payload['pipeline_closure_to_p2_105_decision']} " + f"pipeline_closure_to_p2_105_exit_code={payload['pipeline_closure_to_p2_105_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_105_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py new file mode 100644 index 0000000..c66ca2d --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-107 gate for Linux CI workflow pipeline closure to P2-106. + +This script validates that the P2-106 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_106_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_106_missing": '"pipeline_closure_card_id": "P2-106"', + "pipeline_closure_target_card_id_p2_105_missing": '"pipeline_closure_target_card_id": "P2-105"', + "pipeline_closure_status_key_p2_105_missing": "pipeline_closure_to_p2_105_status", + "pipeline_closure_decision_verified_p2_105_missing": "pipeline_closure_to_p2_105_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_105_missing": "ci_workflow_pipeline_closure_p2_105.json", + "pipeline_closure_report_markdown_p2_105_missing": "ci_workflow_pipeline_closure_p2_105.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_106_payload( + *, + project_root: Path, + pipeline_closure_p2_105_gate_script: Path, + pipeline_closure_p2_105_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_105_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_105_gate_script)) + p2_106_closure_text = "" + else: + p2_106_closure_text = _read_text(pipeline_closure_p2_105_gate_script) + + if not pipeline_closure_p2_105_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_105_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_106_CLOSURE_PATTERNS.items(): + if pattern not in p2_106_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_106_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_106_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_106_verified", + "pipeline_closure_p2_106_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-106 " + f"pipeline_closure_to_p2_106_status={status} " + f"pipeline_closure_to_p2_106_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_105_gate_script": str( + pipeline_closure_p2_105_gate_script.resolve() + ), + "source_pipeline_closure_p2_105_runtime_test": str( + pipeline_closure_p2_105_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-107", + "pipeline_closure_target_card_id": "P2-106", + "pipeline_closure_to_p2_106_status": status, + "pipeline_closure_to_p2_106_decision": decision, + "pipeline_closure_to_p2_106_exit_code": exit_code, + "pipeline_closure_to_p2_106_should_proceed": should_proceed, + "pipeline_closure_to_p2_106_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_106_channel": channel, + "pipeline_closure_to_p2_106_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_106_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-106 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_106_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_106_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_106_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_106_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_106_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_106_channel']}`" + ), + ( + "- Source P2-106 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_105_gate_script']}`" + ), + ( + "- Source P2-106 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_105_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_106_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_106_status": str( + payload["pipeline_closure_to_p2_106_status"] + ), + "workflow_pipeline_closure_to_p2_106_decision": str( + payload["pipeline_closure_to_p2_106_decision"] + ), + "workflow_pipeline_closure_to_p2_106_exit_code": str( + payload["pipeline_closure_to_p2_106_exit_code"] + ), + "workflow_pipeline_closure_to_p2_106_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_106_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_106_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_106_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_106_channel": str( + payload["pipeline_closure_to_p2_106_channel"] + ), + "workflow_pipeline_closure_to_p2_106_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_106_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_106_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_106_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_106_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-106 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-105-gate-script", + dest="pipeline_closure_p2_105_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py", + help="P2-106 closure verifier script path (P2-105 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-105-runtime-test", + dest="pipeline_closure_p2_105_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py", + help="P2-106 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_106.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_106.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_105_gate_script = project_root / Path( + args.pipeline_closure_p2_105_gate_script + ) + pipeline_closure_p2_105_runtime_test = project_root / Path( + args.pipeline_closure_p2_105_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_106_payload( + project_root=project_root, + pipeline_closure_p2_105_gate_script=pipeline_closure_p2_105_gate_script, + pipeline_closure_p2_105_runtime_test=pipeline_closure_p2_105_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-106-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_106_status={payload['pipeline_closure_to_p2_106_status']} " + f"pipeline_closure_to_p2_106_decision={payload['pipeline_closure_to_p2_106_decision']} " + f"pipeline_closure_to_p2_106_exit_code={payload['pipeline_closure_to_p2_106_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_106_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py new file mode 100644 index 0000000..5c81462 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-108 gate for Linux CI workflow pipeline closure to P2-107. + +This script validates that the P2-107 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_107_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_107_missing": '"pipeline_closure_card_id": "P2-107"', + "pipeline_closure_target_card_id_p2_106_missing": '"pipeline_closure_target_card_id": "P2-106"', + "pipeline_closure_status_key_p2_106_missing": "pipeline_closure_to_p2_106_status", + "pipeline_closure_decision_verified_p2_106_missing": "pipeline_closure_to_p2_106_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_106_missing": "ci_workflow_pipeline_closure_p2_106.json", + "pipeline_closure_report_markdown_p2_106_missing": "ci_workflow_pipeline_closure_p2_106.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_107_payload( + *, + project_root: Path, + pipeline_closure_p2_106_gate_script: Path, + pipeline_closure_p2_106_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_106_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_106_gate_script)) + p2_107_closure_text = "" + else: + p2_107_closure_text = _read_text(pipeline_closure_p2_106_gate_script) + + if not pipeline_closure_p2_106_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_106_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_107_CLOSURE_PATTERNS.items(): + if pattern not in p2_107_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_107_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_107_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_107_verified", + "pipeline_closure_p2_107_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-107 " + f"pipeline_closure_to_p2_107_status={status} " + f"pipeline_closure_to_p2_107_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_106_gate_script": str( + pipeline_closure_p2_106_gate_script.resolve() + ), + "source_pipeline_closure_p2_106_runtime_test": str( + pipeline_closure_p2_106_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-108", + "pipeline_closure_target_card_id": "P2-107", + "pipeline_closure_to_p2_107_status": status, + "pipeline_closure_to_p2_107_decision": decision, + "pipeline_closure_to_p2_107_exit_code": exit_code, + "pipeline_closure_to_p2_107_should_proceed": should_proceed, + "pipeline_closure_to_p2_107_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_107_channel": channel, + "pipeline_closure_to_p2_107_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_107_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-107 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_107_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_107_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_107_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_107_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_107_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_107_channel']}`" + ), + ( + "- Source P2-107 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_106_gate_script']}`" + ), + ( + "- Source P2-107 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_106_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_107_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_107_status": str( + payload["pipeline_closure_to_p2_107_status"] + ), + "workflow_pipeline_closure_to_p2_107_decision": str( + payload["pipeline_closure_to_p2_107_decision"] + ), + "workflow_pipeline_closure_to_p2_107_exit_code": str( + payload["pipeline_closure_to_p2_107_exit_code"] + ), + "workflow_pipeline_closure_to_p2_107_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_107_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_107_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_107_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_107_channel": str( + payload["pipeline_closure_to_p2_107_channel"] + ), + "workflow_pipeline_closure_to_p2_107_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_107_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_107_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_107_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_107_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-107 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-106-gate-script", + dest="pipeline_closure_p2_106_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py", + help="P2-107 closure verifier script path (P2-106 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-106-runtime-test", + dest="pipeline_closure_p2_106_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py", + help="P2-107 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_107.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_107.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_106_gate_script = project_root / Path( + args.pipeline_closure_p2_106_gate_script + ) + pipeline_closure_p2_106_runtime_test = project_root / Path( + args.pipeline_closure_p2_106_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_107_payload( + project_root=project_root, + pipeline_closure_p2_106_gate_script=pipeline_closure_p2_106_gate_script, + pipeline_closure_p2_106_runtime_test=pipeline_closure_p2_106_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-107-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_107_status={payload['pipeline_closure_to_p2_107_status']} " + f"pipeline_closure_to_p2_107_decision={payload['pipeline_closure_to_p2_107_decision']} " + f"pipeline_closure_to_p2_107_exit_code={payload['pipeline_closure_to_p2_107_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_107_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py new file mode 100644 index 0000000..5763d30 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-109 gate for Linux CI workflow pipeline closure to P2-108. + +This script validates that the P2-108 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_108_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_108_missing": '"pipeline_closure_card_id": "P2-108"', + "pipeline_closure_target_card_id_p2_107_missing": '"pipeline_closure_target_card_id": "P2-107"', + "pipeline_closure_status_key_p2_107_missing": "pipeline_closure_to_p2_107_status", + "pipeline_closure_decision_verified_p2_107_missing": "pipeline_closure_to_p2_107_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_107_missing": "ci_workflow_pipeline_closure_p2_107.json", + "pipeline_closure_report_markdown_p2_107_missing": "ci_workflow_pipeline_closure_p2_107.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_108_payload( + *, + project_root: Path, + pipeline_closure_p2_107_gate_script: Path, + pipeline_closure_p2_107_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_107_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_107_gate_script)) + p2_108_closure_text = "" + else: + p2_108_closure_text = _read_text(pipeline_closure_p2_107_gate_script) + + if not pipeline_closure_p2_107_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_107_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_108_CLOSURE_PATTERNS.items(): + if pattern not in p2_108_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_108_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_108_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_108_verified", + "pipeline_closure_p2_108_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-108 " + f"pipeline_closure_to_p2_108_status={status} " + f"pipeline_closure_to_p2_108_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_107_gate_script": str( + pipeline_closure_p2_107_gate_script.resolve() + ), + "source_pipeline_closure_p2_107_runtime_test": str( + pipeline_closure_p2_107_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-109", + "pipeline_closure_target_card_id": "P2-108", + "pipeline_closure_to_p2_108_status": status, + "pipeline_closure_to_p2_108_decision": decision, + "pipeline_closure_to_p2_108_exit_code": exit_code, + "pipeline_closure_to_p2_108_should_proceed": should_proceed, + "pipeline_closure_to_p2_108_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_108_channel": channel, + "pipeline_closure_to_p2_108_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_108_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-108 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_108_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_108_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_108_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_108_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_108_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_108_channel']}`" + ), + ( + "- Source P2-108 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_107_gate_script']}`" + ), + ( + "- Source P2-108 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_107_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_108_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_108_status": str( + payload["pipeline_closure_to_p2_108_status"] + ), + "workflow_pipeline_closure_to_p2_108_decision": str( + payload["pipeline_closure_to_p2_108_decision"] + ), + "workflow_pipeline_closure_to_p2_108_exit_code": str( + payload["pipeline_closure_to_p2_108_exit_code"] + ), + "workflow_pipeline_closure_to_p2_108_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_108_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_108_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_108_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_108_channel": str( + payload["pipeline_closure_to_p2_108_channel"] + ), + "workflow_pipeline_closure_to_p2_108_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_108_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_108_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_108_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_108_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-108 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-107-gate-script", + dest="pipeline_closure_p2_107_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py", + help="P2-108 closure verifier script path (P2-107 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-107-runtime-test", + dest="pipeline_closure_p2_107_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py", + help="P2-108 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_108.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_108.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_107_gate_script = project_root / Path( + args.pipeline_closure_p2_107_gate_script + ) + pipeline_closure_p2_107_runtime_test = project_root / Path( + args.pipeline_closure_p2_107_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_108_payload( + project_root=project_root, + pipeline_closure_p2_107_gate_script=pipeline_closure_p2_107_gate_script, + pipeline_closure_p2_107_runtime_test=pipeline_closure_p2_107_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-108-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_108_status={payload['pipeline_closure_to_p2_108_status']} " + f"pipeline_closure_to_p2_108_decision={payload['pipeline_closure_to_p2_108_decision']} " + f"pipeline_closure_to_p2_108_exit_code={payload['pipeline_closure_to_p2_108_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_108_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py new file mode 100644 index 0000000..68ed524 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py @@ -0,0 +1,357 @@ +"""Phase 2 card P2-110 gate for Linux CI workflow pipeline closure to P2-109. + +This script validates that the P2-109 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_109_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_109_missing": '"pipeline_closure_card_id": "P2-109"', + "pipeline_closure_target_card_id_p2_108_missing": '"pipeline_closure_target_card_id": "P2-108"', + "pipeline_closure_status_key_p2_108_missing": "pipeline_closure_to_p2_108_status", + "pipeline_closure_decision_verified_p2_108_missing": "pipeline_closure_to_p2_108_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_108_missing": "ci_workflow_pipeline_closure_p2_108.json", + "pipeline_closure_report_markdown_p2_108_missing": "ci_workflow_pipeline_closure_p2_108.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_109_payload( + *, + project_root: Path, + pipeline_closure_p2_108_gate_script: Path, + pipeline_closure_p2_108_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_108_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_108_gate_script)) + p2_109_closure_text = "" + else: + p2_109_closure_text = _read_text(pipeline_closure_p2_108_gate_script) + + if not pipeline_closure_p2_108_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_108_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_109_CLOSURE_PATTERNS.items(): + if pattern not in p2_109_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_109_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_109_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_109_verified", + "pipeline_closure_p2_109_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-109 " + f"pipeline_closure_to_p2_109_status={status} " + f"pipeline_closure_to_p2_109_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_108_gate_script": str( + pipeline_closure_p2_108_gate_script.resolve() + ), + "source_pipeline_closure_p2_108_runtime_test": str( + pipeline_closure_p2_108_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-110", + "pipeline_closure_target_card_id": "P2-109", + "pipeline_closure_to_p2_109_status": status, + "pipeline_closure_to_p2_109_decision": decision, + "pipeline_closure_to_p2_109_exit_code": exit_code, + "pipeline_closure_to_p2_109_should_proceed": should_proceed, + "pipeline_closure_to_p2_109_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_109_channel": channel, + "pipeline_closure_to_p2_109_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_109_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-109 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_109_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_109_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_109_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_109_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_109_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_109_channel']}`" + ), + ( + "- Source P2-109 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_108_gate_script']}`" + ), + ( + "- Source P2-109 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_108_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_109_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_109_status": str( + payload["pipeline_closure_to_p2_109_status"] + ), + "workflow_pipeline_closure_to_p2_109_decision": str( + payload["pipeline_closure_to_p2_109_decision"] + ), + "workflow_pipeline_closure_to_p2_109_exit_code": str( + payload["pipeline_closure_to_p2_109_exit_code"] + ), + "workflow_pipeline_closure_to_p2_109_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_109_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_109_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_109_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_109_channel": str( + payload["pipeline_closure_to_p2_109_channel"] + ), + "workflow_pipeline_closure_to_p2_109_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_109_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_109_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_109_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_109_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-109 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-108-gate-script", + dest="pipeline_closure_p2_108_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py", + help="P2-109 closure verifier script path (P2-108 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-108-runtime-test", + dest="pipeline_closure_p2_108_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py", + help="P2-109 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_109.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_109.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_108_gate_script = project_root / Path( + args.pipeline_closure_p2_108_gate_script + ) + pipeline_closure_p2_108_runtime_test = project_root / Path( + args.pipeline_closure_p2_108_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_109_payload( + project_root=project_root, + pipeline_closure_p2_108_gate_script=pipeline_closure_p2_108_gate_script, + pipeline_closure_p2_108_runtime_test=pipeline_closure_p2_108_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-109-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_109_status={payload['pipeline_closure_to_p2_109_status']} " + f"pipeline_closure_to_p2_109_decision={payload['pipeline_closure_to_p2_109_decision']} " + f"pipeline_closure_to_p2_109_exit_code={payload['pipeline_closure_to_p2_109_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_109_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py new file mode 100644 index 0000000..bd77f4a --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py @@ -0,0 +1,358 @@ +"""Phase 2 card P2-111 gate for Linux CI workflow pipeline closure to P2-110. + +This script validates that the P2-110 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_110_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_110_missing": '"pipeline_closure_card_id": "P2-110"', + "pipeline_closure_target_card_id_p2_109_missing": '"pipeline_closure_target_card_id": "P2-109"', + "pipeline_closure_status_key_p2_109_missing": "pipeline_closure_to_p2_109_status", + "pipeline_closure_decision_verified_p2_109_missing": "pipeline_closure_to_p2_109_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_109_missing": "ci_workflow_pipeline_closure_p2_109.json", + "pipeline_closure_report_markdown_p2_109_missing": "ci_workflow_pipeline_closure_p2_109.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_110_payload( + *, + project_root: Path, + pipeline_closure_p2_109_gate_script: Path, + pipeline_closure_p2_109_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_109_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_109_gate_script)) + p2_110_closure_text = "" + else: + p2_110_closure_text = _read_text(pipeline_closure_p2_109_gate_script) + + if not pipeline_closure_p2_109_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_109_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_110_CLOSURE_PATTERNS.items(): + if pattern not in p2_110_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_110_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_110_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_110_verified", + "pipeline_closure_p2_110_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-110 " + f"pipeline_closure_to_p2_110_status={status} " + f"pipeline_closure_to_p2_110_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_109_gate_script": str( + pipeline_closure_p2_109_gate_script.resolve() + ), + "source_pipeline_closure_p2_109_runtime_test": str( + pipeline_closure_p2_109_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-111", + "pipeline_closure_target_card_id": "P2-110", + "pipeline_closure_to_p2_110_status": status, + "pipeline_closure_to_p2_110_decision": decision, + "pipeline_closure_to_p2_110_exit_code": exit_code, + "pipeline_closure_to_p2_110_should_proceed": should_proceed, + "pipeline_closure_to_p2_110_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_110_channel": channel, + "pipeline_closure_to_p2_110_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_110_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-110 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_110_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_110_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_110_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_110_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_110_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_110_channel']}`" + ), + ( + "- Source P2-110 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_109_gate_script']}`" + ), + ( + "- Source P2-110 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_109_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_110_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_110_status": str( + payload["pipeline_closure_to_p2_110_status"] + ), + "workflow_pipeline_closure_to_p2_110_decision": str( + payload["pipeline_closure_to_p2_110_decision"] + ), + "workflow_pipeline_closure_to_p2_110_exit_code": str( + payload["pipeline_closure_to_p2_110_exit_code"] + ), + "workflow_pipeline_closure_to_p2_110_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_110_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_110_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_110_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_110_channel": str( + payload["pipeline_closure_to_p2_110_channel"] + ), + "workflow_pipeline_closure_to_p2_110_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_110_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_110_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_110_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_110_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-110 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-109-gate-script", + dest="pipeline_closure_p2_109_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py", + help="P2-110 closure verifier script path (P2-109 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-109-runtime-test", + dest="pipeline_closure_p2_109_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py", + help="P2-110 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_110.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_110.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_109_gate_script = project_root / Path( + args.pipeline_closure_p2_109_gate_script + ) + pipeline_closure_p2_109_runtime_test = project_root / Path( + args.pipeline_closure_p2_109_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_110_payload( + project_root=project_root, + pipeline_closure_p2_109_gate_script=pipeline_closure_p2_109_gate_script, + pipeline_closure_p2_109_runtime_test=pipeline_closure_p2_109_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-110-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_110_status={payload['pipeline_closure_to_p2_110_status']} " + f"pipeline_closure_to_p2_110_decision={payload['pipeline_closure_to_p2_110_decision']} " + f"pipeline_closure_to_p2_110_exit_code={payload['pipeline_closure_to_p2_110_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_110_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py new file mode 100644 index 0000000..100113e --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py @@ -0,0 +1,358 @@ +"""Phase 2 card P2-112 gate for Linux CI workflow pipeline closure to P2-111. + +This script validates that the P2-111 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_111_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_111_missing": '"pipeline_closure_card_id": "P2-111"', + "pipeline_closure_target_card_id_p2_110_missing": '"pipeline_closure_target_card_id": "P2-110"', + "pipeline_closure_status_key_p2_110_missing": "pipeline_closure_to_p2_110_status", + "pipeline_closure_decision_verified_p2_110_missing": "pipeline_closure_to_p2_110_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_110_missing": "ci_workflow_pipeline_closure_p2_110.json", + "pipeline_closure_report_markdown_p2_110_missing": "ci_workflow_pipeline_closure_p2_110.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_111_payload( + *, + project_root: Path, + pipeline_closure_p2_110_gate_script: Path, + pipeline_closure_p2_110_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_110_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_110_gate_script)) + p2_111_closure_text = "" + else: + p2_111_closure_text = _read_text(pipeline_closure_p2_110_gate_script) + + if not pipeline_closure_p2_110_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_110_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_111_CLOSURE_PATTERNS.items(): + if pattern not in p2_111_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_111_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_111_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_111_verified", + "pipeline_closure_p2_111_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-111 " + f"pipeline_closure_to_p2_111_status={status} " + f"pipeline_closure_to_p2_111_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_110_gate_script": str( + pipeline_closure_p2_110_gate_script.resolve() + ), + "source_pipeline_closure_p2_110_runtime_test": str( + pipeline_closure_p2_110_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-112", + "pipeline_closure_target_card_id": "P2-111", + "pipeline_closure_to_p2_111_status": status, + "pipeline_closure_to_p2_111_decision": decision, + "pipeline_closure_to_p2_111_exit_code": exit_code, + "pipeline_closure_to_p2_111_should_proceed": should_proceed, + "pipeline_closure_to_p2_111_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_111_channel": channel, + "pipeline_closure_to_p2_111_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_111_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-111 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_111_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_111_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_111_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_111_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_111_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_111_channel']}`" + ), + ( + "- Source P2-111 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_110_gate_script']}`" + ), + ( + "- Source P2-111 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_110_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_111_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_111_status": str( + payload["pipeline_closure_to_p2_111_status"] + ), + "workflow_pipeline_closure_to_p2_111_decision": str( + payload["pipeline_closure_to_p2_111_decision"] + ), + "workflow_pipeline_closure_to_p2_111_exit_code": str( + payload["pipeline_closure_to_p2_111_exit_code"] + ), + "workflow_pipeline_closure_to_p2_111_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_111_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_111_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_111_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_111_channel": str( + payload["pipeline_closure_to_p2_111_channel"] + ), + "workflow_pipeline_closure_to_p2_111_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_111_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_111_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_111_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_111_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-111 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-110-gate-script", + dest="pipeline_closure_p2_110_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py", + help="P2-111 closure verifier script path (P2-110 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-110-runtime-test", + dest="pipeline_closure_p2_110_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py", + help="P2-111 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_111.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_111.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_110_gate_script = project_root / Path( + args.pipeline_closure_p2_110_gate_script + ) + pipeline_closure_p2_110_runtime_test = project_root / Path( + args.pipeline_closure_p2_110_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_111_payload( + project_root=project_root, + pipeline_closure_p2_110_gate_script=pipeline_closure_p2_110_gate_script, + pipeline_closure_p2_110_runtime_test=pipeline_closure_p2_110_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-111-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_111_status={payload['pipeline_closure_to_p2_111_status']} " + f"pipeline_closure_to_p2_111_decision={payload['pipeline_closure_to_p2_111_decision']} " + f"pipeline_closure_to_p2_111_exit_code={payload['pipeline_closure_to_p2_111_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_111_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py new file mode 100644 index 0000000..8f2e4bf --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py @@ -0,0 +1,359 @@ +"""Phase 2 card P2-113 gate for Linux CI workflow pipeline closure to P2-112. + +This script validates that the P2-112 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_112_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_112_missing": '"pipeline_closure_card_id": "P2-112"', + "pipeline_closure_target_card_id_p2_111_missing": '"pipeline_closure_target_card_id": "P2-111"', + "pipeline_closure_status_key_p2_111_missing": "pipeline_closure_to_p2_111_status", + "pipeline_closure_decision_verified_p2_111_missing": "pipeline_closure_to_p2_111_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_111_missing": "ci_workflow_pipeline_closure_p2_111.json", + "pipeline_closure_report_markdown_p2_111_missing": "ci_workflow_pipeline_closure_p2_111.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_112_payload( + *, + project_root: Path, + pipeline_closure_p2_111_gate_script: Path, + pipeline_closure_p2_111_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_111_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_111_gate_script)) + p2_112_closure_text = "" + else: + p2_112_closure_text = _read_text(pipeline_closure_p2_111_gate_script) + + if not pipeline_closure_p2_111_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_111_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_112_CLOSURE_PATTERNS.items(): + if pattern not in p2_112_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_112_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_112_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_112_verified", + "pipeline_closure_p2_112_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-112 " + f"pipeline_closure_to_p2_112_status={status} " + f"pipeline_closure_to_p2_112_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_111_gate_script": str( + pipeline_closure_p2_111_gate_script.resolve() + ), + "source_pipeline_closure_p2_111_runtime_test": str( + pipeline_closure_p2_111_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-113", + "pipeline_closure_target_card_id": "P2-112", + "pipeline_closure_to_p2_112_status": status, + "pipeline_closure_to_p2_112_decision": decision, + "pipeline_closure_to_p2_112_exit_code": exit_code, + "pipeline_closure_to_p2_112_should_proceed": should_proceed, + "pipeline_closure_to_p2_112_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_112_channel": channel, + "pipeline_closure_to_p2_112_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_112_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-112 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_112_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_112_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_112_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_112_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_112_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_112_channel']}`" + ), + ( + "- Source P2-112 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_111_gate_script']}`" + ), + ( + "- Source P2-112 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_111_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_112_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_112_status": str( + payload["pipeline_closure_to_p2_112_status"] + ), + "workflow_pipeline_closure_to_p2_112_decision": str( + payload["pipeline_closure_to_p2_112_decision"] + ), + "workflow_pipeline_closure_to_p2_112_exit_code": str( + payload["pipeline_closure_to_p2_112_exit_code"] + ), + "workflow_pipeline_closure_to_p2_112_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_112_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_112_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_112_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_112_channel": str( + payload["pipeline_closure_to_p2_112_channel"] + ), + "workflow_pipeline_closure_to_p2_112_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_112_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_112_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_112_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_112_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-112 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-111-gate-script", + dest="pipeline_closure_p2_111_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py", + help="P2-112 closure verifier script path (P2-111 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-111-runtime-test", + dest="pipeline_closure_p2_111_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py", + help="P2-112 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_112.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_112.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_111_gate_script = project_root / Path( + args.pipeline_closure_p2_111_gate_script + ) + pipeline_closure_p2_111_runtime_test = project_root / Path( + args.pipeline_closure_p2_111_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_112_payload( + project_root=project_root, + pipeline_closure_p2_111_gate_script=pipeline_closure_p2_111_gate_script, + pipeline_closure_p2_111_runtime_test=pipeline_closure_p2_111_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-112-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_112_status={payload['pipeline_closure_to_p2_112_status']} " + f"pipeline_closure_to_p2_112_decision={payload['pipeline_closure_to_p2_112_decision']} " + f"pipeline_closure_to_p2_112_exit_code={payload['pipeline_closure_to_p2_112_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_112_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py new file mode 100644 index 0000000..179fb1b --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py @@ -0,0 +1,360 @@ +"""Phase 2 card P2-114 gate for Linux CI workflow pipeline closure to P2-113. + +This script validates that the P2-113 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_113_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_113_missing": '"pipeline_closure_card_id": "P2-113"', + "pipeline_closure_target_card_id_p2_112_missing": '"pipeline_closure_target_card_id": "P2-112"', + "pipeline_closure_status_key_p2_112_missing": "pipeline_closure_to_p2_112_status", + "pipeline_closure_decision_verified_p2_112_missing": "pipeline_closure_to_p2_112_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_112_missing": "ci_workflow_pipeline_closure_p2_112.json", + "pipeline_closure_report_markdown_p2_112_missing": "ci_workflow_pipeline_closure_p2_112.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_113_payload( + *, + project_root: Path, + pipeline_closure_p2_112_gate_script: Path, + pipeline_closure_p2_112_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_112_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_112_gate_script)) + p2_113_closure_text = "" + else: + p2_113_closure_text = _read_text(pipeline_closure_p2_112_gate_script) + + if not pipeline_closure_p2_112_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_112_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_113_CLOSURE_PATTERNS.items(): + if pattern not in p2_113_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_113_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_113_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_113_verified", + "pipeline_closure_p2_113_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-113 " + f"pipeline_closure_to_p2_113_status={status} " + f"pipeline_closure_to_p2_113_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_112_gate_script": str( + pipeline_closure_p2_112_gate_script.resolve() + ), + "source_pipeline_closure_p2_112_runtime_test": str( + pipeline_closure_p2_112_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-114", + "pipeline_closure_target_card_id": "P2-113", + "pipeline_closure_to_p2_113_status": status, + "pipeline_closure_to_p2_113_decision": decision, + "pipeline_closure_to_p2_113_exit_code": exit_code, + "pipeline_closure_to_p2_113_should_proceed": should_proceed, + "pipeline_closure_to_p2_113_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_113_channel": channel, + "pipeline_closure_to_p2_113_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_113_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-113 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_113_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_113_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_113_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_113_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_113_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_113_channel']}`" + ), + ( + "- Source P2-113 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_112_gate_script']}`" + ), + ( + "- Source P2-113 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_112_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_113_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_113_status": str( + payload["pipeline_closure_to_p2_113_status"] + ), + "workflow_pipeline_closure_to_p2_113_decision": str( + payload["pipeline_closure_to_p2_113_decision"] + ), + "workflow_pipeline_closure_to_p2_113_exit_code": str( + payload["pipeline_closure_to_p2_113_exit_code"] + ), + "workflow_pipeline_closure_to_p2_113_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_113_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_113_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_113_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_113_channel": str( + payload["pipeline_closure_to_p2_113_channel"] + ), + "workflow_pipeline_closure_to_p2_113_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_113_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_113_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_113_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_113_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-113 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-112-gate-script", + dest="pipeline_closure_p2_112_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py", + help="P2-113 closure verifier script path (P2-112 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-112-runtime-test", + dest="pipeline_closure_p2_112_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py", + help="P2-113 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_113.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_113.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_112_gate_script = project_root / Path( + args.pipeline_closure_p2_112_gate_script + ) + pipeline_closure_p2_112_runtime_test = project_root / Path( + args.pipeline_closure_p2_112_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_113_payload( + project_root=project_root, + pipeline_closure_p2_112_gate_script=pipeline_closure_p2_112_gate_script, + pipeline_closure_p2_112_runtime_test=pipeline_closure_p2_112_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-113-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_113_status={payload['pipeline_closure_to_p2_113_status']} " + f"pipeline_closure_to_p2_113_decision={payload['pipeline_closure_to_p2_113_decision']} " + f"pipeline_closure_to_p2_113_exit_code={payload['pipeline_closure_to_p2_113_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_113_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py new file mode 100644 index 0000000..f550ece --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py @@ -0,0 +1,361 @@ +"""Phase 2 card P2-115 gate for Linux CI workflow pipeline closure to P2-114. + +This script validates that the P2-114 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_114_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_114_missing": '"pipeline_closure_card_id": "P2-114"', + "pipeline_closure_target_card_id_p2_113_missing": '"pipeline_closure_target_card_id": "P2-113"', + "pipeline_closure_status_key_p2_113_missing": "pipeline_closure_to_p2_113_status", + "pipeline_closure_decision_verified_p2_113_missing": "pipeline_closure_to_p2_113_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_113_missing": "ci_workflow_pipeline_closure_p2_113.json", + "pipeline_closure_report_markdown_p2_113_missing": "ci_workflow_pipeline_closure_p2_113.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_114_payload( + *, + project_root: Path, + pipeline_closure_p2_113_gate_script: Path, + pipeline_closure_p2_113_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_113_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_113_gate_script)) + p2_114_closure_text = "" + else: + p2_114_closure_text = _read_text(pipeline_closure_p2_113_gate_script) + + if not pipeline_closure_p2_113_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_113_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_114_CLOSURE_PATTERNS.items(): + if pattern not in p2_114_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_114_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_114_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_114_verified", + "pipeline_closure_p2_114_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-114 " + f"pipeline_closure_to_p2_114_status={status} " + f"pipeline_closure_to_p2_114_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_113_gate_script": str( + pipeline_closure_p2_113_gate_script.resolve() + ), + "source_pipeline_closure_p2_113_runtime_test": str( + pipeline_closure_p2_113_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-115", + "pipeline_closure_target_card_id": "P2-114", + "pipeline_closure_to_p2_114_status": status, + "pipeline_closure_to_p2_114_decision": decision, + "pipeline_closure_to_p2_114_exit_code": exit_code, + "pipeline_closure_to_p2_114_should_proceed": should_proceed, + "pipeline_closure_to_p2_114_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_114_channel": channel, + "pipeline_closure_to_p2_114_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_114_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-114 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_114_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_114_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_114_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_114_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_114_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_114_channel']}`" + ), + ( + "- Source P2-114 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_113_gate_script']}`" + ), + ( + "- Source P2-114 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_113_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_114_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_114_status": str( + payload["pipeline_closure_to_p2_114_status"] + ), + "workflow_pipeline_closure_to_p2_114_decision": str( + payload["pipeline_closure_to_p2_114_decision"] + ), + "workflow_pipeline_closure_to_p2_114_exit_code": str( + payload["pipeline_closure_to_p2_114_exit_code"] + ), + "workflow_pipeline_closure_to_p2_114_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_114_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_114_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_114_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_114_channel": str( + payload["pipeline_closure_to_p2_114_channel"] + ), + "workflow_pipeline_closure_to_p2_114_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_114_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_114_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_114_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_114_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-114 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-113-gate-script", + dest="pipeline_closure_p2_113_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py", + help="P2-114 closure verifier script path (P2-113 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-113-runtime-test", + dest="pipeline_closure_p2_113_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py", + help="P2-114 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_114.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_114.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_113_gate_script = project_root / Path( + args.pipeline_closure_p2_113_gate_script + ) + pipeline_closure_p2_113_runtime_test = project_root / Path( + args.pipeline_closure_p2_113_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_114_payload( + project_root=project_root, + pipeline_closure_p2_113_gate_script=pipeline_closure_p2_113_gate_script, + pipeline_closure_p2_113_runtime_test=pipeline_closure_p2_113_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-114-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_114_status={payload['pipeline_closure_to_p2_114_status']} " + f"pipeline_closure_to_p2_114_decision={payload['pipeline_closure_to_p2_114_decision']} " + f"pipeline_closure_to_p2_114_exit_code={payload['pipeline_closure_to_p2_114_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_114_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py new file mode 100644 index 0000000..194c811 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py @@ -0,0 +1,361 @@ +"""Phase 2 card P2-116 gate for Linux CI workflow pipeline closure to P2-115. + +This script validates that the P2-115 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_115_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_115_missing": '"pipeline_closure_card_id": "P2-115"', + "pipeline_closure_target_card_id_p2_114_missing": '"pipeline_closure_target_card_id": "P2-114"', + "pipeline_closure_status_key_p2_114_missing": "pipeline_closure_to_p2_114_status", + "pipeline_closure_decision_verified_p2_114_missing": "pipeline_closure_to_p2_114_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_114_missing": "ci_workflow_pipeline_closure_p2_114.json", + "pipeline_closure_report_markdown_p2_114_missing": "ci_workflow_pipeline_closure_p2_114.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_115_payload( + *, + project_root: Path, + pipeline_closure_p2_114_gate_script: Path, + pipeline_closure_p2_114_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_114_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_114_gate_script)) + p2_115_closure_text = "" + else: + p2_115_closure_text = _read_text(pipeline_closure_p2_114_gate_script) + + if not pipeline_closure_p2_114_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_114_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_115_CLOSURE_PATTERNS.items(): + if pattern not in p2_115_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_115_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_115_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_115_verified", + "pipeline_closure_p2_115_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-115 " + f"pipeline_closure_to_p2_115_status={status} " + f"pipeline_closure_to_p2_115_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_114_gate_script": str( + pipeline_closure_p2_114_gate_script.resolve() + ), + "source_pipeline_closure_p2_114_runtime_test": str( + pipeline_closure_p2_114_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-116", + "pipeline_closure_target_card_id": "P2-115", + "pipeline_closure_to_p2_115_status": status, + "pipeline_closure_to_p2_115_decision": decision, + "pipeline_closure_to_p2_115_exit_code": exit_code, + "pipeline_closure_to_p2_115_should_proceed": should_proceed, + "pipeline_closure_to_p2_115_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_115_channel": channel, + "pipeline_closure_to_p2_115_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_115_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-115 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_115_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_115_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_115_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_115_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_115_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_115_channel']}`" + ), + ( + "- Source P2-115 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_114_gate_script']}`" + ), + ( + "- Source P2-115 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_114_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_115_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_115_status": str( + payload["pipeline_closure_to_p2_115_status"] + ), + "workflow_pipeline_closure_to_p2_115_decision": str( + payload["pipeline_closure_to_p2_115_decision"] + ), + "workflow_pipeline_closure_to_p2_115_exit_code": str( + payload["pipeline_closure_to_p2_115_exit_code"] + ), + "workflow_pipeline_closure_to_p2_115_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_115_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_115_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_115_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_115_channel": str( + payload["pipeline_closure_to_p2_115_channel"] + ), + "workflow_pipeline_closure_to_p2_115_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_115_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_115_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_115_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_115_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-115 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-114-gate-script", + dest="pipeline_closure_p2_114_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py", + help="P2-115 closure verifier script path (P2-114 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-114-runtime-test", + dest="pipeline_closure_p2_114_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py", + help="P2-115 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_115.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_115.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_114_gate_script = project_root / Path( + args.pipeline_closure_p2_114_gate_script + ) + pipeline_closure_p2_114_runtime_test = project_root / Path( + args.pipeline_closure_p2_114_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_115_payload( + project_root=project_root, + pipeline_closure_p2_114_gate_script=pipeline_closure_p2_114_gate_script, + pipeline_closure_p2_114_runtime_test=pipeline_closure_p2_114_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-115-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_115_status={payload['pipeline_closure_to_p2_115_status']} " + f"pipeline_closure_to_p2_115_decision={payload['pipeline_closure_to_p2_115_decision']} " + f"pipeline_closure_to_p2_115_exit_code={payload['pipeline_closure_to_p2_115_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_115_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py new file mode 100644 index 0000000..cd38285 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py @@ -0,0 +1,362 @@ +"""Phase 2 card P2-117 gate for Linux CI workflow pipeline closure to P2-116. + +This script validates that the P2-116 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_116_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_116_missing": '"pipeline_closure_card_id": "P2-116"', + "pipeline_closure_target_card_id_p2_115_missing": '"pipeline_closure_target_card_id": "P2-115"', + "pipeline_closure_status_key_p2_115_missing": "pipeline_closure_to_p2_115_status", + "pipeline_closure_decision_verified_p2_115_missing": "pipeline_closure_to_p2_115_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_115_missing": "ci_workflow_pipeline_closure_p2_115.json", + "pipeline_closure_report_markdown_p2_115_missing": "ci_workflow_pipeline_closure_p2_115.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_116_payload( + *, + project_root: Path, + pipeline_closure_p2_115_gate_script: Path, + pipeline_closure_p2_115_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_115_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_115_gate_script)) + p2_116_closure_text = "" + else: + p2_116_closure_text = _read_text(pipeline_closure_p2_115_gate_script) + + if not pipeline_closure_p2_115_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_115_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_116_CLOSURE_PATTERNS.items(): + if pattern not in p2_116_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_116_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_116_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_116_verified", + "pipeline_closure_p2_116_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-116 " + f"pipeline_closure_to_p2_116_status={status} " + f"pipeline_closure_to_p2_116_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_115_gate_script": str( + pipeline_closure_p2_115_gate_script.resolve() + ), + "source_pipeline_closure_p2_115_runtime_test": str( + pipeline_closure_p2_115_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-117", + "pipeline_closure_target_card_id": "P2-116", + "pipeline_closure_to_p2_116_status": status, + "pipeline_closure_to_p2_116_decision": decision, + "pipeline_closure_to_p2_116_exit_code": exit_code, + "pipeline_closure_to_p2_116_should_proceed": should_proceed, + "pipeline_closure_to_p2_116_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_116_channel": channel, + "pipeline_closure_to_p2_116_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_116_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-116 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_116_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_116_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_116_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_116_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_116_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_116_channel']}`" + ), + ( + "- Source P2-116 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_115_gate_script']}`" + ), + ( + "- Source P2-116 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_115_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_116_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_116_status": str( + payload["pipeline_closure_to_p2_116_status"] + ), + "workflow_pipeline_closure_to_p2_116_decision": str( + payload["pipeline_closure_to_p2_116_decision"] + ), + "workflow_pipeline_closure_to_p2_116_exit_code": str( + payload["pipeline_closure_to_p2_116_exit_code"] + ), + "workflow_pipeline_closure_to_p2_116_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_116_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_116_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_116_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_116_channel": str( + payload["pipeline_closure_to_p2_116_channel"] + ), + "workflow_pipeline_closure_to_p2_116_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_116_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_116_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_116_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_116_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-116 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-115-gate-script", + dest="pipeline_closure_p2_115_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py", + help="P2-116 closure verifier script path (P2-115 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-115-runtime-test", + dest="pipeline_closure_p2_115_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py", + help="P2-116 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_116.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_116.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_115_gate_script = project_root / Path( + args.pipeline_closure_p2_115_gate_script + ) + pipeline_closure_p2_115_runtime_test = project_root / Path( + args.pipeline_closure_p2_115_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_116_payload( + project_root=project_root, + pipeline_closure_p2_115_gate_script=pipeline_closure_p2_115_gate_script, + pipeline_closure_p2_115_runtime_test=pipeline_closure_p2_115_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-116-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_116_status={payload['pipeline_closure_to_p2_116_status']} " + f"pipeline_closure_to_p2_116_decision={payload['pipeline_closure_to_p2_116_decision']} " + f"pipeline_closure_to_p2_116_exit_code={payload['pipeline_closure_to_p2_116_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_116_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_117_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_117_gate.py new file mode 100644 index 0000000..a607f0b --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_117_gate.py @@ -0,0 +1,363 @@ +"""Phase 2 card P2-118 gate for Linux CI workflow pipeline closure to P2-117. + +This script validates that the P2-117 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_117_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_117_missing": '"pipeline_closure_card_id": "P2-118"', + "pipeline_closure_target_card_id_p2_116_missing": '"pipeline_closure_target_card_id": "P2-116"', + "pipeline_closure_status_key_p2_116_missing": "pipeline_closure_to_p2_116_status", + "pipeline_closure_decision_verified_p2_116_missing": "pipeline_closure_to_p2_116_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_116_missing": "ci_workflow_pipeline_closure_p2_116.json", + "pipeline_closure_report_markdown_p2_116_missing": "ci_workflow_pipeline_closure_p2_116.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_117_payload( + *, + project_root: Path, + pipeline_closure_p2_116_gate_script: Path, + pipeline_closure_p2_116_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_116_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_116_gate_script)) + p2_117_closure_text = "" + else: + p2_117_closure_text = _read_text(pipeline_closure_p2_116_gate_script) + + if not pipeline_closure_p2_116_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_116_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_117_CLOSURE_PATTERNS.items(): + if pattern not in p2_117_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_117_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_117_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_117_verified", + "pipeline_closure_p2_117_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-117 " + f"pipeline_closure_to_p2_117_status={status} " + f"pipeline_closure_to_p2_117_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_116_gate_script": str( + pipeline_closure_p2_116_gate_script.resolve() + ), + "source_pipeline_closure_p2_116_runtime_test": str( + pipeline_closure_p2_116_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-118", + "pipeline_closure_target_card_id": "P2-116", + "pipeline_closure_to_p2_117_status": status, + "pipeline_closure_to_p2_117_decision": decision, + "pipeline_closure_to_p2_117_exit_code": exit_code, + "pipeline_closure_to_p2_117_should_proceed": should_proceed, + "pipeline_closure_to_p2_117_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_117_channel": channel, + "pipeline_closure_to_p2_117_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_117_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-117 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_117_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_117_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_117_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_117_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_117_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_117_channel']}`" + ), + ( + "- Source P2-117 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_116_gate_script']}`" + ), + ( + "- Source P2-117 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_116_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_117_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_117_status": str( + payload["pipeline_closure_to_p2_117_status"] + ), + "workflow_pipeline_closure_to_p2_117_decision": str( + payload["pipeline_closure_to_p2_117_decision"] + ), + "workflow_pipeline_closure_to_p2_117_exit_code": str( + payload["pipeline_closure_to_p2_117_exit_code"] + ), + "workflow_pipeline_closure_to_p2_117_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_117_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_117_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_117_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_117_channel": str( + payload["pipeline_closure_to_p2_117_channel"] + ), + "workflow_pipeline_closure_to_p2_117_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_117_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_117_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_117_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_117_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-117 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-116-gate-script", + dest="pipeline_closure_p2_116_gate_script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py", + help="P2-117 closure verifier script path (P2-116 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-116-runtime-test", + dest="pipeline_closure_p2_116_runtime_test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py", + help="P2-116 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_117.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_117.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_116_gate_script = project_root / Path( + args.pipeline_closure_p2_116_gate_script + ) + pipeline_closure_p2_116_runtime_test = project_root / Path( + args.pipeline_closure_p2_116_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_117_payload( + project_root=project_root, + pipeline_closure_p2_116_gate_script=pipeline_closure_p2_116_gate_script, + pipeline_closure_p2_116_runtime_test=pipeline_closure_p2_116_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-117-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_117_status={payload['pipeline_closure_to_p2_117_status']} " + f"pipeline_closure_to_p2_117_decision={payload['pipeline_closure_to_p2_117_decision']} " + f"pipeline_closure_to_p2_117_exit_code={payload['pipeline_closure_to_p2_117_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_117_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py new file mode 100644 index 0000000..d0dd169 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-83 gate for Linux CI workflow pipeline closure to P2-82. + +This script validates that the P2-26 pipeline entry script keeps the P2-82 +closure-publish-archive stage wired as the terminal Linux validation stage. +It emits a normalized JSON/Markdown contract and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_PIPELINE_PATTERNS: dict[str, str] = { + "pipeline_chain_to_p2_82_missing": "P2-17 -> P2-82", + "pipeline_stage_closure_publish_archive_missing": ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ), + "pipeline_stage_script_closure_publish_archive_missing": ( + "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py" + ), + "pipeline_skip_flag_closure_publish_archive_missing": ( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive" + ), + "pipeline_output_json_closure_publish_archive_missing": ( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + ), + "pipeline_output_markdown_closure_publish_archive_missing": ( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + ), +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_82_payload( + *, + project_root: Path, + pipeline_gate_script: Path, + closure_publish_archive_gate_script: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_gate_script.is_file(): + missing_artifacts.append(str(pipeline_gate_script)) + pipeline_text = "" + else: + pipeline_text = _read_text(pipeline_gate_script) + + if not closure_publish_archive_gate_script.is_file(): + missing_artifacts.append(str(closure_publish_archive_gate_script)) + + for issue_code, pattern in REQUIRED_PIPELINE_PATTERNS.items(): + if pattern not in pipeline_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_82_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_82_verified", + "pipeline_stage_closure_publish_archive_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-83 " + f"pipeline_closure_to_p2_82_status={status} " + f"pipeline_closure_to_p2_82_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_gate_script": str(pipeline_gate_script.resolve()), + "source_closure_publish_archive_gate_script": str( + closure_publish_archive_gate_script.resolve() + ), + "pipeline_closure_card_id": "P2-83", + "pipeline_closure_target_card_id": "P2-82", + "pipeline_closure_to_p2_82_status": status, + "pipeline_closure_to_p2_82_decision": decision, + "pipeline_closure_to_p2_82_exit_code": exit_code, + "pipeline_closure_to_p2_82_should_proceed": should_proceed, + "pipeline_closure_to_p2_82_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_82_channel": channel, + "pipeline_closure_to_p2_82_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_PIPELINE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-82 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_82_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_82_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_82_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_82_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_82_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_82_channel']}`" + ), + f"- Source Pipeline Gate: `{payload['source_pipeline_gate_script']}`", + ( + "- Source Closure Publish Archive Gate: " + f"`{payload['source_closure_publish_archive_gate_script']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_82_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_82_status": str( + payload["pipeline_closure_to_p2_82_status"] + ), + "workflow_pipeline_closure_to_p2_82_decision": str( + payload["pipeline_closure_to_p2_82_decision"] + ), + "workflow_pipeline_closure_to_p2_82_exit_code": str( + payload["pipeline_closure_to_p2_82_exit_code"] + ), + "workflow_pipeline_closure_to_p2_82_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_82_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_82_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_82_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_82_channel": str( + payload["pipeline_closure_to_p2_82_channel"] + ), + "workflow_pipeline_closure_to_p2_82_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_82_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_82_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_82_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_82_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-82 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_gate.py", + help="Pipeline gate script path", + ) + parser.add_argument( + "--closure-publish-archive-gate-script", + default="scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + help="P2-82 closure publish archive gate script path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_gate_script = project_root / Path(args.pipeline_gate_script) + closure_publish_archive_gate_script = project_root / Path( + args.closure_publish_archive_gate_script + ) + + try: + payload = build_pipeline_closure_to_p2_82_payload( + project_root=project_root, + pipeline_gate_script=pipeline_gate_script, + closure_publish_archive_gate_script=closure_publish_archive_gate_script, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-82-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_82_status={payload['pipeline_closure_to_p2_82_status']} " + f"pipeline_closure_to_p2_82_decision={payload['pipeline_closure_to_p2_82_decision']} " + f"pipeline_closure_to_p2_82_exit_code={payload['pipeline_closure_to_p2_82_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_82_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py new file mode 100644 index 0000000..f26fa94 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-84 gate for Linux CI workflow pipeline closure to P2-83. + +This script validates that the P2-83 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_83_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_83_missing": '"pipeline_closure_card_id": "P2-83"', + "pipeline_closure_target_card_id_p2_82_missing": '"pipeline_closure_target_card_id": "P2-82"', + "pipeline_closure_status_key_p2_82_missing": "pipeline_closure_to_p2_82_status", + "pipeline_closure_decision_verified_p2_82_missing": "pipeline_closure_to_p2_82_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py" + ), + "pipeline_closure_report_json_p2_82_missing": "ci_workflow_pipeline_closure_p2_82.json", + "pipeline_closure_report_markdown_p2_82_missing": "ci_workflow_pipeline_closure_p2_82.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_83_payload( + *, + project_root: Path, + pipeline_closure_p2_82_gate_script: Path, + pipeline_closure_p2_82_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_82_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_82_gate_script)) + p2_83_closure_text = "" + else: + p2_83_closure_text = _read_text(pipeline_closure_p2_82_gate_script) + + if not pipeline_closure_p2_82_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_82_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_83_CLOSURE_PATTERNS.items(): + if pattern not in p2_83_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_83_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_83_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_83_verified", + "pipeline_closure_p2_83_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-84 " + f"pipeline_closure_to_p2_83_status={status} " + f"pipeline_closure_to_p2_83_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_82_gate_script": str( + pipeline_closure_p2_82_gate_script.resolve() + ), + "source_pipeline_closure_p2_82_runtime_test": str( + pipeline_closure_p2_82_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-84", + "pipeline_closure_target_card_id": "P2-83", + "pipeline_closure_to_p2_83_status": status, + "pipeline_closure_to_p2_83_decision": decision, + "pipeline_closure_to_p2_83_exit_code": exit_code, + "pipeline_closure_to_p2_83_should_proceed": should_proceed, + "pipeline_closure_to_p2_83_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_83_channel": channel, + "pipeline_closure_to_p2_83_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_83_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-83 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_83_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_83_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_83_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_83_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_83_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_83_channel']}`" + ), + ( + "- Source P2-83 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_82_gate_script']}`" + ), + ( + "- Source P2-83 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_82_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_83_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_83_status": str( + payload["pipeline_closure_to_p2_83_status"] + ), + "workflow_pipeline_closure_to_p2_83_decision": str( + payload["pipeline_closure_to_p2_83_decision"] + ), + "workflow_pipeline_closure_to_p2_83_exit_code": str( + payload["pipeline_closure_to_p2_83_exit_code"] + ), + "workflow_pipeline_closure_to_p2_83_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_83_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_83_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_83_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_83_channel": str( + payload["pipeline_closure_to_p2_83_channel"] + ), + "workflow_pipeline_closure_to_p2_83_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_83_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_83_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_83_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_83_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-83 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-82-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py", + help="P2-83 closure verifier script path (P2-82 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-82-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py", + help="P2-83 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_82_gate_script = project_root / Path( + args.pipeline_closure_p2_82_gate_script + ) + pipeline_closure_p2_82_runtime_test = project_root / Path( + args.pipeline_closure_p2_82_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_83_payload( + project_root=project_root, + pipeline_closure_p2_82_gate_script=pipeline_closure_p2_82_gate_script, + pipeline_closure_p2_82_runtime_test=pipeline_closure_p2_82_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-83-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_83_status={payload['pipeline_closure_to_p2_83_status']} " + f"pipeline_closure_to_p2_83_decision={payload['pipeline_closure_to_p2_83_decision']} " + f"pipeline_closure_to_p2_83_exit_code={payload['pipeline_closure_to_p2_83_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_83_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py new file mode 100644 index 0000000..bc2a04a --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-85 gate for Linux CI workflow pipeline closure to P2-84. + +This script validates that the P2-84 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_84_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_84_missing": '"pipeline_closure_card_id": "P2-84"', + "pipeline_closure_target_card_id_p2_83_missing": '"pipeline_closure_target_card_id": "P2-83"', + "pipeline_closure_status_key_p2_83_missing": "pipeline_closure_to_p2_83_status", + "pipeline_closure_decision_verified_p2_83_missing": "pipeline_closure_to_p2_83_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py" + ), + "pipeline_closure_report_json_p2_83_missing": "ci_workflow_pipeline_closure_p2_83.json", + "pipeline_closure_report_markdown_p2_83_missing": "ci_workflow_pipeline_closure_p2_83.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_84_payload( + *, + project_root: Path, + pipeline_closure_p2_83_gate_script: Path, + pipeline_closure_p2_83_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_83_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_83_gate_script)) + p2_84_closure_text = "" + else: + p2_84_closure_text = _read_text(pipeline_closure_p2_83_gate_script) + + if not pipeline_closure_p2_83_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_83_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_84_CLOSURE_PATTERNS.items(): + if pattern not in p2_84_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_84_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_84_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_84_verified", + "pipeline_closure_p2_84_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-85 " + f"pipeline_closure_to_p2_84_status={status} " + f"pipeline_closure_to_p2_84_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_83_gate_script": str( + pipeline_closure_p2_83_gate_script.resolve() + ), + "source_pipeline_closure_p2_83_runtime_test": str( + pipeline_closure_p2_83_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-85", + "pipeline_closure_target_card_id": "P2-84", + "pipeline_closure_to_p2_84_status": status, + "pipeline_closure_to_p2_84_decision": decision, + "pipeline_closure_to_p2_84_exit_code": exit_code, + "pipeline_closure_to_p2_84_should_proceed": should_proceed, + "pipeline_closure_to_p2_84_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_84_channel": channel, + "pipeline_closure_to_p2_84_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_84_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-84 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_84_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_84_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_84_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_84_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_84_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_84_channel']}`" + ), + ( + "- Source P2-84 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_83_gate_script']}`" + ), + ( + "- Source P2-84 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_83_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_84_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_84_status": str( + payload["pipeline_closure_to_p2_84_status"] + ), + "workflow_pipeline_closure_to_p2_84_decision": str( + payload["pipeline_closure_to_p2_84_decision"] + ), + "workflow_pipeline_closure_to_p2_84_exit_code": str( + payload["pipeline_closure_to_p2_84_exit_code"] + ), + "workflow_pipeline_closure_to_p2_84_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_84_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_84_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_84_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_84_channel": str( + payload["pipeline_closure_to_p2_84_channel"] + ), + "workflow_pipeline_closure_to_p2_84_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_84_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_84_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_84_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_84_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-84 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-83-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py", + help="P2-84 closure verifier script path (P2-83 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-83-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py", + help="P2-84 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_83_gate_script = project_root / Path( + args.pipeline_closure_p2_83_gate_script + ) + pipeline_closure_p2_83_runtime_test = project_root / Path( + args.pipeline_closure_p2_83_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_84_payload( + project_root=project_root, + pipeline_closure_p2_83_gate_script=pipeline_closure_p2_83_gate_script, + pipeline_closure_p2_83_runtime_test=pipeline_closure_p2_83_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-84-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_84_status={payload['pipeline_closure_to_p2_84_status']} " + f"pipeline_closure_to_p2_84_decision={payload['pipeline_closure_to_p2_84_decision']} " + f"pipeline_closure_to_p2_84_exit_code={payload['pipeline_closure_to_p2_84_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_84_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py new file mode 100644 index 0000000..bd62ce4 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-86 gate for Linux CI workflow pipeline closure to P2-85. + +This script validates that the P2-85 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_85_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_85_missing": '"pipeline_closure_card_id": "P2-85"', + "pipeline_closure_target_card_id_p2_84_missing": '"pipeline_closure_target_card_id": "P2-84"', + "pipeline_closure_status_key_p2_84_missing": "pipeline_closure_to_p2_84_status", + "pipeline_closure_decision_verified_p2_84_missing": "pipeline_closure_to_p2_84_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py" + ), + "pipeline_closure_report_json_p2_84_missing": "ci_workflow_pipeline_closure_p2_84.json", + "pipeline_closure_report_markdown_p2_84_missing": "ci_workflow_pipeline_closure_p2_84.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_85_payload( + *, + project_root: Path, + pipeline_closure_p2_84_gate_script: Path, + pipeline_closure_p2_84_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_84_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_84_gate_script)) + p2_85_closure_text = "" + else: + p2_85_closure_text = _read_text(pipeline_closure_p2_84_gate_script) + + if not pipeline_closure_p2_84_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_84_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_85_CLOSURE_PATTERNS.items(): + if pattern not in p2_85_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_85_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_85_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_85_verified", + "pipeline_closure_p2_85_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-86 " + f"pipeline_closure_to_p2_85_status={status} " + f"pipeline_closure_to_p2_85_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_84_gate_script": str( + pipeline_closure_p2_84_gate_script.resolve() + ), + "source_pipeline_closure_p2_84_runtime_test": str( + pipeline_closure_p2_84_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-86", + "pipeline_closure_target_card_id": "P2-85", + "pipeline_closure_to_p2_85_status": status, + "pipeline_closure_to_p2_85_decision": decision, + "pipeline_closure_to_p2_85_exit_code": exit_code, + "pipeline_closure_to_p2_85_should_proceed": should_proceed, + "pipeline_closure_to_p2_85_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_85_channel": channel, + "pipeline_closure_to_p2_85_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_85_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-85 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_85_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_85_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_85_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_85_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_85_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_85_channel']}`" + ), + ( + "- Source P2-85 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_84_gate_script']}`" + ), + ( + "- Source P2-85 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_84_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_85_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_85_status": str( + payload["pipeline_closure_to_p2_85_status"] + ), + "workflow_pipeline_closure_to_p2_85_decision": str( + payload["pipeline_closure_to_p2_85_decision"] + ), + "workflow_pipeline_closure_to_p2_85_exit_code": str( + payload["pipeline_closure_to_p2_85_exit_code"] + ), + "workflow_pipeline_closure_to_p2_85_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_85_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_85_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_85_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_85_channel": str( + payload["pipeline_closure_to_p2_85_channel"] + ), + "workflow_pipeline_closure_to_p2_85_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_85_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_85_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_85_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_85_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-85 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-84-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py", + help="P2-85 closure verifier script path (P2-84 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-84-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py", + help="P2-85 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_84_gate_script = project_root / Path( + args.pipeline_closure_p2_84_gate_script + ) + pipeline_closure_p2_84_runtime_test = project_root / Path( + args.pipeline_closure_p2_84_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_85_payload( + project_root=project_root, + pipeline_closure_p2_84_gate_script=pipeline_closure_p2_84_gate_script, + pipeline_closure_p2_84_runtime_test=pipeline_closure_p2_84_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-85-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_85_status={payload['pipeline_closure_to_p2_85_status']} " + f"pipeline_closure_to_p2_85_decision={payload['pipeline_closure_to_p2_85_decision']} " + f"pipeline_closure_to_p2_85_exit_code={payload['pipeline_closure_to_p2_85_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_85_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py new file mode 100644 index 0000000..9c286ad --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py @@ -0,0 +1,352 @@ +"""Phase 2 card P2-87 gate for Linux CI workflow pipeline closure to P2-86. + +This script validates that the P2-86 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_86_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_86_missing": '"pipeline_closure_card_id": "P2-86"', + "pipeline_closure_target_card_id_p2_85_missing": '"pipeline_closure_target_card_id": "P2-85"', + "pipeline_closure_status_key_p2_85_missing": "pipeline_closure_to_p2_85_status", + "pipeline_closure_decision_verified_p2_85_missing": "pipeline_closure_to_p2_85_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py" + ), + "pipeline_closure_report_json_p2_85_missing": "ci_workflow_pipeline_closure_p2_85.json", + "pipeline_closure_report_markdown_p2_85_missing": "ci_workflow_pipeline_closure_p2_85.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_86_payload( + *, + project_root: Path, + pipeline_closure_p2_85_gate_script: Path, + pipeline_closure_p2_85_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_85_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_85_gate_script)) + p2_86_closure_text = "" + else: + p2_86_closure_text = _read_text(pipeline_closure_p2_85_gate_script) + + if not pipeline_closure_p2_85_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_85_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_86_CLOSURE_PATTERNS.items(): + if pattern not in p2_86_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_86_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_86_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_86_verified", + "pipeline_closure_p2_86_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-87 " + f"pipeline_closure_to_p2_86_status={status} " + f"pipeline_closure_to_p2_86_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_85_gate_script": str( + pipeline_closure_p2_85_gate_script.resolve() + ), + "source_pipeline_closure_p2_85_runtime_test": str( + pipeline_closure_p2_85_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-87", + "pipeline_closure_target_card_id": "P2-86", + "pipeline_closure_to_p2_86_status": status, + "pipeline_closure_to_p2_86_decision": decision, + "pipeline_closure_to_p2_86_exit_code": exit_code, + "pipeline_closure_to_p2_86_should_proceed": should_proceed, + "pipeline_closure_to_p2_86_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_86_channel": channel, + "pipeline_closure_to_p2_86_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_86_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-86 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_86_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_86_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_86_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_86_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_86_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_86_channel']}`" + ), + ( + "- Source P2-86 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_85_gate_script']}`" + ), + ( + "- Source P2-85 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_85_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_86_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_86_status": str( + payload["pipeline_closure_to_p2_86_status"] + ), + "workflow_pipeline_closure_to_p2_86_decision": str( + payload["pipeline_closure_to_p2_86_decision"] + ), + "workflow_pipeline_closure_to_p2_86_exit_code": str( + payload["pipeline_closure_to_p2_86_exit_code"] + ), + "workflow_pipeline_closure_to_p2_86_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_86_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_86_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_86_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_86_channel": str( + payload["pipeline_closure_to_p2_86_channel"] + ), + "workflow_pipeline_closure_to_p2_86_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_86_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_86_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_86_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_86_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-86 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-85-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py", + help="P2-86 closure verifier script path (P2-85 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-85-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py", + help="P2-86 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_85_gate_script = project_root / Path( + args.pipeline_closure_p2_85_gate_script + ) + pipeline_closure_p2_85_runtime_test = project_root / Path( + args.pipeline_closure_p2_85_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_86_payload( + project_root=project_root, + pipeline_closure_p2_85_gate_script=pipeline_closure_p2_85_gate_script, + pipeline_closure_p2_85_runtime_test=pipeline_closure_p2_85_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-86-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_86_status={payload['pipeline_closure_to_p2_86_status']} " + f"pipeline_closure_to_p2_86_decision={payload['pipeline_closure_to_p2_86_decision']} " + f"pipeline_closure_to_p2_86_exit_code={payload['pipeline_closure_to_p2_86_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_86_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py new file mode 100644 index 0000000..ba4c0d3 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-88 gate for Linux CI workflow pipeline closure to P2-87. + +This script validates that the P2-87 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_87_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_87_missing": '"pipeline_closure_card_id": "P2-87"', + "pipeline_closure_target_card_id_p2_86_missing": '"pipeline_closure_target_card_id": "P2-86"', + "pipeline_closure_status_key_p2_86_missing": "pipeline_closure_to_p2_86_status", + "pipeline_closure_decision_verified_p2_86_missing": "pipeline_closure_to_p2_86_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py" + ), + "pipeline_closure_report_json_p2_86_missing": "ci_workflow_pipeline_closure_p2_86.json", + "pipeline_closure_report_markdown_p2_86_missing": "ci_workflow_pipeline_closure_p2_86.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_87_payload( + *, + project_root: Path, + pipeline_closure_p2_86_gate_script: Path, + pipeline_closure_p2_86_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_86_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_86_gate_script)) + p2_87_closure_text = "" + else: + p2_87_closure_text = _read_text(pipeline_closure_p2_86_gate_script) + + if not pipeline_closure_p2_86_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_86_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_87_CLOSURE_PATTERNS.items(): + if pattern not in p2_87_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_87_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_87_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_87_verified", + "pipeline_closure_p2_87_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-88 " + f"pipeline_closure_to_p2_87_status={status} " + f"pipeline_closure_to_p2_87_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_86_gate_script": str( + pipeline_closure_p2_86_gate_script.resolve() + ), + "source_pipeline_closure_p2_86_runtime_test": str( + pipeline_closure_p2_86_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-88", + "pipeline_closure_target_card_id": "P2-87", + "pipeline_closure_to_p2_87_status": status, + "pipeline_closure_to_p2_87_decision": decision, + "pipeline_closure_to_p2_87_exit_code": exit_code, + "pipeline_closure_to_p2_87_should_proceed": should_proceed, + "pipeline_closure_to_p2_87_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_87_channel": channel, + "pipeline_closure_to_p2_87_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_87_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-87 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_87_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_87_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_87_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_87_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_87_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_87_channel']}`" + ), + ( + "- Source P2-87 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_86_gate_script']}`" + ), + ( + "- Source P2-86 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_86_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_87_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_87_status": str( + payload["pipeline_closure_to_p2_87_status"] + ), + "workflow_pipeline_closure_to_p2_87_decision": str( + payload["pipeline_closure_to_p2_87_decision"] + ), + "workflow_pipeline_closure_to_p2_87_exit_code": str( + payload["pipeline_closure_to_p2_87_exit_code"] + ), + "workflow_pipeline_closure_to_p2_87_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_87_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_87_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_87_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_87_channel": str( + payload["pipeline_closure_to_p2_87_channel"] + ), + "workflow_pipeline_closure_to_p2_87_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_87_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_87_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_87_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_87_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-87 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-86-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py", + help="P2-87 closure verifier script path (P2-86 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-86-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py", + help="P2-87 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_86_gate_script = project_root / Path( + args.pipeline_closure_p2_86_gate_script + ) + pipeline_closure_p2_86_runtime_test = project_root / Path( + args.pipeline_closure_p2_86_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_87_payload( + project_root=project_root, + pipeline_closure_p2_86_gate_script=pipeline_closure_p2_86_gate_script, + pipeline_closure_p2_86_runtime_test=pipeline_closure_p2_86_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-87-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_87_status={payload['pipeline_closure_to_p2_87_status']} " + f"pipeline_closure_to_p2_87_decision={payload['pipeline_closure_to_p2_87_decision']} " + f"pipeline_closure_to_p2_87_exit_code={payload['pipeline_closure_to_p2_87_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_87_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py new file mode 100644 index 0000000..e7075c3 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-89 gate for Linux CI workflow pipeline closure to P2-88. + +This script validates that the P2-88 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_88_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_88_missing": '"pipeline_closure_card_id": "P2-88"', + "pipeline_closure_target_card_id_p2_87_missing": '"pipeline_closure_target_card_id": "P2-87"', + "pipeline_closure_status_key_p2_87_missing": "pipeline_closure_to_p2_87_status", + "pipeline_closure_decision_verified_p2_87_missing": "pipeline_closure_to_p2_87_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_87_missing": "ci_workflow_pipeline_closure_p2_87.json", + "pipeline_closure_report_markdown_p2_87_missing": "ci_workflow_pipeline_closure_p2_87.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_88_payload( + *, + project_root: Path, + pipeline_closure_p2_87_gate_script: Path, + pipeline_closure_p2_87_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_87_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_87_gate_script)) + p2_88_closure_text = "" + else: + p2_88_closure_text = _read_text(pipeline_closure_p2_87_gate_script) + + if not pipeline_closure_p2_87_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_87_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_88_CLOSURE_PATTERNS.items(): + if pattern not in p2_88_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_88_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_88_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_88_verified", + "pipeline_closure_p2_88_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-89 " + f"pipeline_closure_to_p2_88_status={status} " + f"pipeline_closure_to_p2_88_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_87_gate_script": str( + pipeline_closure_p2_87_gate_script.resolve() + ), + "source_pipeline_closure_p2_87_runtime_test": str( + pipeline_closure_p2_87_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-89", + "pipeline_closure_target_card_id": "P2-88", + "pipeline_closure_to_p2_88_status": status, + "pipeline_closure_to_p2_88_decision": decision, + "pipeline_closure_to_p2_88_exit_code": exit_code, + "pipeline_closure_to_p2_88_should_proceed": should_proceed, + "pipeline_closure_to_p2_88_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_88_channel": channel, + "pipeline_closure_to_p2_88_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_88_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-88 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_88_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_88_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_88_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_88_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_88_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_88_channel']}`" + ), + ( + "- Source P2-88 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_87_gate_script']}`" + ), + ( + "- Source P2-88 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_87_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_88_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_88_status": str( + payload["pipeline_closure_to_p2_88_status"] + ), + "workflow_pipeline_closure_to_p2_88_decision": str( + payload["pipeline_closure_to_p2_88_decision"] + ), + "workflow_pipeline_closure_to_p2_88_exit_code": str( + payload["pipeline_closure_to_p2_88_exit_code"] + ), + "workflow_pipeline_closure_to_p2_88_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_88_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_88_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_88_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_88_channel": str( + payload["pipeline_closure_to_p2_88_channel"] + ), + "workflow_pipeline_closure_to_p2_88_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_88_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_88_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_88_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_88_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-88 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-87-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py", + help="P2-88 closure verifier script path (P2-87 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-87-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py", + help="P2-88 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_87_gate_script = project_root / Path( + args.pipeline_closure_p2_87_gate_script + ) + pipeline_closure_p2_87_runtime_test = project_root / Path( + args.pipeline_closure_p2_87_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_88_payload( + project_root=project_root, + pipeline_closure_p2_87_gate_script=pipeline_closure_p2_87_gate_script, + pipeline_closure_p2_87_runtime_test=pipeline_closure_p2_87_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-88-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_88_status={payload['pipeline_closure_to_p2_88_status']} " + f"pipeline_closure_to_p2_88_decision={payload['pipeline_closure_to_p2_88_decision']} " + f"pipeline_closure_to_p2_88_exit_code={payload['pipeline_closure_to_p2_88_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_88_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py new file mode 100644 index 0000000..fd8a07a --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-90 gate for Linux CI workflow pipeline closure to P2-89. + +This script validates that the P2-89 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_89_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_89_missing": '"pipeline_closure_card_id": "P2-89"', + "pipeline_closure_target_card_id_p2_88_missing": '"pipeline_closure_target_card_id": "P2-88"', + "pipeline_closure_status_key_p2_88_missing": "pipeline_closure_to_p2_88_status", + "pipeline_closure_decision_verified_p2_88_missing": "pipeline_closure_to_p2_88_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_88_missing": "ci_workflow_pipeline_closure_p2_88.json", + "pipeline_closure_report_markdown_p2_88_missing": "ci_workflow_pipeline_closure_p2_88.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_89_payload( + *, + project_root: Path, + pipeline_closure_p2_88_gate_script: Path, + pipeline_closure_p2_88_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_88_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_88_gate_script)) + p2_89_closure_text = "" + else: + p2_89_closure_text = _read_text(pipeline_closure_p2_88_gate_script) + + if not pipeline_closure_p2_88_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_88_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_89_CLOSURE_PATTERNS.items(): + if pattern not in p2_89_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_89_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_89_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_89_verified", + "pipeline_closure_p2_89_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-90 " + f"pipeline_closure_to_p2_89_status={status} " + f"pipeline_closure_to_p2_89_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_88_gate_script": str( + pipeline_closure_p2_88_gate_script.resolve() + ), + "source_pipeline_closure_p2_88_runtime_test": str( + pipeline_closure_p2_88_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-90", + "pipeline_closure_target_card_id": "P2-89", + "pipeline_closure_to_p2_89_status": status, + "pipeline_closure_to_p2_89_decision": decision, + "pipeline_closure_to_p2_89_exit_code": exit_code, + "pipeline_closure_to_p2_89_should_proceed": should_proceed, + "pipeline_closure_to_p2_89_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_89_channel": channel, + "pipeline_closure_to_p2_89_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_89_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-89 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_89_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_89_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_89_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_89_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_89_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_89_channel']}`" + ), + ( + "- Source P2-89 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_88_gate_script']}`" + ), + ( + "- Source P2-89 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_88_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_89_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_89_status": str( + payload["pipeline_closure_to_p2_89_status"] + ), + "workflow_pipeline_closure_to_p2_89_decision": str( + payload["pipeline_closure_to_p2_89_decision"] + ), + "workflow_pipeline_closure_to_p2_89_exit_code": str( + payload["pipeline_closure_to_p2_89_exit_code"] + ), + "workflow_pipeline_closure_to_p2_89_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_89_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_89_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_89_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_89_channel": str( + payload["pipeline_closure_to_p2_89_channel"] + ), + "workflow_pipeline_closure_to_p2_89_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_89_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_89_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_89_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_89_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-89 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-88-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py", + help="P2-89 closure verifier script path (P2-88 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-88-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py", + help="P2-89 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_88_gate_script = project_root / Path( + args.pipeline_closure_p2_88_gate_script + ) + pipeline_closure_p2_88_runtime_test = project_root / Path( + args.pipeline_closure_p2_88_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_89_payload( + project_root=project_root, + pipeline_closure_p2_88_gate_script=pipeline_closure_p2_88_gate_script, + pipeline_closure_p2_88_runtime_test=pipeline_closure_p2_88_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-89-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_89_status={payload['pipeline_closure_to_p2_89_status']} " + f"pipeline_closure_to_p2_89_decision={payload['pipeline_closure_to_p2_89_decision']} " + f"pipeline_closure_to_p2_89_exit_code={payload['pipeline_closure_to_p2_89_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_89_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py new file mode 100644 index 0000000..fa89d78 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py @@ -0,0 +1,352 @@ +"""Phase 2 card P2-91 gate for Linux CI workflow pipeline closure to P2-90. + +This script validates that the P2-90 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_90_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_90_missing": '"pipeline_closure_card_id": "P2-90"', + "pipeline_closure_target_card_id_p2_89_missing": '"pipeline_closure_target_card_id": "P2-89"', + "pipeline_closure_status_key_p2_89_missing": "pipeline_closure_to_p2_89_status", + "pipeline_closure_decision_verified_p2_89_missing": "pipeline_closure_to_p2_89_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_89_missing": "ci_workflow_pipeline_closure_p2_89.json", + "pipeline_closure_report_markdown_p2_89_missing": "ci_workflow_pipeline_closure_p2_89.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_90_payload( + *, + project_root: Path, + pipeline_closure_p2_89_gate_script: Path, + pipeline_closure_p2_89_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_89_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_89_gate_script)) + p2_90_closure_text = "" + else: + p2_90_closure_text = _read_text(pipeline_closure_p2_89_gate_script) + + if not pipeline_closure_p2_89_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_89_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_90_CLOSURE_PATTERNS.items(): + if pattern not in p2_90_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_90_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_90_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_90_verified", + "pipeline_closure_p2_90_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-91 " + f"pipeline_closure_to_p2_90_status={status} " + f"pipeline_closure_to_p2_90_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_89_gate_script": str( + pipeline_closure_p2_89_gate_script.resolve() + ), + "source_pipeline_closure_p2_89_runtime_test": str( + pipeline_closure_p2_89_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-91", + "pipeline_closure_target_card_id": "P2-90", + "pipeline_closure_to_p2_90_status": status, + "pipeline_closure_to_p2_90_decision": decision, + "pipeline_closure_to_p2_90_exit_code": exit_code, + "pipeline_closure_to_p2_90_should_proceed": should_proceed, + "pipeline_closure_to_p2_90_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_90_channel": channel, + "pipeline_closure_to_p2_90_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_90_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-90 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_90_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_90_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_90_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_90_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_90_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_90_channel']}`" + ), + ( + "- Source P2-90 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_89_gate_script']}`" + ), + ( + "- Source P2-90 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_89_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_90_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_90_status": str( + payload["pipeline_closure_to_p2_90_status"] + ), + "workflow_pipeline_closure_to_p2_90_decision": str( + payload["pipeline_closure_to_p2_90_decision"] + ), + "workflow_pipeline_closure_to_p2_90_exit_code": str( + payload["pipeline_closure_to_p2_90_exit_code"] + ), + "workflow_pipeline_closure_to_p2_90_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_90_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_90_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_90_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_90_channel": str( + payload["pipeline_closure_to_p2_90_channel"] + ), + "workflow_pipeline_closure_to_p2_90_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_90_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_90_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_90_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_90_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-90 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-89-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py", + help="P2-90 closure verifier script path (P2-89 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-89-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py", + help="P2-90 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_89_gate_script = project_root / Path( + args.pipeline_closure_p2_89_gate_script + ) + pipeline_closure_p2_89_runtime_test = project_root / Path( + args.pipeline_closure_p2_89_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_90_payload( + project_root=project_root, + pipeline_closure_p2_89_gate_script=pipeline_closure_p2_89_gate_script, + pipeline_closure_p2_89_runtime_test=pipeline_closure_p2_89_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-90-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_90_status={payload['pipeline_closure_to_p2_90_status']} " + f"pipeline_closure_to_p2_90_decision={payload['pipeline_closure_to_p2_90_decision']} " + f"pipeline_closure_to_p2_90_exit_code={payload['pipeline_closure_to_p2_90_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_90_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py new file mode 100644 index 0000000..ac1fb03 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py @@ -0,0 +1,353 @@ +"""Phase 2 card P2-92 gate for Linux CI workflow pipeline closure to P2-91. + +This script validates that the P2-91 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_91_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_91_missing": '"pipeline_closure_card_id": "P2-91"', + "pipeline_closure_target_card_id_p2_90_missing": '"pipeline_closure_target_card_id": "P2-90"', + "pipeline_closure_status_key_p2_90_missing": "pipeline_closure_to_p2_90_status", + "pipeline_closure_decision_verified_p2_90_missing": "pipeline_closure_to_p2_90_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_90_missing": "ci_workflow_pipeline_closure_p2_90.json", + "pipeline_closure_report_markdown_p2_90_missing": "ci_workflow_pipeline_closure_p2_90.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_91_payload( + *, + project_root: Path, + pipeline_closure_p2_90_gate_script: Path, + pipeline_closure_p2_90_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_90_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_90_gate_script)) + p2_91_closure_text = "" + else: + p2_91_closure_text = _read_text(pipeline_closure_p2_90_gate_script) + + if not pipeline_closure_p2_90_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_90_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_91_CLOSURE_PATTERNS.items(): + if pattern not in p2_91_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_91_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_91_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_91_verified", + "pipeline_closure_p2_91_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-92 " + f"pipeline_closure_to_p2_91_status={status} " + f"pipeline_closure_to_p2_91_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_90_gate_script": str( + pipeline_closure_p2_90_gate_script.resolve() + ), + "source_pipeline_closure_p2_90_runtime_test": str( + pipeline_closure_p2_90_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-92", + "pipeline_closure_target_card_id": "P2-91", + "pipeline_closure_to_p2_91_status": status, + "pipeline_closure_to_p2_91_decision": decision, + "pipeline_closure_to_p2_91_exit_code": exit_code, + "pipeline_closure_to_p2_91_should_proceed": should_proceed, + "pipeline_closure_to_p2_91_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_91_channel": channel, + "pipeline_closure_to_p2_91_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_91_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-91 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_91_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_91_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_91_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_91_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_91_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_91_channel']}`" + ), + ( + "- Source P2-91 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_90_gate_script']}`" + ), + ( + "- Source P2-91 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_90_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_91_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_91_status": str( + payload["pipeline_closure_to_p2_91_status"] + ), + "workflow_pipeline_closure_to_p2_91_decision": str( + payload["pipeline_closure_to_p2_91_decision"] + ), + "workflow_pipeline_closure_to_p2_91_exit_code": str( + payload["pipeline_closure_to_p2_91_exit_code"] + ), + "workflow_pipeline_closure_to_p2_91_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_91_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_91_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_91_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_91_channel": str( + payload["pipeline_closure_to_p2_91_channel"] + ), + "workflow_pipeline_closure_to_p2_91_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_91_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_91_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_91_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_91_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-91 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-90-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py", + help="P2-91 closure verifier script path (P2-90 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-90-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py", + help="P2-91 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_91.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_91.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_90_gate_script = project_root / Path( + args.pipeline_closure_p2_90_gate_script + ) + pipeline_closure_p2_90_runtime_test = project_root / Path( + args.pipeline_closure_p2_90_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_91_payload( + project_root=project_root, + pipeline_closure_p2_90_gate_script=pipeline_closure_p2_90_gate_script, + pipeline_closure_p2_90_runtime_test=pipeline_closure_p2_90_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-91-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_91_status={payload['pipeline_closure_to_p2_91_status']} " + f"pipeline_closure_to_p2_91_decision={payload['pipeline_closure_to_p2_91_decision']} " + f"pipeline_closure_to_p2_91_exit_code={payload['pipeline_closure_to_p2_91_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_91_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py new file mode 100644 index 0000000..2dc0cc2 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-93 gate for Linux CI workflow pipeline closure to P2-92. + +This script validates that the P2-92 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_92_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_92_missing": '"pipeline_closure_card_id": "P2-92"', + "pipeline_closure_target_card_id_p2_91_missing": '"pipeline_closure_target_card_id": "P2-91"', + "pipeline_closure_status_key_p2_91_missing": "pipeline_closure_to_p2_91_status", + "pipeline_closure_decision_verified_p2_91_missing": "pipeline_closure_to_p2_91_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_91_missing": "ci_workflow_pipeline_closure_p2_91.json", + "pipeline_closure_report_markdown_p2_91_missing": "ci_workflow_pipeline_closure_p2_91.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_92_payload( + *, + project_root: Path, + pipeline_closure_p2_91_gate_script: Path, + pipeline_closure_p2_91_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_91_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_91_gate_script)) + p2_92_closure_text = "" + else: + p2_92_closure_text = _read_text(pipeline_closure_p2_91_gate_script) + + if not pipeline_closure_p2_91_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_91_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_92_CLOSURE_PATTERNS.items(): + if pattern not in p2_92_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_92_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_92_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_92_verified", + "pipeline_closure_p2_92_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-93 " + f"pipeline_closure_to_p2_92_status={status} " + f"pipeline_closure_to_p2_92_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_91_gate_script": str( + pipeline_closure_p2_91_gate_script.resolve() + ), + "source_pipeline_closure_p2_91_runtime_test": str( + pipeline_closure_p2_91_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-93", + "pipeline_closure_target_card_id": "P2-92", + "pipeline_closure_to_p2_92_status": status, + "pipeline_closure_to_p2_92_decision": decision, + "pipeline_closure_to_p2_92_exit_code": exit_code, + "pipeline_closure_to_p2_92_should_proceed": should_proceed, + "pipeline_closure_to_p2_92_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_92_channel": channel, + "pipeline_closure_to_p2_92_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_92_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-92 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_92_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_92_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_92_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_92_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_92_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_92_channel']}`" + ), + ( + "- Source P2-92 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_91_gate_script']}`" + ), + ( + "- Source P2-92 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_91_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_92_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_92_status": str( + payload["pipeline_closure_to_p2_92_status"] + ), + "workflow_pipeline_closure_to_p2_92_decision": str( + payload["pipeline_closure_to_p2_92_decision"] + ), + "workflow_pipeline_closure_to_p2_92_exit_code": str( + payload["pipeline_closure_to_p2_92_exit_code"] + ), + "workflow_pipeline_closure_to_p2_92_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_92_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_92_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_92_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_92_channel": str( + payload["pipeline_closure_to_p2_92_channel"] + ), + "workflow_pipeline_closure_to_p2_92_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_92_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_92_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_92_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_92_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-92 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-91-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py", + help="P2-92 closure verifier script path (P2-91 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-91-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py", + help="P2-92 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_92.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_92.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_91_gate_script = project_root / Path( + args.pipeline_closure_p2_91_gate_script + ) + pipeline_closure_p2_91_runtime_test = project_root / Path( + args.pipeline_closure_p2_91_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_92_payload( + project_root=project_root, + pipeline_closure_p2_91_gate_script=pipeline_closure_p2_91_gate_script, + pipeline_closure_p2_91_runtime_test=pipeline_closure_p2_91_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-92-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_92_status={payload['pipeline_closure_to_p2_92_status']} " + f"pipeline_closure_to_p2_92_decision={payload['pipeline_closure_to_p2_92_decision']} " + f"pipeline_closure_to_p2_92_exit_code={payload['pipeline_closure_to_p2_92_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_92_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py new file mode 100644 index 0000000..5605864 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py @@ -0,0 +1,351 @@ +"""Phase 2 card P2-94 gate for Linux CI workflow pipeline closure to P2-93. + +This script validates that the P2-93 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_93_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_93_missing": '"pipeline_closure_card_id": "P2-93"', + "pipeline_closure_target_card_id_p2_92_missing": '"pipeline_closure_target_card_id": "P2-92"', + "pipeline_closure_status_key_p2_92_missing": "pipeline_closure_to_p2_92_status", + "pipeline_closure_decision_verified_p2_92_missing": "pipeline_closure_to_p2_92_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_92_missing": "ci_workflow_pipeline_closure_p2_92.json", + "pipeline_closure_report_markdown_p2_92_missing": "ci_workflow_pipeline_closure_p2_92.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_93_payload( + *, + project_root: Path, + pipeline_closure_p2_92_gate_script: Path, + pipeline_closure_p2_92_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_92_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_92_gate_script)) + p2_93_closure_text = "" + else: + p2_93_closure_text = _read_text(pipeline_closure_p2_92_gate_script) + + if not pipeline_closure_p2_92_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_92_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_93_CLOSURE_PATTERNS.items(): + if pattern not in p2_93_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_93_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_93_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_93_verified", + "pipeline_closure_p2_93_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-94 " + f"pipeline_closure_to_p2_93_status={status} " + f"pipeline_closure_to_p2_93_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_92_gate_script": str( + pipeline_closure_p2_92_gate_script.resolve() + ), + "source_pipeline_closure_p2_92_runtime_test": str( + pipeline_closure_p2_92_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-94", + "pipeline_closure_target_card_id": "P2-93", + "pipeline_closure_to_p2_93_status": status, + "pipeline_closure_to_p2_93_decision": decision, + "pipeline_closure_to_p2_93_exit_code": exit_code, + "pipeline_closure_to_p2_93_should_proceed": should_proceed, + "pipeline_closure_to_p2_93_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_93_channel": channel, + "pipeline_closure_to_p2_93_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_93_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-93 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_93_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_93_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_93_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_93_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_93_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_93_channel']}`" + ), + ( + "- Source P2-93 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_92_gate_script']}`" + ), + ( + "- Source P2-93 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_92_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_93_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_93_status": str( + payload["pipeline_closure_to_p2_93_status"] + ), + "workflow_pipeline_closure_to_p2_93_decision": str( + payload["pipeline_closure_to_p2_93_decision"] + ), + "workflow_pipeline_closure_to_p2_93_exit_code": str( + payload["pipeline_closure_to_p2_93_exit_code"] + ), + "workflow_pipeline_closure_to_p2_93_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_93_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_93_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_93_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_93_channel": str( + payload["pipeline_closure_to_p2_93_channel"] + ), + "workflow_pipeline_closure_to_p2_93_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_93_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_93_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_93_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_93_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-93 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-92-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py", + help="P2-93 closure verifier script path (P2-92 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-92-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py", + help="P2-93 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_93.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_93.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_92_gate_script = project_root / Path( + args.pipeline_closure_p2_92_gate_script + ) + pipeline_closure_p2_92_runtime_test = project_root / Path( + args.pipeline_closure_p2_92_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_93_payload( + project_root=project_root, + pipeline_closure_p2_92_gate_script=pipeline_closure_p2_92_gate_script, + pipeline_closure_p2_92_runtime_test=pipeline_closure_p2_92_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-93-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_93_status={payload['pipeline_closure_to_p2_93_status']} " + f"pipeline_closure_to_p2_93_decision={payload['pipeline_closure_to_p2_93_decision']} " + f"pipeline_closure_to_p2_93_exit_code={payload['pipeline_closure_to_p2_93_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_93_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py new file mode 100644 index 0000000..e92d7d0 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py @@ -0,0 +1,352 @@ +"""Phase 2 card P2-95 gate for Linux CI workflow pipeline closure to P2-94. + +This script validates that the P2-94 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_94_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_94_missing": '"pipeline_closure_card_id": "P2-94"', + "pipeline_closure_target_card_id_p2_93_missing": '"pipeline_closure_target_card_id": "P2-93"', + "pipeline_closure_status_key_p2_93_missing": "pipeline_closure_to_p2_93_status", + "pipeline_closure_decision_verified_p2_93_missing": "pipeline_closure_to_p2_93_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_93_missing": "ci_workflow_pipeline_closure_p2_93.json", + "pipeline_closure_report_markdown_p2_93_missing": "ci_workflow_pipeline_closure_p2_93.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_94_payload( + *, + project_root: Path, + pipeline_closure_p2_93_gate_script: Path, + pipeline_closure_p2_93_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_93_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_93_gate_script)) + p2_94_closure_text = "" + else: + p2_94_closure_text = _read_text(pipeline_closure_p2_93_gate_script) + + if not pipeline_closure_p2_93_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_93_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_94_CLOSURE_PATTERNS.items(): + if pattern not in p2_94_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_94_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_94_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_94_verified", + "pipeline_closure_p2_94_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-95 " + f"pipeline_closure_to_p2_94_status={status} " + f"pipeline_closure_to_p2_94_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_93_gate_script": str( + pipeline_closure_p2_93_gate_script.resolve() + ), + "source_pipeline_closure_p2_93_runtime_test": str( + pipeline_closure_p2_93_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-95", + "pipeline_closure_target_card_id": "P2-94", + "pipeline_closure_to_p2_94_status": status, + "pipeline_closure_to_p2_94_decision": decision, + "pipeline_closure_to_p2_94_exit_code": exit_code, + "pipeline_closure_to_p2_94_should_proceed": should_proceed, + "pipeline_closure_to_p2_94_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_94_channel": channel, + "pipeline_closure_to_p2_94_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_94_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-94 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_94_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_94_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_94_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_94_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_94_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_94_channel']}`" + ), + ( + "- Source P2-94 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_93_gate_script']}`" + ), + ( + "- Source P2-94 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_93_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_94_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_94_status": str( + payload["pipeline_closure_to_p2_94_status"] + ), + "workflow_pipeline_closure_to_p2_94_decision": str( + payload["pipeline_closure_to_p2_94_decision"] + ), + "workflow_pipeline_closure_to_p2_94_exit_code": str( + payload["pipeline_closure_to_p2_94_exit_code"] + ), + "workflow_pipeline_closure_to_p2_94_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_94_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_94_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_94_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_94_channel": str( + payload["pipeline_closure_to_p2_94_channel"] + ), + "workflow_pipeline_closure_to_p2_94_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_94_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_94_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_94_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_94_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-94 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-93-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py", + help="P2-94 closure verifier script path (P2-93 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-93-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py", + help="P2-94 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_94.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_94.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_93_gate_script = project_root / Path( + args.pipeline_closure_p2_93_gate_script + ) + pipeline_closure_p2_93_runtime_test = project_root / Path( + args.pipeline_closure_p2_93_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_94_payload( + project_root=project_root, + pipeline_closure_p2_93_gate_script=pipeline_closure_p2_93_gate_script, + pipeline_closure_p2_93_runtime_test=pipeline_closure_p2_93_runtime_test, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-pipeline-closure-p2-94-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_94_status={payload['pipeline_closure_to_p2_94_status']} " + f"pipeline_closure_to_p2_94_decision={payload['pipeline_closure_to_p2_94_decision']} " + f"pipeline_closure_to_p2_94_exit_code={payload['pipeline_closure_to_p2_94_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_94_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py new file mode 100644 index 0000000..dd35732 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py @@ -0,0 +1,354 @@ +"""Phase 2 card P2-96 gate for Linux CI workflow pipeline closure to P2-95. + +This script validates that the P2-95 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_95_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_95_missing": '"pipeline_closure_card_id": "P2-95"', + "pipeline_closure_target_card_id_p2_94_missing": '"pipeline_closure_target_card_id": "P2-94"', + "pipeline_closure_status_key_p2_94_missing": "pipeline_closure_to_p2_94_status", + "pipeline_closure_decision_verified_p2_94_missing": "pipeline_closure_to_p2_94_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_94_missing": "ci_workflow_pipeline_closure_p2_94.json", + "pipeline_closure_report_markdown_p2_94_missing": "ci_workflow_pipeline_closure_p2_94.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_95_payload( + *, + project_root: Path, + pipeline_closure_p2_94_gate_script: Path, + pipeline_closure_p2_94_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_94_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_94_gate_script)) + p2_95_closure_text = "" + else: + p2_95_closure_text = _read_text(pipeline_closure_p2_94_gate_script) + + if not pipeline_closure_p2_94_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_94_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_95_CLOSURE_PATTERNS.items(): + if pattern not in p2_95_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_95_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_95_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_95_verified", + "pipeline_closure_p2_95_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-96 " + f"pipeline_closure_to_p2_95_status={status} " + f"pipeline_closure_to_p2_95_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_94_gate_script": str( + pipeline_closure_p2_94_gate_script.resolve() + ), + "source_pipeline_closure_p2_94_runtime_test": str( + pipeline_closure_p2_94_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-96", + "pipeline_closure_target_card_id": "P2-95", + "pipeline_closure_to_p2_95_status": status, + "pipeline_closure_to_p2_95_decision": decision, + "pipeline_closure_to_p2_95_exit_code": exit_code, + "pipeline_closure_to_p2_95_should_proceed": should_proceed, + "pipeline_closure_to_p2_95_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_95_channel": channel, + "pipeline_closure_to_p2_95_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_95_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-95 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_95_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_95_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_95_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_95_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_95_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_95_channel']}`" + ), + ( + "- Source P2-95 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_94_gate_script']}`" + ), + ( + "- Source P2-95 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_94_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_95_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_95_status": str( + payload["pipeline_closure_to_p2_95_status"] + ), + "workflow_pipeline_closure_to_p2_95_decision": str( + payload["pipeline_closure_to_p2_95_decision"] + ), + "workflow_pipeline_closure_to_p2_95_exit_code": str( + payload["pipeline_closure_to_p2_95_exit_code"] + ), + "workflow_pipeline_closure_to_p2_95_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_95_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_95_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_95_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_95_channel": str( + payload["pipeline_closure_to_p2_95_channel"] + ), + "workflow_pipeline_closure_to_p2_95_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_95_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_95_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_95_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_95_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-95 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-94-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py", + help="P2-95 closure verifier script path (P2-94 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-94-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py", + help="P2-95 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_95.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_95.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_94_gate_script = project_root / Path( + args.pipeline_closure_p2_94_gate_script + ) + pipeline_closure_p2_94_runtime_test = project_root / Path( + args.pipeline_closure_p2_94_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_95_payload( + project_root=project_root, + pipeline_closure_p2_94_gate_script=pipeline_closure_p2_94_gate_script, + pipeline_closure_p2_94_runtime_test=pipeline_closure_p2_94_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-p2-95-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_95_status={payload['pipeline_closure_to_p2_95_status']} " + f"pipeline_closure_to_p2_95_decision={payload['pipeline_closure_to_p2_95_decision']} " + f"pipeline_closure_to_p2_95_exit_code={payload['pipeline_closure_to_p2_95_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_95_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py new file mode 100644 index 0000000..81ecb4a --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py @@ -0,0 +1,355 @@ +"""Phase 2 card P2-97 gate for Linux CI workflow pipeline closure to P2-96. + +This script validates that the P2-96 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_96_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_96_missing": '"pipeline_closure_card_id": "P2-96"', + "pipeline_closure_target_card_id_p2_95_missing": '"pipeline_closure_target_card_id": "P2-95"', + "pipeline_closure_status_key_p2_95_missing": "pipeline_closure_to_p2_95_status", + "pipeline_closure_decision_verified_p2_95_missing": "pipeline_closure_to_p2_95_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_95_missing": "ci_workflow_pipeline_closure_p2_95.json", + "pipeline_closure_report_markdown_p2_95_missing": "ci_workflow_pipeline_closure_p2_95.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_96_payload( + *, + project_root: Path, + pipeline_closure_p2_95_gate_script: Path, + pipeline_closure_p2_95_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_95_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_95_gate_script)) + p2_96_closure_text = "" + else: + p2_96_closure_text = _read_text(pipeline_closure_p2_95_gate_script) + + if not pipeline_closure_p2_95_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_95_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_96_CLOSURE_PATTERNS.items(): + if pattern not in p2_96_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_96_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_96_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_96_verified", + "pipeline_closure_p2_96_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-97 " + f"pipeline_closure_to_p2_96_status={status} " + f"pipeline_closure_to_p2_96_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_95_gate_script": str( + pipeline_closure_p2_95_gate_script.resolve() + ), + "source_pipeline_closure_p2_95_runtime_test": str( + pipeline_closure_p2_95_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-97", + "pipeline_closure_target_card_id": "P2-96", + "pipeline_closure_to_p2_96_status": status, + "pipeline_closure_to_p2_96_decision": decision, + "pipeline_closure_to_p2_96_exit_code": exit_code, + "pipeline_closure_to_p2_96_should_proceed": should_proceed, + "pipeline_closure_to_p2_96_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_96_channel": channel, + "pipeline_closure_to_p2_96_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_96_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-96 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_96_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_96_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_96_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_96_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_96_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_96_channel']}`" + ), + ( + "- Source P2-96 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_95_gate_script']}`" + ), + ( + "- Source P2-96 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_95_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_96_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_96_status": str( + payload["pipeline_closure_to_p2_96_status"] + ), + "workflow_pipeline_closure_to_p2_96_decision": str( + payload["pipeline_closure_to_p2_96_decision"] + ), + "workflow_pipeline_closure_to_p2_96_exit_code": str( + payload["pipeline_closure_to_p2_96_exit_code"] + ), + "workflow_pipeline_closure_to_p2_96_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_96_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_96_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_96_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_96_channel": str( + payload["pipeline_closure_to_p2_96_channel"] + ), + "workflow_pipeline_closure_to_p2_96_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_96_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_96_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_96_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_96_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-96 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-95-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py", + help="P2-96 closure verifier script path (P2-95 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-95-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py", + help="P2-96 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_95_gate_script = project_root / Path( + args.pipeline_closure_p2_95_gate_script + ) + pipeline_closure_p2_95_runtime_test = project_root / Path( + args.pipeline_closure_p2_95_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_96_payload( + project_root=project_root, + pipeline_closure_p2_95_gate_script=pipeline_closure_p2_95_gate_script, + pipeline_closure_p2_95_runtime_test=pipeline_closure_p2_95_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-p2-96-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_96_status={payload['pipeline_closure_to_p2_96_status']} " + f"pipeline_closure_to_p2_96_decision={payload['pipeline_closure_to_p2_96_decision']} " + f"pipeline_closure_to_p2_96_exit_code={payload['pipeline_closure_to_p2_96_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_96_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py new file mode 100644 index 0000000..d608fd6 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py @@ -0,0 +1,356 @@ +"""Phase 2 card P2-98 gate for Linux CI workflow pipeline closure to P2-97. + +This script validates that the P2-97 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_97_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_97_missing": '"pipeline_closure_card_id": "P2-97"', + "pipeline_closure_target_card_id_p2_96_missing": '"pipeline_closure_target_card_id": "P2-96"', + "pipeline_closure_status_key_p2_96_missing": "pipeline_closure_to_p2_96_status", + "pipeline_closure_decision_verified_p2_96_missing": "pipeline_closure_to_p2_96_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_96_missing": "ci_workflow_pipeline_closure_p2_96.json", + "pipeline_closure_report_markdown_p2_96_missing": "ci_workflow_pipeline_closure_p2_96.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_97_payload( + *, + project_root: Path, + pipeline_closure_p2_96_gate_script: Path, + pipeline_closure_p2_96_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_96_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_96_gate_script)) + p2_97_closure_text = "" + else: + p2_97_closure_text = _read_text(pipeline_closure_p2_96_gate_script) + + if not pipeline_closure_p2_96_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_96_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_97_CLOSURE_PATTERNS.items(): + if pattern not in p2_97_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_97_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_97_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_97_verified", + "pipeline_closure_p2_97_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-98 " + f"pipeline_closure_to_p2_97_status={status} " + f"pipeline_closure_to_p2_97_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_96_gate_script": str( + pipeline_closure_p2_96_gate_script.resolve() + ), + "source_pipeline_closure_p2_96_runtime_test": str( + pipeline_closure_p2_96_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-98", + "pipeline_closure_target_card_id": "P2-97", + "pipeline_closure_to_p2_97_status": status, + "pipeline_closure_to_p2_97_decision": decision, + "pipeline_closure_to_p2_97_exit_code": exit_code, + "pipeline_closure_to_p2_97_should_proceed": should_proceed, + "pipeline_closure_to_p2_97_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_97_channel": channel, + "pipeline_closure_to_p2_97_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_97_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-97 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_97_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_97_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_97_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_97_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_97_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_97_channel']}`" + ), + ( + "- Source P2-97 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_96_gate_script']}`" + ), + ( + "- Source P2-97 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_96_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_97_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_97_status": str( + payload["pipeline_closure_to_p2_97_status"] + ), + "workflow_pipeline_closure_to_p2_97_decision": str( + payload["pipeline_closure_to_p2_97_decision"] + ), + "workflow_pipeline_closure_to_p2_97_exit_code": str( + payload["pipeline_closure_to_p2_97_exit_code"] + ), + "workflow_pipeline_closure_to_p2_97_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_97_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_97_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_97_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_97_channel": str( + payload["pipeline_closure_to_p2_97_channel"] + ), + "workflow_pipeline_closure_to_p2_97_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_97_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_97_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_97_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_97_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-97 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-96-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py", + help="P2-97 closure verifier script path (P2-96 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-96-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py", + help="P2-97 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_97.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_97.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_96_gate_script = project_root / Path( + args.pipeline_closure_p2_96_gate_script + ) + pipeline_closure_p2_96_runtime_test = project_root / Path( + args.pipeline_closure_p2_96_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_97_payload( + project_root=project_root, + pipeline_closure_p2_96_gate_script=pipeline_closure_p2_96_gate_script, + pipeline_closure_p2_96_runtime_test=pipeline_closure_p2_96_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-p2-97-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_97_status={payload['pipeline_closure_to_p2_97_status']} " + f"pipeline_closure_to_p2_97_decision={payload['pipeline_closure_to_p2_97_decision']} " + f"pipeline_closure_to_p2_97_exit_code={payload['pipeline_closure_to_p2_97_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_97_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py new file mode 100644 index 0000000..1f98ee2 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py @@ -0,0 +1,357 @@ +"""Phase 2 card P2-99 gate for Linux CI workflow pipeline closure to P2-98. + +This script validates that the P2-98 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_98_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_98_missing": '"pipeline_closure_card_id": "P2-98"', + "pipeline_closure_target_card_id_p2_97_missing": '"pipeline_closure_target_card_id": "P2-97"', + "pipeline_closure_status_key_p2_97_missing": "pipeline_closure_to_p2_97_status", + "pipeline_closure_decision_verified_p2_97_missing": "pipeline_closure_to_p2_97_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_97_missing": "ci_workflow_pipeline_closure_p2_97.json", + "pipeline_closure_report_markdown_p2_97_missing": "ci_workflow_pipeline_closure_p2_97.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_98_payload( + *, + project_root: Path, + pipeline_closure_p2_97_gate_script: Path, + pipeline_closure_p2_97_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_97_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_97_gate_script)) + p2_98_closure_text = "" + else: + p2_98_closure_text = _read_text(pipeline_closure_p2_97_gate_script) + + if not pipeline_closure_p2_97_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_97_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_98_CLOSURE_PATTERNS.items(): + if pattern not in p2_98_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_98_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_98_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_98_verified", + "pipeline_closure_p2_98_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-99 " + f"pipeline_closure_to_p2_98_status={status} " + f"pipeline_closure_to_p2_98_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_97_gate_script": str( + pipeline_closure_p2_97_gate_script.resolve() + ), + "source_pipeline_closure_p2_97_runtime_test": str( + pipeline_closure_p2_97_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-99", + "pipeline_closure_target_card_id": "P2-98", + "pipeline_closure_to_p2_98_status": status, + "pipeline_closure_to_p2_98_decision": decision, + "pipeline_closure_to_p2_98_exit_code": exit_code, + "pipeline_closure_to_p2_98_should_proceed": should_proceed, + "pipeline_closure_to_p2_98_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_98_channel": channel, + "pipeline_closure_to_p2_98_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_98_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-98 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_98_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_98_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_98_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_98_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_98_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_98_channel']}`" + ), + ( + "- Source P2-98 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_97_gate_script']}`" + ), + ( + "- Source P2-98 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_97_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_98_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_98_status": str( + payload["pipeline_closure_to_p2_98_status"] + ), + "workflow_pipeline_closure_to_p2_98_decision": str( + payload["pipeline_closure_to_p2_98_decision"] + ), + "workflow_pipeline_closure_to_p2_98_exit_code": str( + payload["pipeline_closure_to_p2_98_exit_code"] + ), + "workflow_pipeline_closure_to_p2_98_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_98_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_98_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_98_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_98_channel": str( + payload["pipeline_closure_to_p2_98_channel"] + ), + "workflow_pipeline_closure_to_p2_98_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_98_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_98_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_98_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_98_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-98 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-p2-97-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py", + help="P2-98 closure verifier script path (P2-97 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-p2-97-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py", + help="P2-98 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_98.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_98.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_97_gate_script = project_root / Path( + args.pipeline_closure_p2_97_gate_script + ) + pipeline_closure_p2_97_runtime_test = project_root / Path( + args.pipeline_closure_p2_97_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_98_payload( + project_root=project_root, + pipeline_closure_p2_97_gate_script=pipeline_closure_p2_97_gate_script, + pipeline_closure_p2_97_runtime_test=pipeline_closure_p2_97_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-p2-98-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_98_status={payload['pipeline_closure_to_p2_98_status']} " + f"pipeline_closure_to_p2_98_decision={payload['pipeline_closure_to_p2_98_decision']} " + f"pipeline_closure_to_p2_98_exit_code={payload['pipeline_closure_to_p2_98_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_98_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py new file mode 100644 index 0000000..f5ff8f2 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py @@ -0,0 +1,357 @@ +"""Phase 2 card P2-100 gate for Linux CI workflow pipeline closure to P2-99. + +This script validates that the P2-99 closure verifier contract remains present +and stable for Linux-stage consumers. It emits normalized JSON/Markdown output +and optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_P2_99_CLOSURE_PATTERNS: dict[str, str] = { + "pipeline_closure_card_id_p2_99_missing": '"pipeline_closure_card_id": "P2-99"', + "pipeline_closure_target_card_id_p2_98_missing": '"pipeline_closure_target_card_id": "P2-98"', + "pipeline_closure_status_key_p2_98_missing": "pipeline_closure_to_p2_98_status", + "pipeline_closure_decision_verified_p2_98_missing": "pipeline_closure_to_p2_98_verified", + "pipeline_closure_required_stage_script_ref_missing": ( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + "pipeline_closure_report_json_p2_98_missing": "ci_workflow_pipeline_closure_p2_98.json", + "pipeline_closure_report_markdown_p2_98_missing": "ci_workflow_pipeline_closure_p2_98.md", +} + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"{path}: invalid utf-8 content ({exc})") from exc + + +def build_pipeline_closure_to_p2_99_payload( + *, + project_root: Path, + pipeline_closure_p2_98_gate_script: Path, + pipeline_closure_p2_98_runtime_test: Path, +) -> dict[str, Any]: + structural_issues: list[str] = [] + missing_artifacts: list[str] = [] + reason_codes: list[str] = [] + + if not pipeline_closure_p2_98_gate_script.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_98_gate_script)) + p2_99_closure_text = "" + else: + p2_99_closure_text = _read_text(pipeline_closure_p2_98_gate_script) + + if not pipeline_closure_p2_98_runtime_test.is_file(): + missing_artifacts.append(str(pipeline_closure_p2_98_runtime_test)) + + for issue_code, pattern in REQUIRED_P2_99_CLOSURE_PATTERNS.items(): + if pattern not in p2_99_closure_text: + structural_issues.append(issue_code) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + status = "failed" + decision = "hold_pipeline_closure_p2_99_manual_action" + exit_code = 1 + should_proceed = False + requires_manual_action = True + channel = "blocker" + reason_codes.extend(structural_issues) + if missing_artifacts: + reason_codes.append("pipeline_closure_required_artifacts_missing") + else: + status = "passed" + decision = "pipeline_closure_to_p2_99_verified" + exit_code = 0 + should_proceed = True + requires_manual_action = False + channel = "release" + reason_codes = [ + "pipeline_closure_to_p2_99_verified", + "pipeline_closure_p2_99_contract_wired", + ] + + reason_codes = _unique(reason_codes) + + summary = ( + "pipeline_closure_card=P2-100 " + f"pipeline_closure_to_p2_99_status={status} " + f"pipeline_closure_to_p2_99_decision={decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "source_pipeline_closure_p2_98_gate_script": str( + pipeline_closure_p2_98_gate_script.resolve() + ), + "source_pipeline_closure_p2_98_runtime_test": str( + pipeline_closure_p2_98_runtime_test.resolve() + ), + "pipeline_closure_card_id": "P2-100", + "pipeline_closure_target_card_id": "P2-99", + "pipeline_closure_to_p2_99_status": status, + "pipeline_closure_to_p2_99_decision": decision, + "pipeline_closure_to_p2_99_exit_code": exit_code, + "pipeline_closure_to_p2_99_should_proceed": should_proceed, + "pipeline_closure_to_p2_99_requires_manual_action": requires_manual_action, + "pipeline_closure_to_p2_99_channel": channel, + "pipeline_closure_to_p2_99_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "required_patterns_checked": REQUIRED_P2_99_CLOSURE_PATTERNS, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Pipeline Closure to P2-99 Report", + "", + ( + "- Pipeline Closure Status: " + f"**{str(payload['pipeline_closure_to_p2_99_status']).upper()}**" + ), + ( + "- Pipeline Closure Decision: " + f"`{payload['pipeline_closure_to_p2_99_decision']}`" + ), + ( + "- Pipeline Closure Exit Code: " + f"`{payload['pipeline_closure_to_p2_99_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['pipeline_closure_to_p2_99_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['pipeline_closure_to_p2_99_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['pipeline_closure_to_p2_99_channel']}`" + ), + ( + "- Source P2-99 Closure Gate: " + f"`{payload['source_pipeline_closure_p2_98_gate_script']}`" + ), + ( + "- Source P2-99 Runtime Test: " + f"`{payload['source_pipeline_closure_p2_98_runtime_test']}`" + ), + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['pipeline_closure_to_p2_99_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + return { + "workflow_pipeline_closure_to_p2_99_status": str( + payload["pipeline_closure_to_p2_99_status"] + ), + "workflow_pipeline_closure_to_p2_99_decision": str( + payload["pipeline_closure_to_p2_99_decision"] + ), + "workflow_pipeline_closure_to_p2_99_exit_code": str( + payload["pipeline_closure_to_p2_99_exit_code"] + ), + "workflow_pipeline_closure_to_p2_99_should_proceed": ( + "true" + if payload["pipeline_closure_to_p2_99_should_proceed"] + else "false" + ), + "workflow_pipeline_closure_to_p2_99_requires_manual_action": ( + "true" + if payload["pipeline_closure_to_p2_99_requires_manual_action"] + else "false" + ), + "workflow_pipeline_closure_to_p2_99_channel": str( + payload["pipeline_closure_to_p2_99_channel"] + ), + "workflow_pipeline_closure_to_p2_99_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_pipeline_closure_to_p2_99_structural_issues_count": str( + len(payload["structural_issues"]) + ), + "workflow_pipeline_closure_to_p2_99_missing_artifacts_count": str( + len(payload["missing_artifacts"]) + ), + "workflow_pipeline_closure_to_p2_99_report_json": str(output_json), + "workflow_pipeline_closure_to_p2_99_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux CI workflow pipeline closure to P2-99 wiring" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--pipeline-closure-P2-98-gate-script", + default="scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py", + help="P2-99 closure verifier script path (P2-98 pipeline closure gate)", + ) + parser.add_argument( + "--pipeline-closure-P2-98-runtime-test", + default="tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py", + help="P2-99 runtime test path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_99.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_99.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + pipeline_closure_p2_98_gate_script = project_root / Path( + args.pipeline_closure_p2_98_gate_script + ) + pipeline_closure_p2_98_runtime_test = project_root / Path( + args.pipeline_closure_p2_98_runtime_test + ) + + try: + payload = build_pipeline_closure_to_p2_99_payload( + project_root=project_root, + pipeline_closure_p2_98_gate_script=pipeline_closure_p2_98_gate_script, + pipeline_closure_p2_98_runtime_test=pipeline_closure_p2_98_runtime_test, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-pipeline-closure-P2-99-gate] {exc}", + file=sys.stderr, + ) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"pipeline closure report json: {output_json}") + print(f"pipeline closure report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "pipeline closure summary: " + f"pipeline_closure_to_p2_99_status={payload['pipeline_closure_to_p2_99_status']} " + f"pipeline_closure_to_p2_99_decision={payload['pipeline_closure_to_p2_99_decision']} " + f"pipeline_closure_to_p2_99_exit_code={payload['pipeline_closure_to_p2_99_exit_code']}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["pipeline_closure_to_p2_99_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + diff --git a/scripts/run_p2_linux_ci_workflow_pipeline_gate.py b/scripts/run_p2_linux_ci_workflow_pipeline_gate.py new file mode 100644 index 0000000..c5a3625 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_pipeline_gate.py @@ -0,0 +1,3234 @@ +"""Phase 2 card P2-26 gate for Linux CI workflow full-pipeline orchestration. + +This script composes P2-17 -> P2-82 into one CI-entry pipeline: +1) workflow plan generation, +2) workflow YAML rendering, +3) render drift sync, +4) command guard, +5) governance convergence, +6) governance publish, +7) execution decision, +8) dispatch readiness, +9) dispatch execution, +10) dispatch traceability, +11) dispatch completion await, +12) dispatch terminal publish, +13) release handoff, +14) release trigger, +15) release traceability, +16) release completion await, +17) release terminal publish, +18) release finalization, +19) release closure publish, +20) release archive publish, +21) release verdict publish, +22) release incident dispatch, +23) release terminal verdict publish, +24) release delivery closure publish, +25) release delivery terminal publish, +26) release delivery final verdict publish, +27) release follow-up dispatch publish, +28) release follow-up closure publish, +29) release follow-up terminal publish, +30) release follow-up final verdict, +31) release final outcome, +32) release final terminal publish, +33) release final handoff, +34) release final closure, +35) release final closure publish, +36) release final archive, +37) release final verdict, +38) release final verdict publish, +39) release final publish archive, +40) gate manifest drift closure, +41) terminal verdict closure, +42) Linux validation dispatch, +43) Linux validation verdict, +44) Linux validation verdict publish, +45) Linux validation terminal publish, +46) Linux validation final verdict, +47) Linux validation final verdict publish, +48) Linux validation final publish archive, +49) Linux validation terminal verdict, +50) Linux validation terminal verdict publish, +51) Linux validation terminal dispatch, +52) Linux validation terminal dispatch execution, +53) Linux validation terminal dispatch traceability (poll-ready run tracking contract). +54) Linux validation terminal dispatch completion await. +55) Linux validation terminal dispatch terminal publish. +56) Linux validation terminal dispatch final verdict. +57) Linux validation terminal dispatch final publish archive. +58) Linux validation terminal dispatch terminal verdict. +59) Linux validation terminal dispatch terminal verdict publish. +60) Linux validation terminal dispatch terminal verdict publish archive. +61) Linux validation terminal dispatch terminal verdict closure. +62) Linux validation terminal dispatch terminal verdict closure publish. +63) Linux validation terminal dispatch terminal verdict closure publish archive. +""" + +from __future__ import annotations + +import argparse +import shlex +import subprocess +import sys +from pathlib import Path +from typing import Sequence + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def build_ci_workflow_pipeline_commands( + *, + python_executable: str, + ci_matrix_path: Path, + workflow_plan_output: Path, + workflow_path: Path, + metadata_path: Path, + command_guard_output: Path, + governance_report_output: Path, + governance_publish_json_output: Path, + governance_publish_markdown_output: Path, + execution_decision_json_output: Path, + execution_decision_markdown_output: Path, + dispatch_json_output: Path, + dispatch_markdown_output: Path, + dispatch_execution_json_output: Path, + dispatch_execution_markdown_output: Path, + dispatch_trace_json_output: Path, + dispatch_trace_markdown_output: Path, + dispatch_completion_json_output: Path, + dispatch_completion_markdown_output: Path, + terminal_publish_json_output: Path, + terminal_publish_markdown_output: Path, + release_handoff_json_output: Path, + release_handoff_markdown_output: Path, + release_trigger_json_output: Path, + release_trigger_markdown_output: Path, + release_trace_json_output: Path, + release_trace_markdown_output: Path, + release_completion_json_output: Path, + release_completion_markdown_output: Path, + release_terminal_publish_json_output: Path, + release_terminal_publish_markdown_output: Path, + release_finalization_json_output: Path, + release_finalization_markdown_output: Path, + release_closure_json_output: Path, + release_closure_markdown_output: Path, + release_archive_json_output: Path, + release_archive_markdown_output: Path, + release_verdict_json_output: Path, + release_verdict_markdown_output: Path, + release_incident_json_output: Path, + release_incident_markdown_output: Path, + release_terminal_verdict_json_output: Path, + release_terminal_verdict_markdown_output: Path, + release_delivery_json_output: Path, + release_delivery_markdown_output: Path, + release_delivery_terminal_publish_json_output: Path, + release_delivery_terminal_publish_markdown_output: Path, + release_delivery_final_verdict_json_output: Path, + release_delivery_final_verdict_markdown_output: Path, + release_follow_up_dispatch_json_output: Path, + release_follow_up_dispatch_markdown_output: Path, + release_follow_up_closure_json_output: Path, + release_follow_up_closure_markdown_output: Path, + release_follow_up_terminal_publish_json_output: Path, + release_follow_up_terminal_publish_markdown_output: Path, + release_follow_up_final_verdict_json_output: Path, + release_follow_up_final_verdict_markdown_output: Path, + release_final_outcome_json_output: Path, + release_final_outcome_markdown_output: Path, + release_final_terminal_publish_json_output: Path, + release_final_terminal_publish_markdown_output: Path, + release_final_handoff_json_output: Path, + release_final_handoff_markdown_output: Path, + release_final_closure_json_output: Path, + release_final_closure_markdown_output: Path, + release_final_closure_publish_json_output: Path, + release_final_closure_publish_markdown_output: Path, + release_final_archive_json_output: Path, + release_final_archive_markdown_output: Path, + release_final_verdict_json_output: Path, + release_final_verdict_markdown_output: Path, + release_final_verdict_publish_json_output: Path, + release_final_verdict_publish_markdown_output: Path, + release_final_publish_archive_json_output: Path, + release_final_publish_archive_markdown_output: Path, + gate_manifest_drift_json_output: Path, + gate_manifest_drift_markdown_output: Path, + terminal_verdict_json_output: Path, + terminal_verdict_markdown_output: Path, + linux_validation_dispatch_json_output: Path, + linux_validation_dispatch_markdown_output: Path, + linux_validation_verdict_json_output: Path, + linux_validation_verdict_markdown_output: Path, + linux_validation_verdict_publish_json_output: Path, + linux_validation_verdict_publish_markdown_output: Path, + linux_validation_terminal_publish_json_output: Path, + linux_validation_terminal_publish_markdown_output: Path, + linux_validation_final_verdict_json_output: Path, + linux_validation_final_verdict_markdown_output: Path, + linux_validation_final_verdict_publish_json_output: Path, + linux_validation_final_verdict_publish_markdown_output: Path, + linux_validation_final_publish_archive_json_output: Path, + linux_validation_final_publish_archive_markdown_output: Path, + linux_validation_terminal_verdict_json_output: Path, + linux_validation_terminal_verdict_markdown_output: Path, + linux_validation_terminal_verdict_publish_json_output: Path, + linux_validation_terminal_verdict_publish_markdown_output: Path, + linux_validation_terminal_dispatch_json_output: Path, + linux_validation_terminal_dispatch_markdown_output: Path, + linux_validation_terminal_dispatch_execution_json_output: Path, + linux_validation_terminal_dispatch_execution_markdown_output: Path, + linux_validation_terminal_dispatch_trace_json_output: Path, + linux_validation_terminal_dispatch_trace_markdown_output: Path, + linux_validation_terminal_dispatch_completion_json_output: Path, + linux_validation_terminal_dispatch_completion_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_publish_json_output: Path, + linux_validation_terminal_dispatch_terminal_publish_markdown_output: Path, + linux_validation_terminal_dispatch_final_verdict_json_output: Path, + linux_validation_terminal_dispatch_final_verdict_markdown_output: Path, + linux_validation_terminal_dispatch_final_verdict_publish_json_output: Path, + linux_validation_terminal_dispatch_final_verdict_publish_markdown_output: Path, + linux_validation_terminal_dispatch_final_publish_archive_json_output: Path, + linux_validation_terminal_dispatch_final_publish_archive_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_publish_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output: Path, + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output: Path, + workflow_name: str, + python_version: str, + artifact_prefix: str, + target_environment: str, + release_channel: str, + workflow_ref: str, + release_workflow_path: str, + release_workflow_ref: str, + release_timeout_seconds: int, + release_command: str, + release_trace_poll_timeout_seconds: int, + release_completion_poll_interval_seconds: int, + release_completion_max_polls: int, + release_completion_poll_timeout_seconds: int, + incident_timeout_seconds: int, + incident_command: str, + incident_repo: str, + incident_label: str, + incident_title_prefix: str, + follow_up_timeout_seconds: int, + follow_up_command: str, + follow_up_repo: str, + follow_up_label: str, + follow_up_title_prefix: str, + on_block_policy: str, + dispatch_timeout_seconds: int, + trace_poll_timeout_seconds: int, + completion_poll_interval_seconds: int, + completion_max_polls: int, + completion_poll_timeout_seconds: int, + linux_validation_timeout_seconds: int, + strict_generated_at: bool, + sync_write: bool, + command_guard_write: bool, + dispatch_trace_poll_now: bool, + release_trace_poll_now: bool, + completion_allow_in_progress: bool, + release_completion_allow_in_progress: bool, + on_release_hold_policy: str, + skip_plan: bool, + skip_yaml: bool, + skip_sync: bool, + skip_command_guard: bool, + skip_governance: bool, + skip_governance_publish: bool, + skip_decision: bool, + skip_dispatch: bool, + skip_dispatch_execution: bool, + skip_dispatch_trace: bool, + skip_dispatch_completion: bool, + skip_dispatch_terminal_publish: bool, + skip_dispatch_release_handoff: bool, + skip_release_trigger: bool, + skip_release_trace: bool, + skip_release_completion: bool, + skip_release_terminal_publish: bool, + skip_release_finalization: bool, + skip_release_closure: bool, + skip_release_archive: bool, + skip_release_verdict: bool, + skip_release_incident: bool, + skip_release_terminal_verdict: bool, + skip_release_delivery: bool, + skip_release_delivery_terminal_publish: bool, + skip_release_delivery_final_verdict: bool, + skip_release_follow_up_dispatch: bool, + skip_release_follow_up_closure: bool, + skip_release_follow_up_terminal_publish: bool, + skip_release_follow_up_final_verdict: bool, + skip_release_final_outcome: bool, + skip_release_final_terminal_publish: bool, + skip_release_final_handoff: bool, + skip_release_final_closure: bool, + skip_release_final_closure_publish: bool, + skip_release_final_archive: bool, + skip_release_final_verdict: bool, + skip_release_final_verdict_publish: bool, + skip_release_final_publish_archive: bool, + skip_gate_manifest_drift: bool, + skip_terminal_verdict: bool, + skip_linux_validation_dispatch: bool, + skip_linux_validation_verdict: bool, + skip_linux_validation_verdict_publish: bool, + skip_linux_validation_terminal_publish: bool, + skip_linux_validation_final_verdict: bool, + skip_linux_validation_final_verdict_publish: bool, + skip_linux_validation_final_publish_archive: bool, + skip_linux_validation_terminal_verdict: bool, + skip_linux_validation_terminal_verdict_publish: bool, + skip_linux_validation_terminal_dispatch: bool, + skip_linux_validation_terminal_dispatch_execution: bool, + skip_linux_validation_terminal_dispatch_trace: bool, + skip_linux_validation_terminal_dispatch_completion: bool, + skip_linux_validation_terminal_dispatch_terminal_publish: bool, + skip_linux_validation_terminal_dispatch_final_verdict: bool, + skip_linux_validation_terminal_dispatch_final_verdict_publish: bool, + skip_linux_validation_terminal_dispatch_final_publish_archive: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: bool, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: bool, +) -> list[tuple[str, list[str]]]: + pipeline: list[tuple[str, list[str]]] = [] + + if not skip_plan: + pipeline.append( + ( + "workflow_plan", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_plan_gate.py", + "--ci-matrix", + str(ci_matrix_path), + "--output", + str(workflow_plan_output), + ], + ) + ) + + if not skip_yaml: + pipeline.append( + ( + "workflow_yaml", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_yaml_gate.py", + "--ci-workflow-plan", + str(workflow_plan_output), + "--output-workflow", + str(workflow_path), + "--output-metadata", + str(metadata_path), + "--workflow-name", + workflow_name, + "--python-version", + python_version, + "--artifact-prefix", + artifact_prefix, + ], + ) + ) + + if not skip_sync: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_sync_gate.py", + "--ci-workflow-plan", + str(workflow_plan_output), + "--workflow-path", + str(workflow_path), + "--metadata-path", + str(metadata_path), + "--workflow-name", + workflow_name, + "--python-version", + python_version, + "--artifact-prefix", + artifact_prefix, + ] + if strict_generated_at: + command.append("--strict-generated-at") + if sync_write: + command.append("--write") + pipeline.append(("workflow_sync", command)) + + if not skip_command_guard: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_command_guard_gate.py", + "--ci-workflow-plan", + str(workflow_plan_output), + "--output", + str(command_guard_output), + ] + if command_guard_write: + command.append("--write") + pipeline.append(("workflow_command_guard", command)) + + if not skip_governance: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_governance_gate.py", + "--ci-workflow-plan", + str(workflow_plan_output), + "--workflow-path", + str(workflow_path), + "--metadata-path", + str(metadata_path), + "--workflow-name", + workflow_name, + "--python-version", + python_version, + "--artifact-prefix", + artifact_prefix, + "--output", + str(governance_report_output), + ] + if strict_generated_at: + command.append("--strict-generated-at") + pipeline.append(("workflow_governance", command)) + + if not skip_governance_publish: + pipeline.append( + ( + "workflow_governance_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_governance_publish_gate.py", + "--governance-report", + str(governance_report_output), + "--output-json", + str(governance_publish_json_output), + "--output-markdown", + str(governance_publish_markdown_output), + ], + ) + ) + + if not skip_decision: + pipeline.append( + ( + "workflow_decision", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_decision_gate.py", + "--governance-publish", + str(governance_publish_json_output), + "--workflow-plan", + str(workflow_plan_output), + "--workflow-path", + str(workflow_path), + "--on-block", + on_block_policy, + "--output-json", + str(execution_decision_json_output), + "--output-markdown", + str(execution_decision_markdown_output), + ], + ) + ) + + if not skip_dispatch: + pipeline.append( + ( + "workflow_dispatch", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_dispatch_gate.py", + "--execution-decision", + str(execution_decision_json_output), + "--workflow-ref", + workflow_ref, + "--output-json", + str(dispatch_json_output), + "--output-markdown", + str(dispatch_markdown_output), + ], + ) + ) + + if not skip_dispatch_execution: + pipeline.append( + ( + "workflow_dispatch_execution", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py", + "--dispatch-report", + str(dispatch_json_output), + "--project-root", + ".", + "--dispatch-timeout-seconds", + str(dispatch_timeout_seconds), + "--output-json", + str(dispatch_execution_json_output), + "--output-markdown", + str(dispatch_execution_markdown_output), + ], + ) + ) + + if not skip_dispatch_trace: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py", + "--dispatch-execution-report", + str(dispatch_execution_json_output), + "--project-root", + ".", + "--poll-timeout-seconds", + str(trace_poll_timeout_seconds), + "--output-json", + str(dispatch_trace_json_output), + "--output-markdown", + str(dispatch_trace_markdown_output), + ] + if dispatch_trace_poll_now: + command.append("--poll-now") + pipeline.append(("workflow_dispatch_trace", command)) + + if not skip_dispatch_completion: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py", + "--dispatch-trace-report", + str(dispatch_trace_json_output), + "--project-root", + ".", + "--poll-interval-seconds", + str(completion_poll_interval_seconds), + "--max-polls", + str(completion_max_polls), + "--poll-timeout-seconds", + str(completion_poll_timeout_seconds), + "--output-json", + str(dispatch_completion_json_output), + "--output-markdown", + str(dispatch_completion_markdown_output), + ] + if completion_allow_in_progress: + command.append("--allow-in-progress") + pipeline.append(("workflow_dispatch_completion", command)) + + if not skip_dispatch_terminal_publish: + pipeline.append( + ( + "workflow_terminal_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py", + "--dispatch-completion-report", + str(dispatch_completion_json_output), + "--output-json", + str(terminal_publish_json_output), + "--output-markdown", + str(terminal_publish_markdown_output), + ], + ) + ) + + if not skip_dispatch_release_handoff: + pipeline.append( + ( + "workflow_release_handoff", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_handoff_gate.py", + "--terminal-publish-report", + str(terminal_publish_json_output), + "--target-environment", + target_environment, + "--release-channel", + release_channel, + "--output-json", + str(release_handoff_json_output), + "--output-markdown", + str(release_handoff_markdown_output), + ], + ) + ) + + if not skip_release_trigger: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_trigger_gate.py", + "--release-handoff-report", + str(release_handoff_json_output), + "--project-root", + ".", + "--release-workflow-path", + release_workflow_path, + "--release-workflow-ref", + release_workflow_ref, + "--release-timeout-seconds", + str(release_timeout_seconds), + "--output-json", + str(release_trigger_json_output), + "--output-markdown", + str(release_trigger_markdown_output), + ] + if release_command.strip(): + command.extend(["--release-command", release_command]) + pipeline.append(("workflow_release_trigger", command)) + + if not skip_release_trace: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_trace_gate.py", + "--release-trigger-report", + str(release_trigger_json_output), + "--project-root", + ".", + "--poll-timeout-seconds", + str(release_trace_poll_timeout_seconds), + "--output-json", + str(release_trace_json_output), + "--output-markdown", + str(release_trace_markdown_output), + ] + if release_trace_poll_now: + command.append("--poll-now") + pipeline.append(("workflow_release_trace", command)) + + if not skip_release_completion: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_completion_gate.py", + "--release-trace-report", + str(release_trace_json_output), + "--project-root", + ".", + "--poll-interval-seconds", + str(release_completion_poll_interval_seconds), + "--max-polls", + str(release_completion_max_polls), + "--poll-timeout-seconds", + str(release_completion_poll_timeout_seconds), + "--output-json", + str(release_completion_json_output), + "--output-markdown", + str(release_completion_markdown_output), + ] + if release_completion_allow_in_progress: + command.append("--allow-in-progress") + pipeline.append(("workflow_release_completion", command)) + + if not skip_release_terminal_publish: + pipeline.append( + ( + "workflow_release_terminal_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py", + "--release-completion-report", + str(release_completion_json_output), + "--output-json", + str(release_terminal_publish_json_output), + "--output-markdown", + str(release_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_release_finalization: + pipeline.append( + ( + "workflow_release_finalization", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_finalization_gate.py", + "--release-terminal-publish-report", + str(release_terminal_publish_json_output), + "--on-hold", + on_release_hold_policy, + "--output-json", + str(release_finalization_json_output), + "--output-markdown", + str(release_finalization_markdown_output), + ], + ) + ) + + if not skip_release_closure: + pipeline.append( + ( + "workflow_release_closure", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_closure_gate.py", + "--release-finalization-report", + str(release_finalization_json_output), + "--output-json", + str(release_closure_json_output), + "--output-markdown", + str(release_closure_markdown_output), + ], + ) + ) + + if not skip_release_archive: + pipeline.append( + ( + "workflow_release_archive", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_archive_gate.py", + "--release-closure-report", + str(release_closure_json_output), + "--output-json", + str(release_archive_json_output), + "--output-markdown", + str(release_archive_markdown_output), + ], + ) + ) + + if not skip_release_verdict: + pipeline.append( + ( + "workflow_release_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_verdict_gate.py", + "--release-archive-report", + str(release_archive_json_output), + "--output-json", + str(release_verdict_json_output), + "--output-markdown", + str(release_verdict_markdown_output), + ], + ) + ) + + if not skip_release_incident: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_incident_gate.py", + "--release-verdict-report", + str(release_verdict_json_output), + "--project-root", + ".", + "--incident-timeout-seconds", + str(incident_timeout_seconds), + "--incident-repo", + incident_repo, + "--incident-label", + incident_label, + "--incident-title-prefix", + incident_title_prefix, + "--output-json", + str(release_incident_json_output), + "--output-markdown", + str(release_incident_markdown_output), + ] + if incident_command.strip(): + command.extend(["--incident-command", incident_command]) + pipeline.append(("workflow_release_incident", command)) + + if not skip_release_terminal_verdict: + pipeline.append( + ( + "workflow_release_terminal_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py", + "--release-incident-report", + str(release_incident_json_output), + "--output-json", + str(release_terminal_verdict_json_output), + "--output-markdown", + str(release_terminal_verdict_markdown_output), + ], + ) + ) + + if not skip_release_delivery: + pipeline.append( + ( + "workflow_release_delivery", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_delivery_gate.py", + "--release-terminal-verdict-report", + str(release_terminal_verdict_json_output), + "--output-json", + str(release_delivery_json_output), + "--output-markdown", + str(release_delivery_markdown_output), + ], + ) + ) + + if not skip_release_delivery_terminal_publish: + pipeline.append( + ( + "workflow_release_delivery_terminal_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py", + "--release-delivery-report", + str(release_delivery_json_output), + "--output-json", + str(release_delivery_terminal_publish_json_output), + "--output-markdown", + str(release_delivery_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_release_delivery_final_verdict: + pipeline.append( + ( + "workflow_release_delivery_final_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py", + "--release-delivery-terminal-publish-report", + str(release_delivery_terminal_publish_json_output), + "--output-json", + str(release_delivery_final_verdict_json_output), + "--output-markdown", + str(release_delivery_final_verdict_markdown_output), + ], + ) + ) + + if not skip_release_follow_up_dispatch: + pipeline.append( + ( + "workflow_release_follow_up_dispatch", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py", + "--release-delivery-final-verdict-report", + str(release_delivery_final_verdict_json_output), + "--output-json", + str(release_follow_up_dispatch_json_output), + "--output-markdown", + str(release_follow_up_dispatch_markdown_output), + ], + ) + ) + + if not skip_release_follow_up_closure: + command = [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py", + "--release-follow-up-dispatch-report", + str(release_follow_up_dispatch_json_output), + "--project-root", + ".", + "--follow-up-timeout-seconds", + str(follow_up_timeout_seconds), + "--follow-up-repo", + follow_up_repo, + "--follow-up-label", + follow_up_label, + "--follow-up-title-prefix", + follow_up_title_prefix, + "--output-json", + str(release_follow_up_closure_json_output), + "--output-markdown", + str(release_follow_up_closure_markdown_output), + ] + if follow_up_command.strip(): + command.extend(["--follow-up-command", follow_up_command]) + pipeline.append(("workflow_release_follow_up_closure", command)) + + if not skip_release_follow_up_terminal_publish: + pipeline.append( + ( + "workflow_release_follow_up_terminal_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py", + "--release-follow-up-closure-report", + str(release_follow_up_closure_json_output), + "--output-json", + str(release_follow_up_terminal_publish_json_output), + "--output-markdown", + str(release_follow_up_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_release_follow_up_final_verdict: + pipeline.append( + ( + "workflow_release_follow_up_final_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py", + "--release-follow-up-terminal-publish-report", + str(release_follow_up_terminal_publish_json_output), + "--output-json", + str(release_follow_up_final_verdict_json_output), + "--output-markdown", + str(release_follow_up_final_verdict_markdown_output), + ], + ) + ) + + if not skip_release_final_outcome: + pipeline.append( + ( + "workflow_release_final_outcome", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py", + "--release-delivery-final-verdict-report", + str(release_delivery_final_verdict_json_output), + "--release-follow-up-final-verdict-report", + str(release_follow_up_final_verdict_json_output), + "--output-json", + str(release_final_outcome_json_output), + "--output-markdown", + str(release_final_outcome_markdown_output), + ], + ) + ) + + if not skip_release_final_terminal_publish: + pipeline.append( + ( + "workflow_release_final_terminal_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py", + "--release-final-outcome-report", + str(release_final_outcome_json_output), + "--output-json", + str(release_final_terminal_publish_json_output), + "--output-markdown", + str(release_final_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_release_final_handoff: + pipeline.append( + ( + "workflow_release_final_handoff", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py", + "--release-final-terminal-publish-report", + str(release_final_terminal_publish_json_output), + "--output-json", + str(release_final_handoff_json_output), + "--output-markdown", + str(release_final_handoff_markdown_output), + ], + ) + ) + + if not skip_release_final_closure: + pipeline.append( + ( + "workflow_release_final_closure", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py", + "--release-final-handoff-report", + str(release_final_handoff_json_output), + "--output-json", + str(release_final_closure_json_output), + "--output-markdown", + str(release_final_closure_markdown_output), + ], + ) + ) + + if not skip_release_final_closure_publish: + pipeline.append( + ( + "workflow_release_final_closure_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py", + "--release-final-closure-report", + str(release_final_closure_json_output), + "--output-json", + str(release_final_closure_publish_json_output), + "--output-markdown", + str(release_final_closure_publish_markdown_output), + ], + ) + ) + + if not skip_release_final_archive: + pipeline.append( + ( + "workflow_release_final_archive", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py", + "--release-final-closure-publish-report", + str(release_final_closure_publish_json_output), + "--output-json", + str(release_final_archive_json_output), + "--output-markdown", + str(release_final_archive_markdown_output), + ], + ) + ) + + if not skip_release_final_verdict: + pipeline.append( + ( + "workflow_release_final_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py", + "--release-final-archive-report", + str(release_final_archive_json_output), + "--output-json", + str(release_final_verdict_json_output), + "--output-markdown", + str(release_final_verdict_markdown_output), + ], + ) + ) + + if not skip_release_final_verdict_publish: + pipeline.append( + ( + "workflow_release_final_verdict_publish", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py", + "--release-final-verdict-report", + str(release_final_verdict_json_output), + "--output-json", + str(release_final_verdict_publish_json_output), + "--output-markdown", + str(release_final_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_release_final_publish_archive: + pipeline.append( + ( + "workflow_release_final_publish_archive", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py", + "--release-final-verdict-publish-report", + str(release_final_verdict_publish_json_output), + "--output-json", + str(release_final_publish_archive_json_output), + "--output-markdown", + str(release_final_publish_archive_markdown_output), + ], + ) + ) + + if not skip_gate_manifest_drift: + pipeline.append( + ( + "workflow_gate_manifest_drift", + [ + python_executable, + "scripts/run_p2_linux_gate_manifest_drift_gate.py", + "--output-json", + str(gate_manifest_drift_json_output), + "--output-markdown", + str(gate_manifest_drift_markdown_output), + ], + ) + ) + + if not skip_terminal_verdict: + pipeline.append( + ( + "workflow_terminal_verdict", + [ + python_executable, + "scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py", + "--release-final-publish-archive-report", + str(release_final_publish_archive_json_output), + "--gate-manifest-drift-report", + str(gate_manifest_drift_json_output), + "--output-json", + str(terminal_verdict_json_output), + "--output-markdown", + str(terminal_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_dispatch: + pipeline.append( + ( + "workflow_linux_validation_dispatch", + [ + python_executable, + "scripts/run_p2_lv_dispatch_gate.py", + "--terminal-verdict-report", + str(terminal_verdict_json_output), + "--project-root", + ".", + "--python-executable", + python_executable, + "--linux-validation-timeout-seconds", + str(linux_validation_timeout_seconds), + "--output-json", + str(linux_validation_dispatch_json_output), + "--output-markdown", + str(linux_validation_dispatch_markdown_output), + ], + ) + ) + + if not skip_linux_validation_verdict: + pipeline.append( + ( + "workflow_linux_validation_verdict", + [ + python_executable, + "scripts/run_p2_lv_verdict_gate.py", + "--linux-validation-dispatch-report", + str(linux_validation_dispatch_json_output), + "--output-json", + str(linux_validation_verdict_json_output), + "--output-markdown", + str(linux_validation_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_verdict_publish: + pipeline.append( + ( + "workflow_linux_validation_verdict_publish", + [ + python_executable, + "scripts/run_p2_lv_verdict_publish_gate.py", + "--linux-validation-verdict-report", + str(linux_validation_verdict_json_output), + "--output-json", + str(linux_validation_verdict_publish_json_output), + "--output-markdown", + str(linux_validation_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_publish", + [ + python_executable, + "scripts/run_p2_lv_terminal_publish_gate.py", + "--linux-validation-verdict-publish-report", + str(linux_validation_verdict_publish_json_output), + "--output-json", + str(linux_validation_terminal_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_final_verdict: + pipeline.append( + ( + "workflow_linux_validation_final_verdict", + [ + python_executable, + "scripts/run_p2_lv_final_verdict_gate.py", + "--linux-validation-terminal-publish-report", + str(linux_validation_terminal_publish_json_output), + "--output-json", + str(linux_validation_final_verdict_json_output), + "--output-markdown", + str(linux_validation_final_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_final_verdict_publish: + pipeline.append( + ( + "workflow_linux_validation_final_verdict_publish", + [ + python_executable, + "scripts/run_p2_lv_final_verdict_publish_gate.py", + "--linux-validation-final-verdict-report", + str(linux_validation_final_verdict_json_output), + "--output-json", + str(linux_validation_final_verdict_publish_json_output), + "--output-markdown", + str(linux_validation_final_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_final_publish_archive: + pipeline.append( + ( + "workflow_linux_validation_final_publish_archive", + [ + python_executable, + "scripts/run_p2_lv_final_publish_archive_gate.py", + "--linux-validation-final-verdict-publish-report", + str(linux_validation_final_verdict_publish_json_output), + "--output-json", + str(linux_validation_final_publish_archive_json_output), + "--output-markdown", + str(linux_validation_final_publish_archive_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_verdict: + pipeline.append( + ( + "workflow_linux_validation_terminal_verdict", + [ + python_executable, + "scripts/run_p2_lv_terminal_verdict_gate.py", + "--linux-validation-final-publish-archive-report", + str(linux_validation_final_publish_archive_json_output), + "--output-json", + str(linux_validation_terminal_verdict_json_output), + "--output-markdown", + str(linux_validation_terminal_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_verdict_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_verdict_publish", + [ + python_executable, + "scripts/run_p2_lv_terminal_verdict_publish_gate.py", + "--linux-validation-terminal-verdict-report", + str(linux_validation_terminal_verdict_json_output), + "--output-json", + str(linux_validation_terminal_verdict_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch", + [ + python_executable, + "scripts/run_p2_lv_td_dispatch_gate.py", + "--linux-validation-terminal-verdict-publish-report", + str(linux_validation_terminal_verdict_publish_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_execution: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_execution", + [ + python_executable, + "scripts/run_p2_lv_td_execution_gate.py", + "--linux-validation-terminal-dispatch-report", + str(linux_validation_terminal_dispatch_json_output), + "--project-root", + ".", + "--python-executable", + python_executable, + "--linux-validation-terminal-timeout-seconds", + str(linux_validation_timeout_seconds), + "--output-json", + str(linux_validation_terminal_dispatch_execution_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_execution_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_trace: + command = [ + python_executable, + "scripts/run_p2_lv_td_trace_gate.py", + "--linux-validation-terminal-dispatch-execution-report", + str(linux_validation_terminal_dispatch_execution_json_output), + "--project-root", + ".", + "--poll-timeout-seconds", + str(trace_poll_timeout_seconds), + "--output-json", + str(linux_validation_terminal_dispatch_trace_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_trace_markdown_output), + ] + if dispatch_trace_poll_now: + command.append("--poll-now") + pipeline.append(("workflow_linux_validation_terminal_dispatch_trace", command)) + + if not skip_linux_validation_terminal_dispatch_completion: + command = [ + python_executable, + "scripts/run_p2_lv_td_completion_gate.py", + "--linux-validation-terminal-dispatch-trace-report", + str(linux_validation_terminal_dispatch_trace_json_output), + "--project-root", + ".", + "--poll-interval-seconds", + str(completion_poll_interval_seconds), + "--max-polls", + str(completion_max_polls), + "--poll-timeout-seconds", + str(completion_poll_timeout_seconds), + "--output-json", + str(linux_validation_terminal_dispatch_completion_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_completion_markdown_output), + ] + if completion_allow_in_progress: + command.append("--allow-in-progress") + pipeline.append(("workflow_linux_validation_terminal_dispatch_completion", command)) + + if not skip_linux_validation_terminal_dispatch_terminal_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_publish", + [ + python_executable, + "scripts/run_p2_lv_td_terminal_publish_gate.py", + "--linux-validation-terminal-dispatch-completion-report", + str(linux_validation_terminal_dispatch_completion_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_final_verdict: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_final_verdict", + [ + python_executable, + "scripts/run_p2_lv_td_final_verdict_gate.py", + "--linux-validation-terminal-dispatch-terminal-publish-report", + str(linux_validation_terminal_dispatch_terminal_publish_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_final_verdict_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_final_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_final_verdict_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_final_verdict_publish", + [ + python_executable, + "scripts/run_p2_lv_td_final_verdict_publish_gate.py", + "--linux-validation-terminal-dispatch-final-verdict-report", + str(linux_validation_terminal_dispatch_final_verdict_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_final_verdict_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_final_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_final_publish_archive: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_final_publish_archive", + [ + python_executable, + "scripts/run_p2_lv_td_final_publish_archive_gate.py", + "--linux-validation-terminal-dispatch-final-verdict-publish-report", + str(linux_validation_terminal_dispatch_final_verdict_publish_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_final_publish_archive_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_final_publish_archive_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict", + [ + python_executable, + "scripts/run_p2_lv_td_terminal_verdict_gate.py", + "--linux-validation-terminal-dispatch-final-publish-archive-report", + str(linux_validation_terminal_dispatch_final_publish_archive_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_verdict_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_verdict_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish", + [ + python_executable, + "scripts/run_p2_lv_td_tverdict_publish_gate.py", + "--linux-validation-terminal-dispatch-terminal-verdict-report", + str(linux_validation_terminal_dispatch_terminal_verdict_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", + [ + python_executable, + "scripts/run_p2_lv_td_tverdict_publish_archive_gate.py", + "--linux-validation-terminal-dispatch-terminal-verdict-publish-report", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure", + [ + python_executable, + "scripts/run_p2_lv_td_tverdict_closure_gate.py", + "--linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report", + str(linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output), + "--gate-manifest-drift-report", + str(gate_manifest_drift_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", + [ + python_executable, + "scripts/run_p2_lv_td_tverdict_closure_publish_gate.py", + "--linux-validation-terminal-dispatch-terminal-verdict-closure-report", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_json_output), + "--output-json", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + "--output-markdown", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output), + ], + ) + ) + + if not skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive: + pipeline.append( + ( + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + [ + python_executable, + "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + str(linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output), + "--output-json", + str( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + ), + "--output-markdown", + str( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + ), + ], + ) + ) + + return pipeline + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run Linux CI workflow full pipeline: P2-17 -> P2-82" + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable for all sub-commands (default: current interpreter)", + ) + parser.add_argument( + "--ci-matrix", + default=".claude/reports/linux_unified_gate/ci_matrix.json", + help="P2-16 CI matrix JSON path", + ) + parser.add_argument( + "--workflow-plan-output", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 workflow plan output path", + ) + parser.add_argument( + "--workflow-path", + default=".github/workflows/linux_unified_gate.yml", + help="P2-18 rendered workflow YAML path", + ) + parser.add_argument( + "--metadata-path", + default=".claude/reports/linux_unified_gate/ci_workflow_render.json", + help="P2-18 rendered metadata JSON path", + ) + parser.add_argument( + "--command-guard-output", + default=".claude/reports/linux_unified_gate/ci_workflow_command_guard.json", + help="P2-20 command guard report JSON path", + ) + parser.add_argument( + "--governance-report-output", + default=".claude/reports/linux_unified_gate/ci_workflow_governance.json", + help="P2-21 governance report JSON path", + ) + parser.add_argument( + "--governance-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_governance_publish.json", + help="P2-22 governance publish JSON path", + ) + parser.add_argument( + "--governance-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_governance_publish.md", + help="P2-22 governance publish markdown path", + ) + parser.add_argument( + "--execution-decision-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_execution_decision.json", + help="P2-23 execution decision JSON path", + ) + parser.add_argument( + "--execution-decision-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_execution_decision.md", + help="P2-23 execution decision markdown path", + ) + parser.add_argument( + "--dispatch-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch.json", + help="P2-24 dispatch JSON path", + ) + parser.add_argument( + "--dispatch-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch.md", + help="P2-24 dispatch markdown path", + ) + parser.add_argument( + "--dispatch-execution-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json", + help="P2-25 dispatch execution JSON path", + ) + parser.add_argument( + "--dispatch-execution-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md", + help="P2-25 dispatch execution markdown path", + ) + parser.add_argument( + "--dispatch-trace-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json", + help="P2-27 dispatch trace JSON path", + ) + parser.add_argument( + "--dispatch-trace-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md", + help="P2-27 dispatch trace markdown path", + ) + parser.add_argument( + "--dispatch-completion-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json", + help="P2-28 dispatch completion JSON path", + ) + parser.add_argument( + "--dispatch-completion-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md", + help="P2-28 dispatch completion markdown path", + ) + parser.add_argument( + "--dispatch-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json", + help="P2-29 terminal publish JSON path", + ) + parser.add_argument( + "--dispatch-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md", + help="P2-29 terminal publish markdown path", + ) + parser.add_argument( + "--dispatch-release-handoff-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_handoff.json", + help="P2-30 release handoff JSON path", + ) + parser.add_argument( + "--dispatch-release-handoff-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_handoff.md", + help="P2-30 release handoff markdown path", + ) + parser.add_argument( + "--release-trigger-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trigger.json", + help="P2-31 release trigger JSON path", + ) + parser.add_argument( + "--release-trigger-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trigger.md", + help="P2-31 release trigger markdown path", + ) + parser.add_argument( + "--release-trace-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trace.json", + help="P2-32 release trace JSON path", + ) + parser.add_argument( + "--release-trace-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trace.md", + help="P2-32 release trace markdown path", + ) + parser.add_argument( + "--release-completion-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_completion.json", + help="P2-33 release completion JSON path", + ) + parser.add_argument( + "--release-completion-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_completion.md", + help="P2-33 release completion markdown path", + ) + parser.add_argument( + "--release-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json", + help="P2-34 release terminal publish JSON path", + ) + parser.add_argument( + "--release-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md", + help="P2-34 release terminal publish markdown path", + ) + parser.add_argument( + "--release-finalization-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_finalization.json", + help="P2-35 release finalization JSON path", + ) + parser.add_argument( + "--release-finalization-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_finalization.md", + help="P2-35 release finalization markdown path", + ) + parser.add_argument( + "--release-closure-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_closure.json", + help="P2-36 release closure JSON path", + ) + parser.add_argument( + "--release-closure-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_closure.md", + help="P2-36 release closure markdown path", + ) + parser.add_argument( + "--release-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_archive.json", + help="P2-37 release archive JSON path", + ) + parser.add_argument( + "--release-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_archive.md", + help="P2-37 release archive markdown path", + ) + parser.add_argument( + "--release-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_verdict.json", + help="P2-38 release verdict JSON path", + ) + parser.add_argument( + "--release-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_verdict.md", + help="P2-38 release verdict markdown path", + ) + parser.add_argument( + "--release-incident-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_incident.json", + help="P2-39 release incident JSON path", + ) + parser.add_argument( + "--release-incident-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_incident.md", + help="P2-39 release incident markdown path", + ) + parser.add_argument( + "--release-terminal-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json", + help="P2-40 release terminal verdict JSON path", + ) + parser.add_argument( + "--release-terminal-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md", + help="P2-40 release terminal verdict markdown path", + ) + parser.add_argument( + "--release-delivery-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery.json", + help="P2-41 release delivery JSON path", + ) + parser.add_argument( + "--release-delivery-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery.md", + help="P2-41 release delivery markdown path", + ) + parser.add_argument( + "--release-delivery-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json", + help="P2-42 release delivery terminal publish JSON path", + ) + parser.add_argument( + "--release-delivery-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md", + help="P2-42 release delivery terminal publish markdown path", + ) + parser.add_argument( + "--release-delivery-final-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json", + help="P2-43 release delivery final verdict JSON path", + ) + parser.add_argument( + "--release-delivery-final-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md", + help="P2-43 release delivery final verdict markdown path", + ) + parser.add_argument( + "--release-follow-up-dispatch-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json", + help="P2-44 release follow-up dispatch JSON path", + ) + parser.add_argument( + "--release-follow-up-dispatch-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md", + help="P2-44 release follow-up dispatch markdown path", + ) + parser.add_argument( + "--release-follow-up-closure-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json", + help="P2-45 release follow-up closure JSON path", + ) + parser.add_argument( + "--release-follow-up-closure-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md", + help="P2-45 release follow-up closure markdown path", + ) + parser.add_argument( + "--release-follow-up-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json", + help="P2-46 release follow-up terminal publish JSON path", + ) + parser.add_argument( + "--release-follow-up-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md", + help="P2-46 release follow-up terminal publish markdown path", + ) + parser.add_argument( + "--release-follow-up-final-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json", + help="P2-47 release follow-up final verdict JSON path", + ) + parser.add_argument( + "--release-follow-up-final-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md", + help="P2-47 release follow-up final verdict markdown path", + ) + parser.add_argument( + "--release-final-outcome-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json", + help="P2-48 release final outcome JSON path", + ) + parser.add_argument( + "--release-final-outcome-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md", + help="P2-48 release final outcome markdown path", + ) + parser.add_argument( + "--release-final-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json", + help="P2-49 release final terminal publish JSON path", + ) + parser.add_argument( + "--release-final-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md", + help="P2-49 release final terminal publish markdown path", + ) + parser.add_argument( + "--release-final-handoff-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json", + help="P2-50 release final handoff JSON path", + ) + parser.add_argument( + "--release-final-handoff-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md", + help="P2-50 release final handoff markdown path", + ) + parser.add_argument( + "--release-final-closure-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json", + help="P2-51 release final closure JSON path", + ) + parser.add_argument( + "--release-final-closure-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md", + help="P2-51 release final closure markdown path", + ) + parser.add_argument( + "--release-final-closure-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json", + help="P2-52 release final closure publish JSON path", + ) + parser.add_argument( + "--release-final-closure-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md", + help="P2-52 release final closure publish markdown path", + ) + parser.add_argument( + "--release-final-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json", + help="P2-53 release final archive JSON path", + ) + parser.add_argument( + "--release-final-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md", + help="P2-53 release final archive markdown path", + ) + parser.add_argument( + "--release-final-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json", + help="P2-54 release final verdict JSON path", + ) + parser.add_argument( + "--release-final-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md", + help="P2-54 release final verdict markdown path", + ) + parser.add_argument( + "--release-final-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json", + help="P2-55 release final verdict publish JSON path", + ) + parser.add_argument( + "--release-final-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md", + help="P2-55 release final verdict publish markdown path", + ) + parser.add_argument( + "--release-final-publish-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json", + help="P2-56 release final publish archive JSON path", + ) + parser.add_argument( + "--release-final-publish-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md", + help="P2-56 release final publish archive markdown path", + ) + parser.add_argument( + "--gate-manifest-drift-json-output", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json", + help="P2-57 gate manifest drift closure JSON path", + ) + parser.add_argument( + "--gate-manifest-drift-markdown-output", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.md", + help="P2-57 gate manifest drift closure markdown path", + ) + parser.add_argument( + "--terminal-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json", + help="P2-59 terminal verdict closure JSON path", + ) + parser.add_argument( + "--terminal-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md", + help="P2-59 terminal verdict closure markdown path", + ) + parser.add_argument( + "--linux-validation-dispatch-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json", + help="P2-60 Linux validation dispatch JSON path", + ) + parser.add_argument( + "--linux-validation-dispatch-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md", + help="P2-60 Linux validation dispatch markdown path", + ) + parser.add_argument( + "--linux-validation-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json", + help="P2-61 Linux validation verdict JSON path", + ) + parser.add_argument( + "--linux-validation-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md", + help="P2-61 Linux validation verdict markdown path", + ) + parser.add_argument( + "--linux-validation-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json", + help="P2-62 Linux validation verdict publish JSON path", + ) + parser.add_argument( + "--linux-validation-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md", + help="P2-62 Linux validation verdict publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json", + help="P2-63 Linux validation terminal publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md", + help="P2-63 Linux validation terminal publish markdown path", + ) + parser.add_argument( + "--linux-validation-final-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json", + help="P2-64 Linux validation final verdict JSON path", + ) + parser.add_argument( + "--linux-validation-final-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md", + help="P2-64 Linux validation final verdict markdown path", + ) + parser.add_argument( + "--linux-validation-final-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json", + help="P2-65 Linux validation final verdict publish JSON path", + ) + parser.add_argument( + "--linux-validation-final-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md", + help="P2-65 Linux validation final verdict publish markdown path", + ) + parser.add_argument( + "--linux-validation-final-publish-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json", + help="P2-66 Linux validation final publish archive JSON path", + ) + parser.add_argument( + "--linux-validation-final-publish-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md", + help="P2-66 Linux validation final publish archive markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json", + help="P2-67 Linux validation terminal verdict JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md", + help="P2-67 Linux validation terminal verdict markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json", + help="P2-68 Linux validation terminal verdict publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md", + help="P2-68 Linux validation terminal verdict publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json", + help="P2-69 Linux validation terminal dispatch JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md", + help="P2-69 Linux validation terminal dispatch markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-execution-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json", + help="P2-70 Linux validation terminal dispatch execution JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-execution-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md", + help="P2-70 Linux validation terminal dispatch execution markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-trace-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json", + help="P2-71 Linux validation terminal dispatch trace JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-trace-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md", + help="P2-71 Linux validation terminal dispatch trace markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-completion-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json", + help="P2-72 Linux validation terminal dispatch completion JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-completion-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md", + help="P2-72 Linux validation terminal dispatch completion markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + help="P2-73 Linux validation terminal dispatch terminal publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md", + help="P2-73 Linux validation terminal dispatch terminal publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + help="P2-74 Linux validation terminal dispatch final verdict JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md", + help="P2-74 Linux validation terminal dispatch final verdict markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + help="P2-75 Linux validation terminal dispatch final verdict publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md", + help="P2-75 Linux validation terminal dispatch final verdict publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-publish-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + help="P2-76 Linux validation terminal dispatch final publish archive JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-publish-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md", + help="P2-76 Linux validation terminal dispatch final publish archive markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json", + help="P2-77 Linux validation terminal dispatch terminal verdict JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md", + help="P2-77 Linux validation terminal dispatch terminal verdict markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json", + help="P2-78 Linux validation terminal dispatch terminal verdict publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md", + help="P2-78 Linux validation terminal dispatch terminal verdict publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json", + help="P2-79 Linux validation terminal dispatch terminal verdict publish archive JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md", + help="P2-79 Linux validation terminal dispatch terminal verdict publish archive markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json", + help="P2-80 Linux validation terminal dispatch terminal verdict closure JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md", + help="P2-80 Linux validation terminal dispatch terminal verdict closure markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json", + help="P2-81 Linux validation terminal dispatch terminal verdict closure publish JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md", + help="P2-81 Linux validation terminal dispatch terminal verdict closure publish markdown path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-json-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json", + help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive JSON path", + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-markdown-output", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md", + help="P2-82 Linux validation terminal dispatch terminal verdict closure publish archive markdown path", + ) + parser.add_argument( + "--workflow-name", + default="Linux Unified Gate", + help="Workflow name forwarded to P2-18/P2-19/P2-21", + ) + parser.add_argument( + "--python-version", + default="3.11", + help="Python version forwarded to P2-18/P2-19/P2-21", + ) + parser.add_argument( + "--artifact-prefix", + default="linux-unified-summary", + help="Artifact prefix forwarded to P2-18/P2-19/P2-21", + ) + parser.add_argument( + "--target-environment", + default="production", + help="P2-30 release target environment", + ) + parser.add_argument( + "--release-channel", + default="stable", + help="P2-30 release channel", + ) + parser.add_argument( + "--workflow-ref", + default="main", + help="Workflow ref used in P2-24 dispatch command generation", + ) + parser.add_argument( + "--release-workflow-path", + default=".github/workflows/release.yml", + help="Release workflow path used in P2-31 release trigger gate", + ) + parser.add_argument( + "--release-workflow-ref", + default="main", + help="Release workflow ref used in P2-31 release trigger gate", + ) + parser.add_argument( + "--release-timeout-seconds", + type=int, + default=900, + help="P2-31 release trigger command timeout in seconds", + ) + parser.add_argument( + "--release-command", + default="", + help="Optional explicit release trigger command string for P2-31", + ) + parser.add_argument( + "--release-trace-poll-timeout-seconds", + type=int, + default=600, + help="P2-32 release run poll command timeout in seconds", + ) + parser.add_argument( + "--release-completion-poll-interval-seconds", + type=int, + default=30, + help="P2-33 release completion poll loop sleep interval in seconds", + ) + parser.add_argument( + "--release-completion-max-polls", + type=int, + default=20, + help="P2-33 release completion maximum poll attempts", + ) + parser.add_argument( + "--release-completion-poll-timeout-seconds", + type=int, + default=600, + help="P2-33 per-poll command timeout in seconds", + ) + parser.add_argument( + "--incident-timeout-seconds", + type=int, + default=600, + help="P2-39 incident command timeout in seconds", + ) + parser.add_argument( + "--incident-command", + default="", + help="Optional explicit incident command string for P2-39", + ) + parser.add_argument( + "--incident-repo", + default="", + help="Optional incident target repo (OWNER/REPO) for P2-39", + ) + parser.add_argument( + "--incident-label", + default="release-incident", + help="Incident label for P2-39 auto-built command", + ) + parser.add_argument( + "--incident-title-prefix", + default="[release-incident]", + help="Incident title prefix for P2-39 auto-built command", + ) + parser.add_argument( + "--follow-up-timeout-seconds", + type=int, + default=600, + help="P2-45 follow-up command timeout in seconds", + ) + parser.add_argument( + "--follow-up-command", + default="", + help="Optional explicit follow-up command string for P2-45", + ) + parser.add_argument( + "--follow-up-repo", + default="", + help="Optional follow-up target repo (OWNER/REPO) for P2-45", + ) + parser.add_argument( + "--follow-up-label", + default="release-follow-up", + help="Follow-up label for P2-45 auto-built command", + ) + parser.add_argument( + "--follow-up-title-prefix", + default="[release-follow-up]", + help="Follow-up title prefix for P2-45 auto-built command", + ) + parser.add_argument( + "--on-block", + choices=("fail", "skip"), + default="fail", + help="P2-23 blocked policy: fail or skip", + ) + parser.add_argument( + "--dispatch-timeout-seconds", + type=int, + default=300, + help="P2-25 dispatch command timeout in seconds", + ) + parser.add_argument( + "--trace-poll-timeout-seconds", + type=int, + default=600, + help="P2-27 run poll command timeout in seconds", + ) + parser.add_argument( + "--completion-poll-interval-seconds", + type=int, + default=30, + help="P2-28 poll loop sleep interval in seconds", + ) + parser.add_argument( + "--completion-max-polls", + type=int, + default=20, + help="P2-28 maximum poll attempts", + ) + parser.add_argument( + "--completion-poll-timeout-seconds", + type=int, + default=600, + help="P2-28 per-poll command timeout in seconds", + ) + parser.add_argument( + "--linux-validation-timeout-seconds", + type=int, + default=7200, + help="P2-60 Linux validation dispatch command timeout in seconds", + ) + parser.add_argument( + "--strict-generated-at", + action="store_true", + help="Forward strict generated_at drift check into P2-19/P2-21", + ) + parser.add_argument( + "--sync-write", + action="store_true", + help="Forward --write into P2-19 sync gate", + ) + parser.add_argument( + "--command-guard-write", + action="store_true", + help="Forward --write into P2-20 command guard gate", + ) + parser.add_argument( + "--dispatch-trace-poll-now", + action="store_true", + help="Forward --poll-now into P2-27 dispatch trace gate", + ) + parser.add_argument( + "--release-trace-poll-now", + action="store_true", + help="Forward --poll-now into P2-32 release trace gate", + ) + parser.add_argument( + "--completion-allow-in-progress", + action="store_true", + help="Forward --allow-in-progress into P2-28 dispatch completion gate", + ) + parser.add_argument( + "--release-completion-allow-in-progress", + action="store_true", + help="Forward --allow-in-progress into P2-33 release completion gate", + ) + parser.add_argument( + "--on-release-hold", + choices=("pass", "fail"), + default="pass", + help="P2-35 hold policy for blocked/in_progress finalization paths", + ) + parser.add_argument( + "--skip-plan", + action="store_true", + help="Skip P2-17 workflow plan stage", + ) + parser.add_argument( + "--skip-yaml", + action="store_true", + help="Skip P2-18 workflow YAML stage", + ) + parser.add_argument( + "--skip-sync", + action="store_true", + help="Skip P2-19 sync stage", + ) + parser.add_argument( + "--skip-command-guard", + action="store_true", + help="Skip P2-20 command guard stage", + ) + parser.add_argument( + "--skip-governance", + action="store_true", + help="Skip P2-21 governance stage", + ) + parser.add_argument( + "--skip-governance-publish", + action="store_true", + help="Skip P2-22 governance publish stage", + ) + parser.add_argument( + "--skip-decision", + action="store_true", + help="Skip P2-23 execution decision stage", + ) + parser.add_argument( + "--skip-dispatch", + action="store_true", + help="Skip P2-24 dispatch stage", + ) + parser.add_argument( + "--skip-dispatch-execution", + action="store_true", + help="Skip P2-25 dispatch execution stage", + ) + parser.add_argument( + "--skip-dispatch-trace", + action="store_true", + help="Skip P2-27 dispatch traceability stage", + ) + parser.add_argument( + "--skip-dispatch-completion", + action="store_true", + help="Skip P2-28 dispatch completion await stage", + ) + parser.add_argument( + "--skip-dispatch-terminal-publish", + action="store_true", + help="Skip P2-29 terminal publish stage", + ) + parser.add_argument( + "--skip-dispatch-release-handoff", + action="store_true", + help="Skip P2-30 release handoff stage", + ) + parser.add_argument( + "--skip-release-trigger", + action="store_true", + help="Skip P2-31 release trigger stage", + ) + parser.add_argument( + "--skip-release-trace", + action="store_true", + help="Skip P2-32 release trace stage", + ) + parser.add_argument( + "--skip-release-completion", + action="store_true", + help="Skip P2-33 release completion stage", + ) + parser.add_argument( + "--skip-release-terminal-publish", + action="store_true", + help="Skip P2-34 release terminal publish stage", + ) + parser.add_argument( + "--skip-release-finalization", + action="store_true", + help="Skip P2-35 release finalization stage", + ) + parser.add_argument( + "--skip-release-closure", + action="store_true", + help="Skip P2-36 release closure stage", + ) + parser.add_argument( + "--skip-release-archive", + action="store_true", + help="Skip P2-37 release archive stage", + ) + parser.add_argument( + "--skip-release-verdict", + action="store_true", + help="Skip P2-38 release verdict stage", + ) + parser.add_argument( + "--skip-release-incident", + action="store_true", + help="Skip P2-39 release incident stage", + ) + parser.add_argument( + "--skip-release-terminal-verdict", + action="store_true", + help="Skip P2-40 release terminal verdict stage", + ) + parser.add_argument( + "--skip-release-delivery", + action="store_true", + help="Skip P2-41 release delivery stage", + ) + parser.add_argument( + "--skip-release-delivery-terminal-publish", + action="store_true", + help="Skip P2-42 release delivery terminal publish stage", + ) + parser.add_argument( + "--skip-release-delivery-final-verdict", + action="store_true", + help="Skip P2-43 release delivery final verdict stage", + ) + parser.add_argument( + "--skip-release-follow-up-dispatch", + action="store_true", + help="Skip P2-44 release follow-up dispatch stage", + ) + parser.add_argument( + "--skip-release-follow-up-closure", + action="store_true", + help="Skip P2-45 release follow-up closure stage", + ) + parser.add_argument( + "--skip-release-follow-up-terminal-publish", + action="store_true", + help="Skip P2-46 release follow-up terminal publish stage", + ) + parser.add_argument( + "--skip-release-follow-up-final-verdict", + action="store_true", + help="Skip P2-47 release follow-up final verdict stage", + ) + parser.add_argument( + "--skip-release-final-outcome", + action="store_true", + help="Skip P2-48 release final outcome stage", + ) + parser.add_argument( + "--skip-release-final-terminal-publish", + action="store_true", + help="Skip P2-49 release final terminal publish stage", + ) + parser.add_argument( + "--skip-release-final-handoff", + action="store_true", + help="Skip P2-50 release final handoff stage", + ) + parser.add_argument( + "--skip-release-final-closure", + action="store_true", + help="Skip P2-51 release final closure stage", + ) + parser.add_argument( + "--skip-release-final-closure-publish", + action="store_true", + help="Skip P2-52 release final closure publish stage", + ) + parser.add_argument( + "--skip-release-final-archive", + action="store_true", + help="Skip P2-53 release final archive stage", + ) + parser.add_argument( + "--skip-release-final-verdict", + action="store_true", + help="Skip P2-54 release final verdict stage", + ) + parser.add_argument( + "--skip-release-final-verdict-publish", + action="store_true", + help="Skip P2-55 release final verdict publish stage", + ) + parser.add_argument( + "--skip-release-final-publish-archive", + action="store_true", + help="Skip P2-56 release final publish archive stage", + ) + parser.add_argument( + "--skip-gate-manifest-drift", + action="store_true", + help="Skip P2-57 gate manifest drift closure stage", + ) + parser.add_argument( + "--skip-terminal-verdict", + action="store_true", + help="Skip P2-59 terminal verdict closure stage", + ) + parser.add_argument( + "--skip-linux-validation-dispatch", + action="store_true", + help="Skip P2-60 Linux validation dispatch stage", + ) + parser.add_argument( + "--skip-linux-validation-verdict", + action="store_true", + help="Skip P2-61 Linux validation verdict stage", + ) + parser.add_argument( + "--skip-linux-validation-verdict-publish", + action="store_true", + help="Skip P2-62 Linux validation verdict publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-publish", + action="store_true", + help="Skip P2-63 Linux validation terminal publish stage", + ) + parser.add_argument( + "--skip-linux-validation-final-verdict", + action="store_true", + help="Skip P2-64 Linux validation final verdict stage", + ) + parser.add_argument( + "--skip-linux-validation-final-verdict-publish", + action="store_true", + help="Skip P2-65 Linux validation final verdict publish stage", + ) + parser.add_argument( + "--skip-linux-validation-final-publish-archive", + action="store_true", + help="Skip P2-66 Linux validation final publish archive stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-verdict", + action="store_true", + help="Skip P2-67 Linux validation terminal verdict stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-verdict-publish", + action="store_true", + help="Skip P2-68 Linux validation terminal verdict publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch", + action="store_true", + help="Skip P2-69 Linux validation terminal dispatch stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-execution", + action="store_true", + help="Skip P2-70 Linux validation terminal dispatch execution stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-trace", + action="store_true", + help="Skip P2-71 Linux validation terminal dispatch trace stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-completion", + action="store_true", + help="Skip P2-72 Linux validation terminal dispatch completion stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-publish", + action="store_true", + help="Skip P2-73 Linux validation terminal dispatch terminal publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-final-verdict", + action="store_true", + help="Skip P2-74 Linux validation terminal dispatch final verdict stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-final-verdict-publish", + action="store_true", + help="Skip P2-75 Linux validation terminal dispatch final verdict publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-final-publish-archive", + action="store_true", + help="Skip P2-76 Linux validation terminal dispatch final publish archive stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict", + action="store_true", + help="Skip P2-77 Linux validation terminal dispatch terminal verdict stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-publish", + action="store_true", + help="Skip P2-78 Linux validation terminal dispatch terminal verdict publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-publish-archive", + action="store_true", + help="Skip P2-79 Linux validation terminal dispatch terminal verdict publish archive stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure", + action="store_true", + help="Skip P2-80 Linux validation terminal dispatch terminal verdict closure stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure-publish", + action="store_true", + help="Skip P2-81 Linux validation terminal dispatch terminal verdict closure publish stage", + ) + parser.add_argument( + "--skip-linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive", + action="store_true", + help="Skip P2-82 Linux validation terminal dispatch terminal verdict closure publish archive stage", + ) + parser.add_argument( + "--print-commands", + action="store_true", + help="Print planned commands", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print planned commands and exit", + ) + parser.add_argument( + "--fail-fast", + action="store_true", + help="Stop at first non-zero stage exit code", + ) + args = parser.parse_args() + + if not args.skip_linux_validation_terminal_verdict: + args.skip_linux_validation_terminal_verdict = bool( + args.skip_linux_validation_final_publish_archive + ) + if not args.skip_linux_validation_terminal_verdict_publish: + args.skip_linux_validation_terminal_verdict_publish = bool( + args.skip_linux_validation_terminal_verdict + ) + if not args.skip_linux_validation_terminal_dispatch: + args.skip_linux_validation_terminal_dispatch = bool( + args.skip_linux_validation_terminal_verdict_publish + ) + if not args.skip_linux_validation_terminal_dispatch_execution: + args.skip_linux_validation_terminal_dispatch_execution = bool( + args.skip_linux_validation_terminal_dispatch + ) + if not args.skip_linux_validation_terminal_dispatch_trace: + args.skip_linux_validation_terminal_dispatch_trace = bool( + args.skip_linux_validation_terminal_dispatch_execution + ) + if not args.skip_linux_validation_terminal_dispatch_completion: + args.skip_linux_validation_terminal_dispatch_completion = bool( + args.skip_linux_validation_terminal_dispatch_trace + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_publish: + args.skip_linux_validation_terminal_dispatch_terminal_publish = bool( + args.skip_linux_validation_terminal_dispatch_completion + ) + if not args.skip_linux_validation_terminal_dispatch_final_verdict: + args.skip_linux_validation_terminal_dispatch_final_verdict = bool( + args.skip_linux_validation_terminal_dispatch_terminal_publish + ) + if not args.skip_linux_validation_terminal_dispatch_final_verdict_publish: + args.skip_linux_validation_terminal_dispatch_final_verdict_publish = bool( + args.skip_linux_validation_terminal_dispatch_final_verdict + ) + if not args.skip_linux_validation_terminal_dispatch_final_publish_archive: + args.skip_linux_validation_terminal_dispatch_final_publish_archive = bool( + args.skip_linux_validation_terminal_dispatch_final_verdict_publish + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_verdict: + args.skip_linux_validation_terminal_dispatch_terminal_verdict = bool( + args.skip_linux_validation_terminal_dispatch_final_publish_archive + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish: + args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish = bool( + args.skip_linux_validation_terminal_dispatch_terminal_verdict + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive: + args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive = bool( + args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure: + args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure = bool( + args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive + ) + if not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish: + args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish = bool( + args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure + ) + if ( + not args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive + ): + args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive = bool( + args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish + ) + + if args.dispatch_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --dispatch-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.release_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --release-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.release_trace_poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--release-trace-poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.trace_poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --trace-poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.completion_poll_interval_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --completion-poll-interval-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.completion_max_polls < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --completion-max-polls must be >= 1", + file=sys.stderr, + ) + return 2 + if args.completion_poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] --completion-poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.release_completion_poll_interval_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--release-completion-poll-interval-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.release_completion_max_polls < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--release-completion-max-polls must be >= 1", + file=sys.stderr, + ) + return 2 + if args.release_completion_poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--release-completion-poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.incident_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--incident-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.follow_up_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--follow-up-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.linux_validation_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-pipeline-gate] " + "--linux-validation-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + pipeline = build_ci_workflow_pipeline_commands( + python_executable=args.python_executable, + ci_matrix_path=Path(args.ci_matrix), + workflow_plan_output=Path(args.workflow_plan_output), + workflow_path=Path(args.workflow_path), + metadata_path=Path(args.metadata_path), + command_guard_output=Path(args.command_guard_output), + governance_report_output=Path(args.governance_report_output), + governance_publish_json_output=Path(args.governance_publish_json_output), + governance_publish_markdown_output=Path(args.governance_publish_markdown_output), + execution_decision_json_output=Path(args.execution_decision_json_output), + execution_decision_markdown_output=Path(args.execution_decision_markdown_output), + dispatch_json_output=Path(args.dispatch_json_output), + dispatch_markdown_output=Path(args.dispatch_markdown_output), + dispatch_execution_json_output=Path(args.dispatch_execution_json_output), + dispatch_execution_markdown_output=Path(args.dispatch_execution_markdown_output), + dispatch_trace_json_output=Path(args.dispatch_trace_json_output), + dispatch_trace_markdown_output=Path(args.dispatch_trace_markdown_output), + dispatch_completion_json_output=Path(args.dispatch_completion_json_output), + dispatch_completion_markdown_output=Path(args.dispatch_completion_markdown_output), + terminal_publish_json_output=Path(args.dispatch_terminal_publish_json_output), + terminal_publish_markdown_output=Path(args.dispatch_terminal_publish_markdown_output), + release_handoff_json_output=Path(args.dispatch_release_handoff_json_output), + release_handoff_markdown_output=Path(args.dispatch_release_handoff_markdown_output), + release_trigger_json_output=Path(args.release_trigger_json_output), + release_trigger_markdown_output=Path(args.release_trigger_markdown_output), + release_trace_json_output=Path(args.release_trace_json_output), + release_trace_markdown_output=Path(args.release_trace_markdown_output), + release_completion_json_output=Path(args.release_completion_json_output), + release_completion_markdown_output=Path(args.release_completion_markdown_output), + release_terminal_publish_json_output=Path(args.release_terminal_publish_json_output), + release_terminal_publish_markdown_output=Path( + args.release_terminal_publish_markdown_output + ), + release_finalization_json_output=Path(args.release_finalization_json_output), + release_finalization_markdown_output=Path(args.release_finalization_markdown_output), + release_closure_json_output=Path(args.release_closure_json_output), + release_closure_markdown_output=Path(args.release_closure_markdown_output), + release_archive_json_output=Path(args.release_archive_json_output), + release_archive_markdown_output=Path(args.release_archive_markdown_output), + release_verdict_json_output=Path(args.release_verdict_json_output), + release_verdict_markdown_output=Path(args.release_verdict_markdown_output), + release_incident_json_output=Path(args.release_incident_json_output), + release_incident_markdown_output=Path(args.release_incident_markdown_output), + release_terminal_verdict_json_output=Path(args.release_terminal_verdict_json_output), + release_terminal_verdict_markdown_output=Path( + args.release_terminal_verdict_markdown_output + ), + release_delivery_json_output=Path(args.release_delivery_json_output), + release_delivery_markdown_output=Path(args.release_delivery_markdown_output), + release_delivery_terminal_publish_json_output=Path( + args.release_delivery_terminal_publish_json_output + ), + release_delivery_terminal_publish_markdown_output=Path( + args.release_delivery_terminal_publish_markdown_output + ), + release_delivery_final_verdict_json_output=Path( + args.release_delivery_final_verdict_json_output + ), + release_delivery_final_verdict_markdown_output=Path( + args.release_delivery_final_verdict_markdown_output + ), + release_follow_up_dispatch_json_output=Path( + args.release_follow_up_dispatch_json_output + ), + release_follow_up_dispatch_markdown_output=Path( + args.release_follow_up_dispatch_markdown_output + ), + release_follow_up_closure_json_output=Path( + args.release_follow_up_closure_json_output + ), + release_follow_up_closure_markdown_output=Path( + args.release_follow_up_closure_markdown_output + ), + release_follow_up_terminal_publish_json_output=Path( + args.release_follow_up_terminal_publish_json_output + ), + release_follow_up_terminal_publish_markdown_output=Path( + args.release_follow_up_terminal_publish_markdown_output + ), + release_follow_up_final_verdict_json_output=Path( + args.release_follow_up_final_verdict_json_output + ), + release_follow_up_final_verdict_markdown_output=Path( + args.release_follow_up_final_verdict_markdown_output + ), + release_final_outcome_json_output=Path(args.release_final_outcome_json_output), + release_final_outcome_markdown_output=Path(args.release_final_outcome_markdown_output), + release_final_terminal_publish_json_output=Path( + args.release_final_terminal_publish_json_output + ), + release_final_terminal_publish_markdown_output=Path( + args.release_final_terminal_publish_markdown_output + ), + release_final_handoff_json_output=Path(args.release_final_handoff_json_output), + release_final_handoff_markdown_output=Path(args.release_final_handoff_markdown_output), + release_final_closure_json_output=Path(args.release_final_closure_json_output), + release_final_closure_markdown_output=Path(args.release_final_closure_markdown_output), + release_final_closure_publish_json_output=Path( + args.release_final_closure_publish_json_output + ), + release_final_closure_publish_markdown_output=Path( + args.release_final_closure_publish_markdown_output + ), + release_final_archive_json_output=Path(args.release_final_archive_json_output), + release_final_archive_markdown_output=Path(args.release_final_archive_markdown_output), + release_final_verdict_json_output=Path(args.release_final_verdict_json_output), + release_final_verdict_markdown_output=Path(args.release_final_verdict_markdown_output), + release_final_verdict_publish_json_output=Path( + args.release_final_verdict_publish_json_output + ), + release_final_verdict_publish_markdown_output=Path( + args.release_final_verdict_publish_markdown_output + ), + release_final_publish_archive_json_output=Path( + args.release_final_publish_archive_json_output + ), + release_final_publish_archive_markdown_output=Path( + args.release_final_publish_archive_markdown_output + ), + gate_manifest_drift_json_output=Path(args.gate_manifest_drift_json_output), + gate_manifest_drift_markdown_output=Path(args.gate_manifest_drift_markdown_output), + terminal_verdict_json_output=Path(args.terminal_verdict_json_output), + terminal_verdict_markdown_output=Path(args.terminal_verdict_markdown_output), + linux_validation_dispatch_json_output=Path( + args.linux_validation_dispatch_json_output + ), + linux_validation_dispatch_markdown_output=Path( + args.linux_validation_dispatch_markdown_output + ), + linux_validation_verdict_json_output=Path( + args.linux_validation_verdict_json_output + ), + linux_validation_verdict_markdown_output=Path( + args.linux_validation_verdict_markdown_output + ), + linux_validation_verdict_publish_json_output=Path( + args.linux_validation_verdict_publish_json_output + ), + linux_validation_verdict_publish_markdown_output=Path( + args.linux_validation_verdict_publish_markdown_output + ), + linux_validation_terminal_publish_json_output=Path( + args.linux_validation_terminal_publish_json_output + ), + linux_validation_terminal_publish_markdown_output=Path( + args.linux_validation_terminal_publish_markdown_output + ), + linux_validation_final_verdict_json_output=Path( + args.linux_validation_final_verdict_json_output + ), + linux_validation_final_verdict_markdown_output=Path( + args.linux_validation_final_verdict_markdown_output + ), + linux_validation_final_verdict_publish_json_output=Path( + args.linux_validation_final_verdict_publish_json_output + ), + linux_validation_final_verdict_publish_markdown_output=Path( + args.linux_validation_final_verdict_publish_markdown_output + ), + linux_validation_final_publish_archive_json_output=Path( + args.linux_validation_final_publish_archive_json_output + ), + linux_validation_final_publish_archive_markdown_output=Path( + args.linux_validation_final_publish_archive_markdown_output + ), + linux_validation_terminal_verdict_json_output=Path( + args.linux_validation_terminal_verdict_json_output + ), + linux_validation_terminal_verdict_markdown_output=Path( + args.linux_validation_terminal_verdict_markdown_output + ), + linux_validation_terminal_verdict_publish_json_output=Path( + args.linux_validation_terminal_verdict_publish_json_output + ), + linux_validation_terminal_verdict_publish_markdown_output=Path( + args.linux_validation_terminal_verdict_publish_markdown_output + ), + linux_validation_terminal_dispatch_json_output=Path( + args.linux_validation_terminal_dispatch_json_output + ), + linux_validation_terminal_dispatch_markdown_output=Path( + args.linux_validation_terminal_dispatch_markdown_output + ), + linux_validation_terminal_dispatch_execution_json_output=Path( + args.linux_validation_terminal_dispatch_execution_json_output + ), + linux_validation_terminal_dispatch_execution_markdown_output=Path( + args.linux_validation_terminal_dispatch_execution_markdown_output + ), + linux_validation_terminal_dispatch_trace_json_output=Path( + args.linux_validation_terminal_dispatch_trace_json_output + ), + linux_validation_terminal_dispatch_trace_markdown_output=Path( + args.linux_validation_terminal_dispatch_trace_markdown_output + ), + linux_validation_terminal_dispatch_completion_json_output=Path( + args.linux_validation_terminal_dispatch_completion_json_output + ), + linux_validation_terminal_dispatch_completion_markdown_output=Path( + args.linux_validation_terminal_dispatch_completion_markdown_output + ), + linux_validation_terminal_dispatch_terminal_publish_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_publish_json_output + ), + linux_validation_terminal_dispatch_terminal_publish_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_publish_markdown_output + ), + linux_validation_terminal_dispatch_final_verdict_json_output=Path( + args.linux_validation_terminal_dispatch_final_verdict_json_output + ), + linux_validation_terminal_dispatch_final_verdict_markdown_output=Path( + args.linux_validation_terminal_dispatch_final_verdict_markdown_output + ), + linux_validation_terminal_dispatch_final_verdict_publish_json_output=Path( + args.linux_validation_terminal_dispatch_final_verdict_publish_json_output + ), + linux_validation_terminal_dispatch_final_verdict_publish_markdown_output=Path( + args.linux_validation_terminal_dispatch_final_verdict_publish_markdown_output + ), + linux_validation_terminal_dispatch_final_publish_archive_json_output=Path( + args.linux_validation_terminal_dispatch_final_publish_archive_json_output + ), + linux_validation_terminal_dispatch_final_publish_archive_markdown_output=Path( + args.linux_validation_terminal_dispatch_final_publish_archive_markdown_output + ), + workflow_name=args.workflow_name, + linux_validation_terminal_dispatch_terminal_verdict_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_markdown_output + ), + linux_validation_terminal_dispatch_terminal_verdict_publish_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output + ), + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output + ), + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output=Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output + ), + python_version=args.python_version, + artifact_prefix=args.artifact_prefix, + target_environment=args.target_environment, + release_channel=args.release_channel, + workflow_ref=args.workflow_ref, + release_workflow_path=args.release_workflow_path, + release_workflow_ref=args.release_workflow_ref, + release_timeout_seconds=args.release_timeout_seconds, + release_command=args.release_command, + release_trace_poll_timeout_seconds=args.release_trace_poll_timeout_seconds, + release_completion_poll_interval_seconds=args.release_completion_poll_interval_seconds, + release_completion_max_polls=args.release_completion_max_polls, + release_completion_poll_timeout_seconds=args.release_completion_poll_timeout_seconds, + incident_timeout_seconds=args.incident_timeout_seconds, + incident_command=args.incident_command, + incident_repo=args.incident_repo, + incident_label=args.incident_label, + incident_title_prefix=args.incident_title_prefix, + follow_up_timeout_seconds=args.follow_up_timeout_seconds, + follow_up_command=args.follow_up_command, + follow_up_repo=args.follow_up_repo, + follow_up_label=args.follow_up_label, + follow_up_title_prefix=args.follow_up_title_prefix, + on_block_policy=args.on_block, + dispatch_timeout_seconds=args.dispatch_timeout_seconds, + trace_poll_timeout_seconds=args.trace_poll_timeout_seconds, + completion_poll_interval_seconds=args.completion_poll_interval_seconds, + completion_max_polls=args.completion_max_polls, + completion_poll_timeout_seconds=args.completion_poll_timeout_seconds, + linux_validation_timeout_seconds=args.linux_validation_timeout_seconds, + strict_generated_at=args.strict_generated_at, + sync_write=args.sync_write, + command_guard_write=args.command_guard_write, + dispatch_trace_poll_now=args.dispatch_trace_poll_now, + release_trace_poll_now=args.release_trace_poll_now, + completion_allow_in_progress=args.completion_allow_in_progress, + release_completion_allow_in_progress=args.release_completion_allow_in_progress, + on_release_hold_policy=args.on_release_hold, + skip_plan=args.skip_plan, + skip_yaml=args.skip_yaml, + skip_sync=args.skip_sync, + skip_command_guard=args.skip_command_guard, + skip_governance=args.skip_governance, + skip_governance_publish=args.skip_governance_publish, + skip_decision=args.skip_decision, + skip_dispatch=args.skip_dispatch, + skip_dispatch_execution=args.skip_dispatch_execution, + skip_dispatch_trace=args.skip_dispatch_trace, + skip_dispatch_completion=args.skip_dispatch_completion, + skip_dispatch_terminal_publish=args.skip_dispatch_terminal_publish, + skip_dispatch_release_handoff=args.skip_dispatch_release_handoff, + skip_release_trigger=args.skip_release_trigger, + skip_release_trace=args.skip_release_trace, + skip_release_completion=args.skip_release_completion, + skip_release_terminal_publish=args.skip_release_terminal_publish, + skip_release_finalization=args.skip_release_finalization, + skip_release_closure=args.skip_release_closure, + skip_release_archive=args.skip_release_archive, + skip_release_verdict=args.skip_release_verdict, + skip_release_incident=args.skip_release_incident, + skip_release_terminal_verdict=args.skip_release_terminal_verdict, + skip_release_delivery=args.skip_release_delivery, + skip_release_delivery_terminal_publish=args.skip_release_delivery_terminal_publish, + skip_release_delivery_final_verdict=args.skip_release_delivery_final_verdict, + skip_release_follow_up_dispatch=args.skip_release_follow_up_dispatch, + skip_release_follow_up_closure=args.skip_release_follow_up_closure, + skip_release_follow_up_terminal_publish=args.skip_release_follow_up_terminal_publish, + skip_release_follow_up_final_verdict=args.skip_release_follow_up_final_verdict, + skip_release_final_outcome=args.skip_release_final_outcome, + skip_release_final_terminal_publish=args.skip_release_final_terminal_publish, + skip_release_final_handoff=args.skip_release_final_handoff, + skip_release_final_closure=args.skip_release_final_closure, + skip_release_final_closure_publish=args.skip_release_final_closure_publish, + skip_release_final_archive=args.skip_release_final_archive, + skip_release_final_verdict=args.skip_release_final_verdict, + skip_release_final_verdict_publish=args.skip_release_final_verdict_publish, + skip_release_final_publish_archive=args.skip_release_final_publish_archive, + skip_gate_manifest_drift=args.skip_gate_manifest_drift, + skip_terminal_verdict=args.skip_terminal_verdict, + skip_linux_validation_dispatch=args.skip_linux_validation_dispatch, + skip_linux_validation_verdict=args.skip_linux_validation_verdict, + skip_linux_validation_verdict_publish=args.skip_linux_validation_verdict_publish, + skip_linux_validation_terminal_publish=args.skip_linux_validation_terminal_publish, + skip_linux_validation_final_verdict=args.skip_linux_validation_final_verdict, + skip_linux_validation_final_verdict_publish=args.skip_linux_validation_final_verdict_publish, + skip_linux_validation_final_publish_archive=args.skip_linux_validation_final_publish_archive, + skip_linux_validation_terminal_verdict=args.skip_linux_validation_terminal_verdict, + skip_linux_validation_terminal_verdict_publish=args.skip_linux_validation_terminal_verdict_publish, + skip_linux_validation_terminal_dispatch=args.skip_linux_validation_terminal_dispatch, + skip_linux_validation_terminal_dispatch_execution=args.skip_linux_validation_terminal_dispatch_execution, + skip_linux_validation_terminal_dispatch_trace=args.skip_linux_validation_terminal_dispatch_trace, + skip_linux_validation_terminal_dispatch_completion=args.skip_linux_validation_terminal_dispatch_completion, + skip_linux_validation_terminal_dispatch_terminal_publish=args.skip_linux_validation_terminal_dispatch_terminal_publish, + skip_linux_validation_terminal_dispatch_final_verdict=args.skip_linux_validation_terminal_dispatch_final_verdict, + skip_linux_validation_terminal_dispatch_final_verdict_publish=args.skip_linux_validation_terminal_dispatch_final_verdict_publish, + skip_linux_validation_terminal_dispatch_final_publish_archive=args.skip_linux_validation_terminal_dispatch_final_publish_archive, + skip_linux_validation_terminal_dispatch_terminal_verdict=args.skip_linux_validation_terminal_dispatch_terminal_verdict, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=args.skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=args.skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive, + ) + + if not pipeline: + print("[p2-linux-ci-workflow-pipeline-gate] no stages selected") + return 0 + + if args.print_commands or args.dry_run: + for idx, (stage, command) in enumerate(pipeline, start=1): + print(f"{idx}. [{stage}] {_format_shell_command(command)}") + if args.dry_run: + return 0 + + project_root = Path(__file__).resolve().parents[1] + overall_exit = 0 + for idx, (stage, command) in enumerate(pipeline, start=1): + print(f"[{idx}/{len(pipeline)}] stage={stage} {_format_shell_command(command)}") + exit_code = subprocess.call(command, cwd=project_root) + if exit_code != 0: + if overall_exit == 0: + overall_exit = exit_code + if args.fail_fast: + return exit_code + return overall_exit + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_plan_gate.py b/scripts/run_p2_linux_ci_workflow_plan_gate.py new file mode 100644 index 0000000..a1d2de5 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_plan_gate.py @@ -0,0 +1,390 @@ +"""Phase 2 card P2-17 gate for Linux CI workflow plan generation. + +This script converts the P2-16 CI matrix artifact into a workflow-level plan +that Linux CI can consume directly: +1) fan-out shard job descriptors, +2) fan-in aggregation command, +3) fan-in final publish command. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path}: field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def load_ci_matrix(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: ci matrix file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: ci matrix payload must be object") + + required_fields = ( + "shard_total", + "manifest_total_tests", + "selected_shards", + "matrix", + "summary_paths", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + shard_total = _coerce_int(payload["shard_total"], field="shard_total", path=path) + manifest_total_tests = _coerce_int( + payload["manifest_total_tests"], + field="manifest_total_tests", + path=path, + ) + selected_shards = _coerce_int( + payload["selected_shards"], + field="selected_shards", + path=path, + ) + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + if manifest_total_tests < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if selected_shards < 0: + raise ValueError(f"{path}: selected_shards must be >= 0") + + skipped_empty_shards = payload.get("skipped_empty_shards", False) + if not isinstance(skipped_empty_shards, bool): + raise ValueError(f"{path}: field 'skipped_empty_shards' must be bool") + + matrix = payload["matrix"] + if not isinstance(matrix, dict): + raise ValueError(f"{path}: field 'matrix' must be object") + include = matrix.get("include") + if not isinstance(include, list): + raise ValueError(f"{path}: field 'matrix.include' must be list") + + summary_paths = _coerce_str_list( + payload["summary_paths"], + field="summary_paths", + path=path, + ) + + normalized_include: list[dict[str, Any]] = [] + include_summary_paths: list[str] = [] + seen_indexes: set[int] = set() + include_total_tests = 0 + for index, shard in enumerate(include, start=1): + if not isinstance(shard, dict): + raise ValueError(f"{path}: matrix.include[{index - 1}] must be object") + + for required in ( + "shard_index", + "shard_total", + "total_tests", + "report_dir", + "summary_path", + "command", + "command_parts", + ): + if required not in shard: + raise ValueError(f"{path}: matrix.include[{index - 1}] missing field '{required}'") + + shard_index = _coerce_int( + shard["shard_index"], + field=f"matrix.include[{index - 1}].shard_index", + path=path, + ) + nested_total = _coerce_int( + shard["shard_total"], + field=f"matrix.include[{index - 1}].shard_total", + path=path, + ) + total_tests = _coerce_int( + shard["total_tests"], + field=f"matrix.include[{index - 1}].total_tests", + path=path, + ) + report_dir = _coerce_str( + shard["report_dir"], + field=f"matrix.include[{index - 1}].report_dir", + path=path, + ) + summary_path = _coerce_str( + shard["summary_path"], + field=f"matrix.include[{index - 1}].summary_path", + path=path, + ) + command = _coerce_str( + shard["command"], + field=f"matrix.include[{index - 1}].command", + path=path, + ) + command_parts = _coerce_str_list( + shard["command_parts"], + field=f"matrix.include[{index - 1}].command_parts", + path=path, + ) + if shard_index < 1 or shard_index > shard_total: + raise ValueError(f"{path}: matrix.include[{index - 1}].shard_index out of range") + if nested_total != shard_total: + raise ValueError( + f"{path}: matrix.include[{index - 1}].shard_total ({nested_total}) " + f"!= top-level shard_total ({shard_total})" + ) + if shard_index in seen_indexes: + raise ValueError(f"{path}: duplicate shard_index {shard_index} in matrix.include") + if total_tests < 0: + raise ValueError(f"{path}: matrix.include[{index - 1}].total_tests must be >= 0") + + seen_indexes.add(shard_index) + include_total_tests += total_tests + include_summary_paths.append(summary_path) + normalized_include.append( + { + "shard_index": shard_index, + "shard_total": nested_total, + "total_tests": total_tests, + "report_dir": report_dir, + "summary_path": summary_path, + "command": command, + "command_parts": command_parts, + } + ) + + if selected_shards != len(normalized_include): + raise ValueError( + f"{path}: selected_shards ({selected_shards}) " + f"!= matrix.include count ({len(normalized_include)})" + ) + if summary_paths != include_summary_paths: + raise ValueError(f"{path}: summary_paths must match matrix.include[*].summary_path order") + if include_total_tests > manifest_total_tests: + raise ValueError( + f"{path}: matrix.include total_tests ({include_total_tests}) " + f"exceeds manifest_total_tests ({manifest_total_tests})" + ) + if not skipped_empty_shards and include_total_tests != manifest_total_tests: + raise ValueError( + f"{path}: matrix.include total_tests ({include_total_tests}) " + f"must equal manifest_total_tests ({manifest_total_tests}) when skipped_empty_shards is false" + ) + + return { + "generated_at": payload.get("generated_at"), + "source_shard_plan": payload.get("source_shard_plan"), + "shard_total": shard_total, + "manifest_total_tests": manifest_total_tests, + "selected_shards": selected_shards, + "skipped_empty_shards": skipped_empty_shards, + "matrix": {"include": normalized_include}, + "summary_paths": summary_paths, + } + + +def build_ci_workflow_plan_payload( + ci_matrix: dict[str, Any], + *, + source_path: Path, + python_executable: str, + merged_summary_output: Path, + final_report_json: Path, + final_report_markdown: Path, +) -> dict[str, Any]: + fan_out_jobs: list[dict[str, Any]] = [] + for shard in ci_matrix["matrix"]["include"]: + shard_index = int(shard["shard_index"]) + fan_out_jobs.append( + { + "job_id": f"linux-shard-{shard_index}", + "shard_index": shard_index, + "shard_total": int(shard["shard_total"]), + "total_tests": int(shard["total_tests"]), + "report_dir": str(shard["report_dir"]), + "summary_path": str(shard["summary_path"]), + "command": str(shard["command"]), + "command_parts": list(shard["command_parts"]), + } + ) + + summary_paths = [str(path) for path in ci_matrix["summary_paths"]] + aggregation_command_parts = [ + python_executable, + "scripts/run_p2_linux_shard_aggregation_gate.py", + "--output", + str(merged_summary_output), + ] + for summary_path in summary_paths: + aggregation_command_parts.extend(["--summary", summary_path]) + + publish_command_parts = [ + python_executable, + "scripts/run_p2_linux_report_publish_gate.py", + "--merged-summary", + str(merged_summary_output), + "--output-json", + str(final_report_json), + "--output-markdown", + str(final_report_markdown), + ] + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_ci_matrix": str(source_path), + "shard_total": int(ci_matrix["shard_total"]), + "manifest_total_tests": int(ci_matrix["manifest_total_tests"]), + "selected_shards": int(ci_matrix["selected_shards"]), + "skipped_empty_shards": bool(ci_matrix["skipped_empty_shards"]), + "fan_out_matrix": { + "include": fan_out_jobs, + }, + "fan_out_jobs": fan_out_jobs, + "fan_in": { + "summary_paths": summary_paths, + "merged_summary_path": str(merged_summary_output), + "aggregation_command": _format_shell_command(aggregation_command_parts), + "aggregation_command_parts": aggregation_command_parts, + "final_report_json": str(final_report_json), + "final_report_markdown": str(final_report_markdown), + "publish_command": _format_shell_command(publish_command_parts), + "publish_command_parts": publish_command_parts, + }, + } + + +def build_github_output_values(payload: dict[str, Any]) -> dict[str, str]: + return { + "fan_out_matrix": json.dumps(payload["fan_out_matrix"], ensure_ascii=False, separators=(",", ":")), + "fan_in_summary_paths": json.dumps( + payload["fan_in"]["summary_paths"], + ensure_ascii=False, + separators=(",", ":"), + ), + "aggregation_command": str(payload["fan_in"]["aggregation_command"]), + "publish_command": str(payload["fan_in"]["publish_command"]), + "selected_shards": str(payload["selected_shards"]), + "manifest_total_tests": str(payload["manifest_total_tests"]), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate Linux CI workflow plan from P2 CI matrix artifact" + ) + parser.add_argument( + "--ci-matrix", + default=".claude/reports/linux_unified_gate/ci_matrix.json", + help="CI matrix JSON produced by run_p2_linux_ci_matrix_gate.py", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="Output CI workflow plan JSON path", + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable used when generating fan-in commands", + ) + parser.add_argument( + "--merged-summary-output", + default=".claude/reports/linux_unified_gate/merged_summary.json", + help="Merged summary path consumed by aggregation/publish stages", + ) + parser.add_argument( + "--final-report-json", + default=".claude/reports/linux_unified_gate/final_report.json", + help="Final report JSON path used in publish stage command", + ) + parser.add_argument( + "--final-report-markdown", + default=".claude/reports/linux_unified_gate/final_report.md", + help="Final report markdown path used in publish stage command", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-plan", + action="store_true", + help="Print generated CI workflow plan JSON", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Generate and print summary without writing output file", + ) + args = parser.parse_args() + + ci_matrix_path = Path(args.ci_matrix) + try: + ci_matrix = load_ci_matrix(ci_matrix_path) + payload = build_ci_workflow_plan_payload( + ci_matrix, + source_path=ci_matrix_path.resolve(), + python_executable=args.python_executable, + merged_summary_output=Path(args.merged_summary_output), + final_report_json=Path(args.final_report_json), + final_report_markdown=Path(args.final_report_markdown), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-plan-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"ci workflow plan: {output_path}") + + if args.github_output: + write_github_output(Path(args.github_output), build_github_output_values(payload)) + print(f"github output: {args.github_output}") + + print( + "workflow plan summary: " + f"{payload['selected_shards']} fan-out shard job(s), " + f"manifest_total_tests={payload['manifest_total_tests']}" + ) + if args.print_plan or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_archive_gate.py b/scripts/run_p2_linux_ci_workflow_release_archive_gate.py new file mode 100644 index 0000000..f518517 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_archive_gate.py @@ -0,0 +1,477 @@ +"""Phase 2 card P2-37 gate for Linux CI workflow release evidence archive. + +This script consumes the P2-36 release closure artifact and converges +the terminal release archive contract: +1) validate release closure contract consistency, +2) collect release evidence manifest from upstream source reports, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_CLOSURE_STATUSES: set[str] = {"closed", "pending", "failed", "contract_failed"} +ALLOWED_RELEASE_CLOSURE_DECISIONS: set[str] = {"ship", "hold", "rollback"} +ALLOWED_RELEASE_ARCHIVE_STATUSES: set[str] = {"ready", "pending", "failed", "contract_failed"} +ALLOWED_RELEASE_ARCHIVE_DECISIONS: set[str] = {"publish", "hold", "block"} + +SOURCE_FIELDS: tuple[str, ...] = ( + "source_release_finalization_report", + "source_release_terminal_publish_report", + "source_release_completion_report", + "source_release_trace_report", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", +) + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_closure_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release closure report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release closure payload must be object") + + required_fields = ( + "release_closure_status", + "release_closure_decision", + "release_closure_exit_code", + "should_close_release", + "should_notify", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + *SOURCE_FIELDS, + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_closure_status = _coerce_str( + payload["release_closure_status"], + field="release_closure_status", + path=path, + ) + if release_closure_status not in ALLOWED_RELEASE_CLOSURE_STATUSES: + raise ValueError( + f"{path}: field 'release_closure_status' must be one of " + f"{sorted(ALLOWED_RELEASE_CLOSURE_STATUSES)}" + ) + + release_closure_decision = _coerce_str( + payload["release_closure_decision"], + field="release_closure_decision", + path=path, + ) + if release_closure_decision not in ALLOWED_RELEASE_CLOSURE_DECISIONS: + raise ValueError( + f"{path}: field 'release_closure_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_CLOSURE_DECISIONS)}" + ) + + release_closure_exit_code = _coerce_int( + payload["release_closure_exit_code"], + field="release_closure_exit_code", + path=path, + ) + if release_closure_exit_code < 0: + raise ValueError(f"{path}: field 'release_closure_exit_code' must be >= 0") + + should_close_release = _coerce_bool( + payload["should_close_release"], + field="should_close_release", + path=path, + ) + should_notify = _coerce_bool( + payload["should_notify"], + field="should_notify", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_paths = { + field: _coerce_str(payload[field], field=field, path=path) + for field in SOURCE_FIELDS + } + + return { + "generated_at": payload.get("generated_at"), + "release_closure_status": release_closure_status, + "release_closure_decision": release_closure_decision, + "release_closure_exit_code": release_closure_exit_code, + "should_close_release": should_close_release, + "should_notify": should_notify, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + **source_paths, + } + + +def _build_evidence_manifest(release_closure_report: dict[str, Any], source_path: Path) -> list[dict[str, Any]]: + manifest: list[dict[str, Any]] = [] + seen: set[str] = set() + + evidence_paths = [str(source_path)] + [str(release_closure_report[field]) for field in SOURCE_FIELDS] + for evidence_path in evidence_paths: + if evidence_path in seen: + continue + seen.add(evidence_path) + evidence_file = Path(evidence_path) + manifest.append( + { + "path": evidence_path, + "exists": evidence_file.is_file(), + } + ) + return manifest + + +def build_release_archive_payload( + release_closure_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_closure_status = str(release_closure_report["release_closure_status"]) + release_closure_decision = str(release_closure_report["release_closure_decision"]) + release_closure_exit_code = int(release_closure_report["release_closure_exit_code"]) + should_close_release = bool(release_closure_report["should_close_release"]) + should_notify = bool(release_closure_report["should_notify"]) + + reason_codes = list(release_closure_report["reason_codes"]) + structural_issues = list(release_closure_report["structural_issues"]) + missing_artifacts = list(release_closure_report["missing_artifacts"]) + + if release_closure_status == "closed": + if release_closure_decision != "ship": + structural_issues.append("release_closure_decision_mismatch_closed") + if release_closure_exit_code != 0: + structural_issues.append("release_closure_exit_code_mismatch_closed") + if not should_close_release: + structural_issues.append("should_close_release_mismatch_closed") + elif release_closure_status == "pending": + if release_closure_decision != "hold": + structural_issues.append("release_closure_decision_mismatch_pending") + if should_close_release: + structural_issues.append("should_close_release_mismatch_pending") + elif release_closure_status in {"failed", "contract_failed"}: + if release_closure_decision != "rollback": + structural_issues.append("release_closure_decision_mismatch_failed") + if should_close_release: + structural_issues.append("should_close_release_mismatch_failed") + if release_closure_exit_code == 0: + structural_issues.append("release_closure_exit_code_mismatch_failed") + + evidence_manifest = _build_evidence_manifest(release_closure_report, source_path) + manifest_missing_paths = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if manifest_missing_paths: + missing_artifacts.extend(manifest_missing_paths) + reason_codes.append("release_evidence_missing") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + release_archive_status = "contract_failed" + release_archive_decision = "block" + release_archive_exit_code = 1 + should_publish_archive = False + reason_codes.extend(structural_issues) + elif release_closure_status == "closed": + release_archive_status = "ready" + release_archive_decision = "publish" + release_archive_exit_code = 0 + should_publish_archive = True + reason_codes = ["release_archive_ready"] + elif release_closure_status == "pending": + release_archive_status = "pending" + release_archive_decision = "hold" + release_archive_exit_code = max(1, release_closure_exit_code) + should_publish_archive = False + elif release_closure_status == "failed": + release_archive_status = "failed" + release_archive_decision = "block" + release_archive_exit_code = max(1, release_closure_exit_code) + should_publish_archive = False + else: + release_archive_status = "contract_failed" + release_archive_decision = "block" + release_archive_exit_code = 1 + should_publish_archive = False + + if release_archive_status not in ALLOWED_RELEASE_ARCHIVE_STATUSES: + raise ValueError("internal: unsupported release_archive_status computed") + if release_archive_decision not in ALLOWED_RELEASE_ARCHIVE_DECISIONS: + raise ValueError("internal: unsupported release_archive_decision computed") + + release_archive_summary = ( + f"release_closure_status={release_closure_status} " + f"release_archive_status={release_archive_status} " + f"release_archive_decision={release_archive_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_closure_report": str(source_path), + "release_closure_status": release_closure_status, + "release_closure_decision": release_closure_decision, + "release_closure_exit_code": release_closure_exit_code, + "release_archive_status": release_archive_status, + "release_archive_decision": release_archive_decision, + "release_archive_exit_code": int(release_archive_exit_code), + "should_close_release": should_close_release, + "should_notify": should_notify, + "should_publish_archive": should_publish_archive, + "release_run_id": release_closure_report["release_run_id"], + "release_run_url": str(release_closure_report["release_run_url"]), + "release_archive_summary": release_archive_summary, + "reason_codes": _unique(reason_codes), + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Archive Report", + "", + f"- Release Archive Status: **{str(payload['release_archive_status']).upper()}**", + f"- Release Archive Decision: `{payload['release_archive_decision']}`", + f"- Should Publish Archive: `{payload['should_publish_archive']}`", + f"- Release Archive Exit Code: `{payload['release_archive_exit_code']}`", + f"- Release Closure Status: `{payload['release_closure_status']}`", + f"- Release Closure Decision: `{payload['release_closure_decision']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Evidence Manifest"]) + for item in payload["evidence_manifest"]: + exists = "yes" if bool(item["exists"]) else "no" + lines.append(f"- exists={exists} path=`{item['path']}`") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_archive_status": str(payload["release_archive_status"]), + "workflow_release_archive_decision": str(payload["release_archive_decision"]), + "workflow_release_archive_should_publish": ( + "true" if payload["should_publish_archive"] else "false" + ), + "workflow_release_archive_exit_code": str(payload["release_archive_exit_code"]), + "workflow_release_archive_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_archive_run_url": str(payload["release_run_url"]), + "workflow_release_archive_missing_artifacts": json.dumps( + payload["missing_artifacts"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_archive_report_json": str(output_json), + "workflow_release_archive_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release archive contract from P2-36 release closure report" + ) + ) + parser.add_argument( + "--release-closure-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_closure.json", + help="P2-36 release closure report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_archive.json", + help="Output release archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_archive.md", + help="Output release archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_closure_report_path = Path(args.release_closure_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_closure_report = load_release_closure_report(release_closure_report_path) + payload = build_release_archive_payload( + release_closure_report, + source_path=release_closure_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-archive-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release archive json: {output_json_path}") + print(f"release archive markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release archive summary: " + f"release_archive_status={payload['release_archive_status']} " + f"release_archive_decision={payload['release_archive_decision']} " + f"release_archive_exit_code={payload['release_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_archive_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_closure_gate.py b/scripts/run_p2_linux_ci_workflow_release_closure_gate.py new file mode 100644 index 0000000..73cfd34 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_closure_gate.py @@ -0,0 +1,622 @@ +"""Phase 2 card P2-36 gate for Linux CI workflow release closure publishing. + +This script consumes the P2-35 release finalization artifact and converges +the terminal release closure contract: +1) validate release finalization contract consistency, +2) normalize closure decision/status for external consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FINALIZATION_STATUSES: set[str] = { + "finalized", + "awaiting_release", + "failed", + "contract_failed", +} + +ALLOWED_RELEASE_FINALIZATION_DECISIONS: set[str] = {"finalize", "hold", "abort"} +ALLOWED_RELEASE_CLOSURE_STATUSES: set[str] = {"closed", "pending", "failed", "contract_failed"} +ALLOWED_RELEASE_CLOSURE_DECISIONS: set[str] = {"ship", "hold", "rollback"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_finalization_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release finalization report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release finalization payload must be object") + + required_fields = ( + "release_finalization_status", + "release_finalization_decision", + "release_finalization_exit_code", + "should_finalize_release", + "on_hold_policy", + "release_publish_status", + "release_publish_exit_code", + "release_trigger_status", + "release_tracking_status", + "release_completion_status", + "release_completion_exit_code", + "allow_in_progress", + "should_poll_release_run", + "release_run_id", + "release_run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_release_terminal_publish_report", + "source_release_completion_report", + "source_release_trace_report", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_finalization_status = _coerce_str( + payload["release_finalization_status"], + field="release_finalization_status", + path=path, + ) + if release_finalization_status not in ALLOWED_RELEASE_FINALIZATION_STATUSES: + raise ValueError( + f"{path}: field 'release_finalization_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINALIZATION_STATUSES)}" + ) + + release_finalization_decision = _coerce_str( + payload["release_finalization_decision"], + field="release_finalization_decision", + path=path, + ) + if release_finalization_decision not in ALLOWED_RELEASE_FINALIZATION_DECISIONS: + raise ValueError( + f"{path}: field 'release_finalization_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINALIZATION_DECISIONS)}" + ) + + release_finalization_exit_code = _coerce_int( + payload["release_finalization_exit_code"], + field="release_finalization_exit_code", + path=path, + ) + if release_finalization_exit_code < 0: + raise ValueError(f"{path}: field 'release_finalization_exit_code' must be >= 0") + + should_finalize_release = _coerce_bool( + payload["should_finalize_release"], + field="should_finalize_release", + path=path, + ) + on_hold_policy = _coerce_str(payload["on_hold_policy"], field="on_hold_policy", path=path) + if on_hold_policy not in {"pass", "fail"}: + raise ValueError(f"{path}: field 'on_hold_policy' must be 'pass' or 'fail'") + + release_publish_status = _coerce_str( + payload["release_publish_status"], + field="release_publish_status", + path=path, + ) + release_publish_exit_code = _coerce_int( + payload["release_publish_exit_code"], + field="release_publish_exit_code", + path=path, + ) + if release_publish_exit_code < 0: + raise ValueError(f"{path}: field 'release_publish_exit_code' must be >= 0") + + release_trigger_status = _coerce_str( + payload["release_trigger_status"], + field="release_trigger_status", + path=path, + ) + release_tracking_status = _coerce_str( + payload["release_tracking_status"], + field="release_tracking_status", + path=path, + ) + release_completion_status = _coerce_str( + payload["release_completion_status"], + field="release_completion_status", + path=path, + ) + release_completion_exit_code = _coerce_int( + payload["release_completion_exit_code"], + field="release_completion_exit_code", + path=path, + ) + if release_completion_exit_code < 0: + raise ValueError(f"{path}: field 'release_completion_exit_code' must be >= 0") + + allow_in_progress = _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ) + should_poll_release_run = _coerce_bool( + payload["should_poll_release_run"], + field="should_poll_release_run", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_release_terminal_publish_report = _coerce_str( + payload["source_release_terminal_publish_report"], + field="source_release_terminal_publish_report", + path=path, + ) + source_release_completion_report = _coerce_str( + payload["source_release_completion_report"], + field="source_release_completion_report", + path=path, + ) + source_release_trace_report = _coerce_str( + payload["source_release_trace_report"], + field="source_release_trace_report", + path=path, + ) + source_release_trigger_report = _coerce_str( + payload["source_release_trigger_report"], + field="source_release_trigger_report", + path=path, + ) + source_release_handoff_report = _coerce_str( + payload["source_release_handoff_report"], + field="source_release_handoff_report", + path=path, + ) + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "release_finalization_status": release_finalization_status, + "release_finalization_decision": release_finalization_decision, + "release_finalization_exit_code": release_finalization_exit_code, + "should_finalize_release": should_finalize_release, + "on_hold_policy": on_hold_policy, + "release_publish_status": release_publish_status, + "release_publish_exit_code": release_publish_exit_code, + "release_trigger_status": release_trigger_status, + "release_tracking_status": release_tracking_status, + "release_completion_status": release_completion_status, + "release_completion_exit_code": release_completion_exit_code, + "allow_in_progress": allow_in_progress, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_release_terminal_publish_report": source_release_terminal_publish_report, + "source_release_completion_report": source_release_completion_report, + "source_release_trace_report": source_release_trace_report, + "source_release_trigger_report": source_release_trigger_report, + "source_release_handoff_report": source_release_handoff_report, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_release_closure_payload( + release_finalization_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_finalization_status = str(release_finalization_report["release_finalization_status"]) + release_finalization_decision = str( + release_finalization_report["release_finalization_decision"] + ) + release_finalization_exit_code = int( + release_finalization_report["release_finalization_exit_code"] + ) + should_finalize_release = bool(release_finalization_report["should_finalize_release"]) + + structural_issues = list(release_finalization_report["structural_issues"]) + reason_codes = list(release_finalization_report["reason_codes"]) + + if release_finalization_status == "finalized": + if release_finalization_decision != "finalize": + structural_issues.append("release_finalization_decision_mismatch_finalized") + if release_finalization_exit_code != 0: + structural_issues.append("release_finalization_exit_code_mismatch_finalized") + if not should_finalize_release: + structural_issues.append("should_finalize_release_mismatch_finalized") + elif release_finalization_status == "awaiting_release": + if release_finalization_decision != "hold": + structural_issues.append("release_finalization_decision_mismatch_awaiting") + if should_finalize_release: + structural_issues.append("should_finalize_release_mismatch_awaiting") + elif release_finalization_status in {"failed", "contract_failed"}: + if release_finalization_decision != "abort": + structural_issues.append("release_finalization_decision_mismatch_failed") + if should_finalize_release: + structural_issues.append("should_finalize_release_mismatch_failed") + if release_finalization_exit_code == 0: + structural_issues.append("release_finalization_exit_code_mismatch_failed") + + structural_issues = _unique(structural_issues) + + if structural_issues: + release_closure_status = "contract_failed" + release_closure_decision = "rollback" + release_closure_exit_code = 1 + should_close_release = False + should_notify = True + reason_codes.extend(structural_issues) + elif release_finalization_status == "finalized": + release_closure_status = "closed" + release_closure_decision = "ship" + release_closure_exit_code = 0 + should_close_release = True + should_notify = True + reason_codes = ["release_closed"] + elif release_finalization_status == "awaiting_release": + release_closure_status = "pending" + release_closure_decision = "hold" + release_closure_exit_code = release_finalization_exit_code + should_close_release = False + should_notify = True + elif release_finalization_status == "failed": + release_closure_status = "failed" + release_closure_decision = "rollback" + release_closure_exit_code = release_finalization_exit_code + should_close_release = False + should_notify = True + else: + release_closure_status = "contract_failed" + release_closure_decision = "rollback" + release_closure_exit_code = 1 + should_close_release = False + should_notify = True + + if release_closure_status not in ALLOWED_RELEASE_CLOSURE_STATUSES: + raise ValueError("internal: unsupported release_closure_status computed") + if release_closure_decision not in ALLOWED_RELEASE_CLOSURE_DECISIONS: + raise ValueError("internal: unsupported release_closure_decision computed") + + release_closure_summary = ( + f"release_finalization_status={release_finalization_status} " + f"release_closure_status={release_closure_status} " + f"release_closure_decision={release_closure_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_finalization_report": str(source_path), + "release_finalization_status": release_finalization_status, + "release_finalization_decision": release_finalization_decision, + "release_finalization_exit_code": release_finalization_exit_code, + "release_closure_status": release_closure_status, + "release_closure_decision": release_closure_decision, + "release_closure_exit_code": int(release_closure_exit_code), + "should_close_release": should_close_release, + "should_notify": should_notify, + "release_closure_summary": release_closure_summary, + "should_finalize_release": should_finalize_release, + "on_hold_policy": str(release_finalization_report["on_hold_policy"]), + "release_publish_status": str(release_finalization_report["release_publish_status"]), + "release_publish_exit_code": int(release_finalization_report["release_publish_exit_code"]), + "release_trigger_status": str(release_finalization_report["release_trigger_status"]), + "release_tracking_status": str(release_finalization_report["release_tracking_status"]), + "release_completion_status": str(release_finalization_report["release_completion_status"]), + "release_completion_exit_code": int( + release_finalization_report["release_completion_exit_code"] + ), + "allow_in_progress": bool(release_finalization_report["allow_in_progress"]), + "should_poll_release_run": bool(release_finalization_report["should_poll_release_run"]), + "release_run_id": release_finalization_report["release_run_id"], + "release_run_url": str(release_finalization_report["release_run_url"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(release_finalization_report["failed_checks"]), + "structural_issues": structural_issues, + "missing_artifacts": list(release_finalization_report["missing_artifacts"]), + "source_release_terminal_publish_report": str( + release_finalization_report["source_release_terminal_publish_report"] + ), + "source_release_completion_report": str( + release_finalization_report["source_release_completion_report"] + ), + "source_release_trace_report": str( + release_finalization_report["source_release_trace_report"] + ), + "source_release_trigger_report": str( + release_finalization_report["source_release_trigger_report"] + ), + "source_release_handoff_report": str( + release_finalization_report["source_release_handoff_report"] + ), + "source_terminal_publish_report": str( + release_finalization_report["source_terminal_publish_report"] + ), + "source_dispatch_completion_report": str( + release_finalization_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str( + release_finalization_report["source_dispatch_trace_report"] + ), + "source_dispatch_execution_report": str( + release_finalization_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(release_finalization_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Closure Report", + "", + f"- Release Closure Status: **{str(payload['release_closure_status']).upper()}**", + f"- Release Closure Decision: `{payload['release_closure_decision']}`", + f"- Should Close Release: `{payload['should_close_release']}`", + f"- Should Notify: `{payload['should_notify']}`", + f"- Release Closure Exit Code: `{payload['release_closure_exit_code']}`", + f"- Release Finalization Status: `{payload['release_finalization_status']}`", + f"- Release Finalization Decision: `{payload['release_finalization_decision']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Finalization Report: `{payload['source_release_finalization_report']}`", + f"- Source Release Terminal Publish Report: `{payload['source_release_terminal_publish_report']}`", + f"- Source Release Completion Report: `{payload['source_release_completion_report']}`", + f"- Source Release Trace Report: `{payload['source_release_trace_report']}`", + "", + "## Summary", + "", + f"- {payload['release_closure_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_closure_status": str(payload["release_closure_status"]), + "workflow_release_closure_decision": str(payload["release_closure_decision"]), + "workflow_release_closure_should_close_release": ( + "true" if payload["should_close_release"] else "false" + ), + "workflow_release_closure_should_notify": "true" if payload["should_notify"] else "false", + "workflow_release_closure_exit_code": str(payload["release_closure_exit_code"]), + "workflow_release_closure_finalization_status": str( + payload["release_finalization_status"] + ), + "workflow_release_closure_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_closure_run_url": str(payload["release_run_url"]), + "workflow_release_closure_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_closure_report_json": str(output_json), + "workflow_release_closure_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release closure contract from P2-35 finalization report" + ) + ) + parser.add_argument( + "--release-finalization-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_finalization.json", + help="P2-35 release finalization report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_closure.json", + help="Output release closure JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_closure.md", + help="Output release closure markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release closure JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_finalization_report_path = Path(args.release_finalization_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_finalization_report = load_release_finalization_report( + release_finalization_report_path + ) + payload = build_release_closure_payload( + release_finalization_report, + source_path=release_finalization_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-closure-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release closure json: {output_json_path}") + print(f"release closure markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release closure summary: " + f"release_closure_status={payload['release_closure_status']} " + f"release_closure_decision={payload['release_closure_decision']} " + f"release_closure_exit_code={payload['release_closure_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_closure_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_completion_gate.py b/scripts/run_p2_linux_ci_workflow_release_completion_gate.py new file mode 100644 index 0000000..f44f645 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_completion_gate.py @@ -0,0 +1,813 @@ +"""Phase 2 card P2-33 gate for Linux CI workflow release completion await. + +This script consumes the P2-32 release trace artifact and converges to a +release completion verdict: +1) validate release trace contract and poll command, +2) optionally poll release run status until terminal/timeout, +3) emit completion JSON/Markdown + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable + + +ALLOWED_RELEASE_TRACKING_STATUSES: set[str] = { + "blocked", + "awaiting_completion", + "handoff_failed", + "release_trigger_failed", + "release_ready_dry_run", + "release_run_tracking_missing", + "release_run_tracking_ready", + "release_run_in_progress", + "release_run_completed_success", + "release_run_completed_failure", + "release_run_poll_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_trace_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release trace report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release trace payload must be object") + + required_fields = ( + "release_trigger_status", + "release_tracking_status", + "release_trace_exit_code", + "should_poll_release_run", + "release_run_id", + "release_run_url", + "repo_owner", + "repo_name", + "release_workflow_path", + "release_workflow_ref", + "release_poll_command", + "release_poll_command_parts", + "release_poll_attempted", + "release_poll_returncode", + "release_poll_status", + "release_poll_conclusion", + "release_poll_url", + "release_poll_error_type", + "release_poll_error_message", + "release_poll_stdout_tail", + "release_poll_stderr_tail", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_trigger_status = _coerce_str( + payload["release_trigger_status"], + field="release_trigger_status", + path=path, + ) + release_tracking_status = _coerce_str( + payload["release_tracking_status"], + field="release_tracking_status", + path=path, + ) + if release_tracking_status not in ALLOWED_RELEASE_TRACKING_STATUSES: + raise ValueError( + f"{path}: field 'release_tracking_status' must be one of " + f"{sorted(ALLOWED_RELEASE_TRACKING_STATUSES)}" + ) + + release_trace_exit_code = _coerce_int( + payload["release_trace_exit_code"], + field="release_trace_exit_code", + path=path, + ) + if release_trace_exit_code < 0: + raise ValueError(f"{path}: field 'release_trace_exit_code' must be >= 0") + + should_poll_release_run = _coerce_bool( + payload["should_poll_release_run"], + field="should_poll_release_run", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + repo_owner = _coerce_str(payload["repo_owner"], field="repo_owner", path=path) + repo_name = _coerce_str(payload["repo_name"], field="repo_name", path=path) + release_workflow_path = _coerce_str( + payload["release_workflow_path"], + field="release_workflow_path", + path=path, + ) + release_workflow_ref = _coerce_str( + payload["release_workflow_ref"], + field="release_workflow_ref", + path=path, + ) + release_poll_command = _coerce_str( + payload["release_poll_command"], + field="release_poll_command", + path=path, + ) + release_poll_command_parts = _coerce_str_list( + payload["release_poll_command_parts"], + field="release_poll_command_parts", + path=path, + ) + release_poll_attempted = _coerce_bool( + payload["release_poll_attempted"], + field="release_poll_attempted", + path=path, + ) + release_poll_returncode_raw = payload["release_poll_returncode"] + if release_poll_returncode_raw is None: + release_poll_returncode = None + else: + release_poll_returncode = _coerce_int( + release_poll_returncode_raw, + field="release_poll_returncode", + path=path, + ) + release_poll_status = _coerce_str( + payload["release_poll_status"], + field="release_poll_status", + path=path, + ) + release_poll_conclusion = _coerce_str( + payload["release_poll_conclusion"], + field="release_poll_conclusion", + path=path, + ) + release_poll_url = _coerce_str( + payload["release_poll_url"], + field="release_poll_url", + path=path, + ) + release_poll_error_type = _coerce_str( + payload["release_poll_error_type"], + field="release_poll_error_type", + path=path, + ) + release_poll_error_message = _coerce_str( + payload["release_poll_error_message"], + field="release_poll_error_message", + path=path, + ) + release_poll_stdout_tail = _coerce_str( + payload["release_poll_stdout_tail"], + field="release_poll_stdout_tail", + path=path, + ) + release_poll_stderr_tail = _coerce_str( + payload["release_poll_stderr_tail"], + field="release_poll_stderr_tail", + path=path, + ) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + source_release_trigger_report = _coerce_str( + payload["source_release_trigger_report"], + field="source_release_trigger_report", + path=path, + ) + source_release_handoff_report = _coerce_str( + payload["source_release_handoff_report"], + field="source_release_handoff_report", + path=path, + ) + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + if should_poll_release_run: + if release_run_id is None: + raise ValueError(f"{path}: should_poll_release_run=true requires release_run_id") + if not release_poll_command_parts: + raise ValueError( + f"{path}: should_poll_release_run=true requires release_poll_command_parts" + ) + if len(release_poll_command_parts) < 3 or release_poll_command_parts[1:3] != [ + "run", + "view", + ]: + raise ValueError( + f"{path}: release_poll_command_parts must include ' run view ...' contract" + ) + parsed_command = shlex.split(release_poll_command) + if parsed_command != release_poll_command_parts: + raise ValueError(f"{path}: release_poll_command and release_poll_command_parts mismatch") + else: + if release_run_id is not None: + raise ValueError(f"{path}: should_poll_release_run=false requires release_run_id=null") + + return { + "generated_at": payload.get("generated_at"), + "release_trigger_status": release_trigger_status, + "release_tracking_status": release_tracking_status, + "release_trace_exit_code": release_trace_exit_code, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "repo_owner": repo_owner, + "repo_name": repo_name, + "release_workflow_path": release_workflow_path, + "release_workflow_ref": release_workflow_ref, + "release_poll_command": release_poll_command, + "release_poll_command_parts": release_poll_command_parts, + "release_poll_attempted": release_poll_attempted, + "release_poll_returncode": release_poll_returncode, + "release_poll_status": release_poll_status, + "release_poll_conclusion": release_poll_conclusion, + "release_poll_url": release_poll_url, + "release_poll_error_type": release_poll_error_type, + "release_poll_error_message": release_poll_error_message, + "release_poll_stdout_tail": release_poll_stdout_tail, + "release_poll_stderr_tail": release_poll_stderr_tail, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_release_trigger_report": source_release_trigger_report, + "source_release_handoff_report": source_release_handoff_report, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def execute_release_poll_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"release run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def build_release_completion_payload( + release_trace_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + poll_interval_seconds: int, + max_polls: int, + poll_timeout_seconds: int, + dry_run: bool, + allow_in_progress: bool, + poll_executor: Callable[..., dict[str, Any]] = execute_release_poll_command, + sleep_fn: Callable[[float], None] = time.sleep, +) -> dict[str, Any]: + release_tracking_status = str(release_trace_report["release_tracking_status"]) + should_poll_release_run = bool(release_trace_report["should_poll_release_run"]) + + release_completion_status = release_tracking_status + release_completion_exit_code = int(release_trace_report["release_trace_exit_code"]) + reason_codes = list(release_trace_report["reason_codes"]) + + poll_iterations = 0 + poll_attempted = False + poll_returncode = release_trace_report["release_poll_returncode"] + poll_status = str(release_trace_report["release_poll_status"]) + poll_conclusion = str(release_trace_report["release_poll_conclusion"]) + poll_url = str(release_trace_report["release_poll_url"]) + poll_error_type = str(release_trace_report["release_poll_error_type"]) + poll_error_message = str(release_trace_report["release_poll_error_message"]) + poll_stdout_tail = str(release_trace_report["release_poll_stdout_tail"]) + poll_stderr_tail = str(release_trace_report["release_poll_stderr_tail"]) + + should_await = should_poll_release_run and release_tracking_status in { + "release_run_tracking_ready", + "release_run_in_progress", + } + + if should_await and not dry_run: + for attempt in range(1, max_polls + 1): + poll_iterations = attempt + poll_attempted = True + poll_result = poll_executor( + list(release_trace_report["release_poll_command_parts"]), + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + poll_returncode = int(poll_result["returncode"]) + poll_stdout_tail = str(poll_result["stdout_tail"]) + poll_stderr_tail = str(poll_result["stderr_tail"]) + poll_error_type = str(poll_result["error_type"]) + poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if poll_returncode != 0: + release_completion_status = "release_run_poll_failed" + release_completion_exit_code = 1 + if poll_error_type == "command_not_found": + reason_codes.append("release_run_poll_cli_unavailable") + elif poll_error_type == "timeout": + reason_codes.append("release_run_poll_timeout") + else: + reason_codes.append("release_run_poll_command_failed") + break + + if poll_payload is None: + release_completion_status = "release_run_poll_failed" + release_completion_exit_code = 1 + reason_codes.append("release_run_poll_payload_invalid") + break + + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + poll_status = poll_status_value + else: + poll_status = "" + if isinstance(poll_conclusion_value, str): + poll_conclusion = poll_conclusion_value + else: + poll_conclusion = "" + if isinstance(poll_url_value, str) and poll_url_value: + poll_url = poll_url_value + + if poll_status == "completed": + if poll_conclusion == "success": + release_completion_status = "release_run_completed_success" + release_completion_exit_code = 0 + reason_codes = ["release_run_completed_success"] + else: + release_completion_status = "release_run_completed_failure" + release_completion_exit_code = 1 + reason_codes.append("release_run_completed_with_failure") + break + + release_completion_status = "release_run_in_progress" + release_completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append("release_run_still_in_progress") + if attempt < max_polls: + sleep_fn(float(poll_interval_seconds)) + else: + release_completion_status = "release_run_await_timeout" + release_completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append("release_run_await_timeout") + + elif should_await and dry_run: + release_completion_status = release_tracking_status + release_completion_exit_code = int(release_trace_report["release_trace_exit_code"]) + reason_codes.append("release_completion_poll_skipped_dry_run") + else: + if release_tracking_status == "release_run_completed_success": + release_completion_exit_code = 0 + elif release_tracking_status in { + "release_run_tracking_missing", + "release_run_completed_failure", + "release_run_poll_failed", + "release_trigger_failed", + "handoff_failed", + }: + release_completion_exit_code = 1 + elif release_tracking_status in {"blocked", "release_ready_dry_run", "awaiting_completion"}: + release_completion_exit_code = int(release_trace_report["release_trace_exit_code"]) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_trace_report": str(source_path), + "project_root": str(project_root), + "release_trigger_status": str(release_trace_report["release_trigger_status"]), + "release_tracking_status": release_tracking_status, + "release_completion_status": release_completion_status, + "release_completion_exit_code": int(release_completion_exit_code), + "dry_run": dry_run, + "allow_in_progress": allow_in_progress, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_trace_report["release_run_id"], + "release_run_url": str(release_trace_report["release_run_url"]), + "repo_owner": str(release_trace_report["repo_owner"]), + "repo_name": str(release_trace_report["repo_name"]), + "release_workflow_path": str(release_trace_report["release_workflow_path"]), + "release_workflow_ref": str(release_trace_report["release_workflow_ref"]), + "release_poll_command": str(release_trace_report["release_poll_command"]), + "release_poll_command_parts": list(release_trace_report["release_poll_command_parts"]), + "poll_interval_seconds": poll_interval_seconds, + "max_polls": max_polls, + "poll_timeout_seconds": poll_timeout_seconds, + "poll_iterations": poll_iterations, + "poll_attempted": poll_attempted, + "poll_returncode": poll_returncode, + "poll_status": poll_status, + "poll_conclusion": poll_conclusion, + "poll_url": poll_url, + "poll_error_type": poll_error_type, + "poll_error_message": poll_error_message, + "poll_stdout_tail": poll_stdout_tail, + "poll_stderr_tail": poll_stderr_tail, + "release_trace_reason_codes": list(release_trace_report["reason_codes"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(release_trace_report["failed_checks"]), + "structural_issues": list(release_trace_report["structural_issues"]), + "missing_artifacts": list(release_trace_report["missing_artifacts"]), + "source_release_trigger_report": str(release_trace_report["source_release_trigger_report"]), + "source_release_handoff_report": str(release_trace_report["source_release_handoff_report"]), + "source_terminal_publish_report": str(release_trace_report["source_terminal_publish_report"]), + "source_dispatch_completion_report": str( + release_trace_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str(release_trace_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str( + release_trace_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(release_trace_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Completion Report", + "", + f"- Release Completion Status: **{str(payload['release_completion_status']).upper()}**", + f"- Release Completion Exit Code: `{payload['release_completion_exit_code']}`", + f"- Should Poll Release Run: `{payload['should_poll_release_run']}`", + f"- Poll Attempted: `{payload['poll_attempted']}`", + f"- Poll Iterations: `{payload['poll_iterations']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Trace Report: `{payload['source_release_trace_report']}`", + f"- Release Workflow Path: `{payload['release_workflow_path']}`", + f"- Release Workflow Ref: `{payload['release_workflow_ref']}`", + "", + "## Poll Result", + "", + f"- Poll Return Code: `{payload['poll_returncode']}`", + f"- Poll Status: `{payload['poll_status']}`", + f"- Poll Conclusion: `{payload['poll_conclusion']}`", + f"- Poll URL: `{payload['poll_url']}`", + f"- Poll Error Type: `{payload['poll_error_type']}`", + ] + + if payload["poll_stdout_tail"]: + lines.append("- Poll stdout:") + lines.append("```text") + lines.append(str(payload["poll_stdout_tail"])) + lines.append("```") + if payload["poll_stderr_tail"]: + lines.append("- Poll stderr:") + lines.append("```text") + lines.append(str(payload["poll_stderr_tail"])) + lines.append("```") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_completion_status": str(payload["release_completion_status"]), + "workflow_release_completion_exit_code": str(payload["release_completion_exit_code"]), + "workflow_release_completion_should_poll": ( + "true" if payload["should_poll_release_run"] else "false" + ), + "workflow_release_completion_poll_attempted": ( + "true" if payload["poll_attempted"] else "false" + ), + "workflow_release_completion_poll_iterations": str(payload["poll_iterations"]), + "workflow_release_completion_poll_status": str(payload["poll_status"]), + "workflow_release_completion_poll_conclusion": str(payload["poll_conclusion"]), + "workflow_release_completion_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_completion_run_url": str(payload["release_run_url"]), + "workflow_release_completion_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_completion_report_json": str(output_json), + "workflow_release_completion_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Await Linux CI workflow release completion from P2-32 release trace report" + ) + parser.add_argument( + "--release-trace-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trace.json", + help="P2-32 release trace report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root used for optional release poll command execution", + ) + parser.add_argument( + "--poll-interval-seconds", + type=int, + default=30, + help="Sleep interval between polls when awaiting release completion", + ) + parser.add_argument( + "--max-polls", + type=int, + default=20, + help="Maximum poll attempts when awaiting release completion", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Timeout seconds for each release poll command execution", + ) + parser.add_argument( + "--allow-in-progress", + action="store_true", + help="Return exit 0 when release run remains in progress after max polls", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_completion.json", + help="Output release completion JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_completion.md", + help="Output release completion markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release completion JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_interval_seconds < 1: + print( + "[p2-linux-ci-workflow-release-completion-gate] " + "--poll-interval-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.max_polls < 1: + print( + "[p2-linux-ci-workflow-release-completion-gate] --max-polls must be >= 1", + file=sys.stderr, + ) + return 2 + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-release-completion-gate] " + "--poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + release_trace_report_path = Path(args.release_trace_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + release_trace_report = load_release_trace_report(release_trace_report_path) + payload = build_release_completion_payload( + release_trace_report, + source_path=release_trace_report_path.resolve(), + project_root=project_root, + poll_interval_seconds=args.poll_interval_seconds, + max_polls=args.max_polls, + poll_timeout_seconds=args.poll_timeout_seconds, + dry_run=args.dry_run, + allow_in_progress=args.allow_in_progress, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-completion-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release completion json: {output_json_path}") + print(f"release completion markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release completion summary: " + f"release_completion_status={payload['release_completion_status']} " + f"poll_attempted={payload['poll_attempted']} " + f"release_completion_exit_code={payload['release_completion_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_completion_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py new file mode 100644 index 0000000..9538c62 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py @@ -0,0 +1,928 @@ +"""Phase 2 card P2-43 gate for Linux CI workflow release delivery final verdict. + +This script consumes the P2-42 release delivery terminal publish artifact and +converges the final delivery verdict contract: +1) validate delivery terminal publish + incident consistency, +2) normalize close/follow-up/escalate/abort final semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "pending_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release", + "announce_hold", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "hold", + "blocker", +} +ALLOWED_RELEASE_DELIVERY_STATUSES: set[str] = { + "shipped", + "pending_follow_up", + "blocked_incident", + "blocked_incident_failed", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_DECISIONS: set[str] = {"deliver", "hold", "escalate", "block"} +ALLOWED_INCIDENT_DISPATCH_STATUSES: set[str] = { + "not_required", + "ready_dry_run", + "dispatched", + "dispatch_failed", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: set[str] = { + "release", + "hold", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_delivery_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release delivery terminal publish report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release delivery terminal publish payload must be object") + + required_fields = ( + "source_release_delivery_report", + "source_release_terminal_verdict_report", + "source_release_incident_report", + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_terminal_verdict_status", + "release_terminal_verdict_decision", + "release_terminal_verdict_exit_code", + "terminal_should_ship_release", + "terminal_requires_follow_up", + "terminal_incident_linked", + "release_delivery_status", + "release_delivery_decision", + "release_delivery_exit_code", + "delivery_should_ship_release", + "delivery_requires_human_action", + "delivery_should_announce_blocker", + "release_delivery_terminal_publish_status", + "release_delivery_terminal_publish_decision", + "release_delivery_terminal_publish_exit_code", + "terminal_publish_should_notify", + "terminal_publish_should_create_follow_up", + "terminal_publish_channel", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_publish_status = _coerce_str( + payload["release_delivery_terminal_publish_status"], + field="release_delivery_terminal_publish_status", + path=path, + ) + if terminal_publish_status not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_STATUSES)}" + ) + + terminal_publish_decision = _coerce_str( + payload["release_delivery_terminal_publish_decision"], + field="release_delivery_terminal_publish_decision", + path=path, + ) + if terminal_publish_decision not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'release_delivery_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_DECISIONS)}" + ) + + terminal_publish_exit_code = _coerce_int( + payload["release_delivery_terminal_publish_exit_code"], + field="release_delivery_terminal_publish_exit_code", + path=path, + ) + if terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_delivery_terminal_publish_exit_code' must be >= 0" + ) + + terminal_publish_channel = _coerce_str( + payload["terminal_publish_channel"], + field="terminal_publish_channel", + path=path, + ) + if terminal_publish_channel not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'terminal_publish_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_CHANNELS)}" + ) + + release_delivery_status = _coerce_str( + payload["release_delivery_status"], + field="release_delivery_status", + path=path, + ) + if release_delivery_status not in ALLOWED_RELEASE_DELIVERY_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_STATUSES)}" + ) + + release_delivery_decision = _coerce_str( + payload["release_delivery_decision"], + field="release_delivery_decision", + path=path, + ) + if release_delivery_decision not in ALLOWED_RELEASE_DELIVERY_DECISIONS: + raise ValueError( + f"{path}: field 'release_delivery_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_DECISIONS)}" + ) + + release_delivery_exit_code = _coerce_int( + payload["release_delivery_exit_code"], + field="release_delivery_exit_code", + path=path, + ) + if release_delivery_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_exit_code' must be >= 0") + + incident_dispatch_status = _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ) + if incident_dispatch_status not in ALLOWED_INCIDENT_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'incident_dispatch_status' must be one of " + f"{sorted(ALLOWED_INCIDENT_DISPATCH_STATUSES)}" + ) + + incident_dispatch_exit_code = _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ) + if incident_dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'incident_dispatch_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_delivery_report": _coerce_str( + payload["source_release_delivery_report"], + field="source_release_delivery_report", + path=path, + ), + "source_release_terminal_verdict_report": _coerce_str( + payload["source_release_terminal_verdict_report"], + field="source_release_terminal_verdict_report", + path=path, + ), + "source_release_incident_report": _coerce_str( + payload["source_release_incident_report"], + field="source_release_incident_report", + path=path, + ), + "source_release_verdict_report": _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ), + "source_release_archive_report": _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ), + "release_archive_status": _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ), + "release_archive_decision": _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ), + "release_archive_exit_code": _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ), + "should_publish_archive": _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ), + "release_verdict_status": _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ), + "release_verdict_decision": _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ), + "release_verdict_exit_code": _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ), + "should_ship_release": _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ), + "should_open_incident": _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ), + "should_dispatch_incident": _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ), + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ), + "incident_url": _coerce_str( + payload["incident_url"], + field="incident_url", + path=path, + ), + "release_terminal_verdict_status": _coerce_str( + payload["release_terminal_verdict_status"], + field="release_terminal_verdict_status", + path=path, + ), + "release_terminal_verdict_decision": _coerce_str( + payload["release_terminal_verdict_decision"], + field="release_terminal_verdict_decision", + path=path, + ), + "release_terminal_verdict_exit_code": _coerce_int( + payload["release_terminal_verdict_exit_code"], + field="release_terminal_verdict_exit_code", + path=path, + ), + "terminal_should_ship_release": _coerce_bool( + payload["terminal_should_ship_release"], + field="terminal_should_ship_release", + path=path, + ), + "terminal_requires_follow_up": _coerce_bool( + payload["terminal_requires_follow_up"], + field="terminal_requires_follow_up", + path=path, + ), + "terminal_incident_linked": _coerce_bool( + payload["terminal_incident_linked"], + field="terminal_incident_linked", + path=path, + ), + "release_delivery_status": release_delivery_status, + "release_delivery_decision": release_delivery_decision, + "release_delivery_exit_code": release_delivery_exit_code, + "delivery_should_ship_release": _coerce_bool( + payload["delivery_should_ship_release"], + field="delivery_should_ship_release", + path=path, + ), + "delivery_requires_human_action": _coerce_bool( + payload["delivery_requires_human_action"], + field="delivery_requires_human_action", + path=path, + ), + "delivery_should_announce_blocker": _coerce_bool( + payload["delivery_should_announce_blocker"], + field="delivery_should_announce_blocker", + path=path, + ), + "release_delivery_terminal_publish_status": terminal_publish_status, + "release_delivery_terminal_publish_decision": terminal_publish_decision, + "release_delivery_terminal_publish_exit_code": terminal_publish_exit_code, + "terminal_publish_should_notify": _coerce_bool( + payload["terminal_publish_should_notify"], + field="terminal_publish_should_notify", + path=path, + ), + "terminal_publish_should_create_follow_up": _coerce_bool( + payload["terminal_publish_should_create_follow_up"], + field="terminal_publish_should_create_follow_up", + path=path, + ), + "terminal_publish_channel": terminal_publish_channel, + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_delivery_final_verdict_payload( + terminal_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + terminal_publish_status = str( + terminal_publish_report["release_delivery_terminal_publish_status"] + ) + terminal_publish_decision = str( + terminal_publish_report["release_delivery_terminal_publish_decision"] + ) + terminal_publish_exit_code = int( + terminal_publish_report["release_delivery_terminal_publish_exit_code"] + ) + terminal_publish_should_notify = bool(terminal_publish_report["terminal_publish_should_notify"]) + terminal_publish_should_create_follow_up = bool( + terminal_publish_report["terminal_publish_should_create_follow_up"] + ) + terminal_publish_channel = str(terminal_publish_report["terminal_publish_channel"]) + release_delivery_status = str(terminal_publish_report["release_delivery_status"]) + release_delivery_decision = str(terminal_publish_report["release_delivery_decision"]) + release_delivery_exit_code = int(terminal_publish_report["release_delivery_exit_code"]) + delivery_should_ship_release = bool(terminal_publish_report["delivery_should_ship_release"]) + delivery_requires_human_action = bool(terminal_publish_report["delivery_requires_human_action"]) + delivery_should_announce_blocker = bool( + terminal_publish_report["delivery_should_announce_blocker"] + ) + incident_dispatch_status = str(terminal_publish_report["incident_dispatch_status"]) + incident_dispatch_exit_code = int(terminal_publish_report["incident_dispatch_exit_code"]) + terminal_should_ship_release = bool(terminal_publish_report["terminal_should_ship_release"]) + terminal_requires_follow_up = bool(terminal_publish_report["terminal_requires_follow_up"]) + + reason_codes = list(terminal_publish_report["reason_codes"]) + structural_issues = list(terminal_publish_report["structural_issues"]) + missing_artifacts = list(terminal_publish_report["missing_artifacts"]) + evidence_manifest = list(terminal_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_delivery_final_evidence_missing") + + if terminal_publish_status == "published": + if terminal_publish_decision != "announce_release": + structural_issues.append("terminal_publish_decision_mismatch_published") + if terminal_publish_exit_code != 0: + structural_issues.append("terminal_publish_exit_code_mismatch_published") + if not terminal_publish_should_notify: + structural_issues.append("terminal_publish_should_notify_mismatch_published") + if terminal_publish_should_create_follow_up: + structural_issues.append("terminal_publish_follow_up_mismatch_published") + if terminal_publish_channel != "release": + structural_issues.append("terminal_publish_channel_mismatch_published") + if release_delivery_status != "shipped": + structural_issues.append("release_delivery_status_mismatch_published") + if release_delivery_decision != "deliver": + structural_issues.append("release_delivery_decision_mismatch_published") + if release_delivery_exit_code != 0: + structural_issues.append("release_delivery_exit_code_mismatch_published") + if not delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_published") + if delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_published") + if incident_dispatch_status != "not_required": + structural_issues.append("incident_status_mismatch_published") + if not terminal_should_ship_release: + structural_issues.append("terminal_should_ship_mismatch_published") + if terminal_requires_follow_up: + structural_issues.append("terminal_requires_follow_up_mismatch_published") + elif terminal_publish_status == "pending_follow_up": + if terminal_publish_decision != "announce_hold": + structural_issues.append("terminal_publish_decision_mismatch_pending_follow_up") + if terminal_publish_exit_code == 0: + structural_issues.append("terminal_publish_exit_code_mismatch_pending_follow_up") + if not terminal_publish_should_notify: + structural_issues.append("terminal_publish_should_notify_mismatch_pending_follow_up") + if not terminal_publish_should_create_follow_up: + structural_issues.append("terminal_publish_follow_up_mismatch_pending_follow_up") + if terminal_publish_channel != "hold": + structural_issues.append("terminal_publish_channel_mismatch_pending_follow_up") + if release_delivery_status != "pending_follow_up": + structural_issues.append("release_delivery_status_mismatch_pending_follow_up") + if release_delivery_decision != "hold": + structural_issues.append("release_delivery_decision_mismatch_pending_follow_up") + if release_delivery_exit_code == 0: + structural_issues.append("release_delivery_exit_code_mismatch_pending_follow_up") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_pending_follow_up") + if not delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_pending_follow_up") + elif terminal_publish_status == "blocked": + if terminal_publish_decision != "announce_blocker": + structural_issues.append("terminal_publish_decision_mismatch_blocked") + if terminal_publish_exit_code == 0: + structural_issues.append("terminal_publish_exit_code_mismatch_blocked") + if not terminal_publish_should_notify: + structural_issues.append("terminal_publish_should_notify_mismatch_blocked") + if not terminal_publish_should_create_follow_up: + structural_issues.append("terminal_publish_follow_up_mismatch_blocked") + if terminal_publish_channel != "blocker": + structural_issues.append("terminal_publish_channel_mismatch_blocked") + if release_delivery_status not in {"blocked_incident", "blocked_incident_failed", "blocked"}: + structural_issues.append("release_delivery_status_mismatch_blocked") + if release_delivery_decision not in {"escalate", "block", "hold"}: + structural_issues.append("release_delivery_decision_mismatch_blocked") + if release_delivery_exit_code == 0: + structural_issues.append("release_delivery_exit_code_mismatch_blocked") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_blocked") + if not delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_blocked") + if release_delivery_status == "blocked_incident" and incident_dispatch_status not in { + "ready_dry_run", + "dispatched", + }: + structural_issues.append("incident_status_mismatch_blocked_incident") + if release_delivery_status == "blocked_incident_failed" and incident_dispatch_status != "dispatch_failed": + structural_issues.append("incident_status_mismatch_blocked_incident_failed") + if not delivery_should_announce_blocker: + structural_issues.append("delivery_should_announce_blocker_mismatch_blocked") + elif terminal_publish_status == "contract_failed": + if terminal_publish_decision != "abort_publish": + structural_issues.append("terminal_publish_decision_mismatch_contract_failed") + if terminal_publish_exit_code == 0: + structural_issues.append("terminal_publish_exit_code_mismatch_contract_failed") + if terminal_publish_channel != "blocker": + structural_issues.append("terminal_publish_channel_mismatch_contract_failed") + if release_delivery_exit_code == 0: + structural_issues.append("release_delivery_exit_code_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + final_status = "contract_failed" + final_decision = "abort_close" + final_exit_code = 1 + final_should_close_release = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.extend(structural_issues) + elif terminal_publish_status == "published": + final_status = "completed" + final_decision = "close_release" + final_exit_code = 0 + final_should_close_release = True + final_should_open_follow_up = False + final_should_page_owner = False + final_announcement_target = "release" + reason_codes = ["release_delivery_final_completed"] + elif terminal_publish_status == "pending_follow_up": + final_status = "requires_follow_up" + final_decision = "open_follow_up" + final_exit_code = max(1, terminal_publish_exit_code, release_delivery_exit_code) + final_should_close_release = False + final_should_open_follow_up = True + final_should_page_owner = False + final_announcement_target = "hold" + reason_codes.append("release_delivery_final_follow_up_required") + elif terminal_publish_status == "contract_failed": + final_status = "contract_failed" + final_decision = "abort_close" + final_exit_code = max(1, terminal_publish_exit_code, release_delivery_exit_code) + final_should_close_release = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.append("release_delivery_final_upstream_contract_failed") + else: + final_status = "blocked" + final_decision = "escalate_blocker" + final_exit_code = max( + 1, + terminal_publish_exit_code, + release_delivery_exit_code, + incident_dispatch_exit_code, + ) + final_should_close_release = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.append("release_delivery_final_blocked") + + if final_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported release_delivery_final_verdict_status computed") + if final_decision not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported release_delivery_final_verdict_decision computed") + if final_announcement_target not in ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: + raise ValueError("internal: unsupported release_delivery_final_announcement_target computed") + + reason_codes = _unique(reason_codes) + release_delivery_final_verdict_summary = ( + f"release_delivery_terminal_publish_status={terminal_publish_status} " + f"release_delivery_final_verdict_status={final_status} " + f"release_delivery_final_verdict_decision={final_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_delivery_terminal_publish_report": str(source_path), + "source_release_delivery_report": str(terminal_publish_report["source_release_delivery_report"]), + "source_release_terminal_verdict_report": str( + terminal_publish_report["source_release_terminal_verdict_report"] + ), + "source_release_incident_report": str(terminal_publish_report["source_release_incident_report"]), + "source_release_verdict_report": str(terminal_publish_report["source_release_verdict_report"]), + "source_release_archive_report": str(terminal_publish_report["source_release_archive_report"]), + "release_archive_status": str(terminal_publish_report["release_archive_status"]), + "release_archive_decision": str(terminal_publish_report["release_archive_decision"]), + "release_archive_exit_code": int(terminal_publish_report["release_archive_exit_code"]), + "should_publish_archive": bool(terminal_publish_report["should_publish_archive"]), + "release_verdict_status": str(terminal_publish_report["release_verdict_status"]), + "release_verdict_decision": str(terminal_publish_report["release_verdict_decision"]), + "release_verdict_exit_code": int(terminal_publish_report["release_verdict_exit_code"]), + "should_ship_release": bool(terminal_publish_report["should_ship_release"]), + "should_open_incident": bool(terminal_publish_report["should_open_incident"]), + "should_dispatch_incident": bool(terminal_publish_report["should_dispatch_incident"]), + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": bool(terminal_publish_report["incident_dispatch_attempted"]), + "incident_url": str(terminal_publish_report["incident_url"]), + "release_terminal_verdict_status": str( + terminal_publish_report["release_terminal_verdict_status"] + ), + "release_terminal_verdict_decision": str( + terminal_publish_report["release_terminal_verdict_decision"] + ), + "release_terminal_verdict_exit_code": int( + terminal_publish_report["release_terminal_verdict_exit_code"] + ), + "terminal_should_ship_release": terminal_should_ship_release, + "terminal_requires_follow_up": terminal_requires_follow_up, + "terminal_incident_linked": bool(terminal_publish_report["terminal_incident_linked"]), + "release_delivery_status": release_delivery_status, + "release_delivery_decision": release_delivery_decision, + "release_delivery_exit_code": release_delivery_exit_code, + "delivery_should_ship_release": delivery_should_ship_release, + "delivery_requires_human_action": delivery_requires_human_action, + "delivery_should_announce_blocker": delivery_should_announce_blocker, + "release_delivery_terminal_publish_status": terminal_publish_status, + "release_delivery_terminal_publish_decision": terminal_publish_decision, + "release_delivery_terminal_publish_exit_code": terminal_publish_exit_code, + "terminal_publish_should_notify": terminal_publish_should_notify, + "terminal_publish_should_create_follow_up": terminal_publish_should_create_follow_up, + "terminal_publish_channel": terminal_publish_channel, + "release_delivery_final_verdict_status": final_status, + "release_delivery_final_verdict_decision": final_decision, + "release_delivery_final_verdict_exit_code": final_exit_code, + "final_should_close_release": final_should_close_release, + "final_should_open_follow_up": final_should_open_follow_up, + "final_should_page_owner": final_should_page_owner, + "final_announcement_target": final_announcement_target, + "release_run_id": terminal_publish_report["release_run_id"], + "release_run_url": str(terminal_publish_report["release_run_url"]), + "release_delivery_final_verdict_summary": release_delivery_final_verdict_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Delivery Final Verdict Report", + "", + ( + "- Release Delivery Final Verdict Status: " + f"**{str(payload['release_delivery_final_verdict_status']).upper()}**" + ), + ( + "- Release Delivery Final Verdict Decision: " + f"`{payload['release_delivery_final_verdict_decision']}`" + ), + ( + "- Release Delivery Final Verdict Exit Code: " + f"`{payload['release_delivery_final_verdict_exit_code']}`" + ), + f"- Final Should Close Release: `{payload['final_should_close_release']}`", + f"- Final Should Open Follow Up: `{payload['final_should_open_follow_up']}`", + f"- Final Should Page Owner: `{payload['final_should_page_owner']}`", + f"- Final Announcement Target: `{payload['final_announcement_target']}`", + ( + "- Release Delivery Terminal Publish Status: " + f"`{payload['release_delivery_terminal_publish_status']}`" + ), + f"- Incident Dispatch Status: `{payload['incident_dispatch_status']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_delivery_final_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_delivery_final_verdict_status": str( + payload["release_delivery_final_verdict_status"] + ), + "workflow_release_delivery_final_verdict_decision": str( + payload["release_delivery_final_verdict_decision"] + ), + "workflow_release_delivery_final_verdict_exit_code": str( + payload["release_delivery_final_verdict_exit_code"] + ), + "workflow_release_delivery_final_should_close_release": ( + "true" if payload["final_should_close_release"] else "false" + ), + "workflow_release_delivery_final_should_open_follow_up": ( + "true" if payload["final_should_open_follow_up"] else "false" + ), + "workflow_release_delivery_final_should_page_owner": ( + "true" if payload["final_should_page_owner"] else "false" + ), + "workflow_release_delivery_final_announcement_target": str( + payload["final_announcement_target"] + ), + "workflow_release_delivery_final_terminal_publish_status": str( + payload["release_delivery_terminal_publish_status"] + ), + "workflow_release_delivery_final_incident_status": str(payload["incident_dispatch_status"]), + "workflow_release_delivery_final_incident_url": str(payload["incident_url"]), + "workflow_release_delivery_final_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_delivery_final_run_url": str(payload["release_run_url"]), + "workflow_release_delivery_final_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_delivery_final_report_json": str(output_json), + "workflow_release_delivery_final_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release delivery final verdict contract " + "from P2-42 release delivery terminal publish report" + ) + ) + parser.add_argument( + "--release-delivery-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json", + help="P2-42 release delivery terminal publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json", + help="Output release delivery final verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md", + help="Output release delivery final verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release delivery final verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_publish_report_path = Path(args.release_delivery_terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_publish_report = load_release_delivery_terminal_publish_report( + terminal_publish_report_path + ) + payload = build_release_delivery_final_verdict_payload( + terminal_publish_report, + source_path=terminal_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-delivery-final-verdict-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release delivery final verdict json: {output_json_path}") + print(f"release delivery final verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release delivery final verdict summary: " + f"release_delivery_final_verdict_status=" + f"{payload['release_delivery_final_verdict_status']} " + f"release_delivery_final_verdict_decision=" + f"{payload['release_delivery_final_verdict_decision']} " + f"release_delivery_final_verdict_exit_code=" + f"{payload['release_delivery_final_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_delivery_final_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_delivery_gate.py b/scripts/run_p2_linux_ci_workflow_release_delivery_gate.py new file mode 100644 index 0000000..7d66c6e --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_delivery_gate.py @@ -0,0 +1,671 @@ +"""Phase 2 card P2-41 gate for Linux CI workflow release delivery closure. + +This script consumes the P2-40 release terminal verdict artifact and converges +the final delivery contract: +1) validate release terminal verdict + incident consistency, +2) normalize final ship/hold/escalate/block delivery semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_TERMINAL_VERDICT_STATUSES: set[str] = { + "released", + "awaiting_archive", + "blocked_incident_ready_dry_run", + "blocked_incident_dispatched", + "blocked_incident_failed", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_TERMINAL_VERDICT_DECISIONS: set[str] = {"ship", "hold", "escalate", "block"} +ALLOWED_INCIDENT_DISPATCH_STATUSES: set[str] = { + "not_required", + "ready_dry_run", + "dispatched", + "dispatch_failed", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_STATUSES: set[str] = { + "shipped", + "pending_follow_up", + "blocked_incident", + "blocked_incident_failed", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_DECISIONS: set[str] = {"deliver", "hold", "escalate", "block"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_terminal_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release terminal verdict report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release terminal verdict payload must be object") + + required_fields = ( + "source_release_incident_report", + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_terminal_verdict_status", + "release_terminal_verdict_decision", + "release_terminal_verdict_exit_code", + "terminal_should_ship_release", + "terminal_requires_follow_up", + "terminal_incident_linked", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_verdict_status = _coerce_str( + payload["release_terminal_verdict_status"], + field="release_terminal_verdict_status", + path=path, + ) + if terminal_verdict_status not in ALLOWED_RELEASE_TERMINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_terminal_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_TERMINAL_VERDICT_STATUSES)}" + ) + + terminal_verdict_decision = _coerce_str( + payload["release_terminal_verdict_decision"], + field="release_terminal_verdict_decision", + path=path, + ) + if terminal_verdict_decision not in ALLOWED_RELEASE_TERMINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_terminal_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_TERMINAL_VERDICT_DECISIONS)}" + ) + + terminal_verdict_exit_code = _coerce_int( + payload["release_terminal_verdict_exit_code"], + field="release_terminal_verdict_exit_code", + path=path, + ) + if terminal_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_terminal_verdict_exit_code' must be >= 0") + + incident_dispatch_status = _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ) + if incident_dispatch_status not in ALLOWED_INCIDENT_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'incident_dispatch_status' must be one of " + f"{sorted(ALLOWED_INCIDENT_DISPATCH_STATUSES)}" + ) + + incident_dispatch_exit_code = _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ) + if incident_dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'incident_dispatch_exit_code' must be >= 0") + + release_archive_exit_code = _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ) + if release_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_archive_exit_code' must be >= 0") + + release_verdict_exit_code = _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ) + if release_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_verdict_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_incident_report": _coerce_str( + payload["source_release_incident_report"], + field="source_release_incident_report", + path=path, + ), + "source_release_verdict_report": _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ), + "source_release_archive_report": _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ), + "release_archive_status": _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ), + "release_archive_decision": _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ), + "release_archive_exit_code": release_archive_exit_code, + "should_publish_archive": _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ), + "release_verdict_status": _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ), + "release_verdict_decision": _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ), + "release_verdict_exit_code": release_verdict_exit_code, + "should_ship_release": _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ), + "should_open_incident": _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ), + "should_dispatch_incident": _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ), + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ), + "incident_url": _coerce_str(payload["incident_url"], field="incident_url", path=path), + "release_terminal_verdict_status": terminal_verdict_status, + "release_terminal_verdict_decision": terminal_verdict_decision, + "release_terminal_verdict_exit_code": terminal_verdict_exit_code, + "terminal_should_ship_release": _coerce_bool( + payload["terminal_should_ship_release"], + field="terminal_should_ship_release", + path=path, + ), + "terminal_requires_follow_up": _coerce_bool( + payload["terminal_requires_follow_up"], + field="terminal_requires_follow_up", + path=path, + ), + "terminal_incident_linked": _coerce_bool( + payload["terminal_incident_linked"], + field="terminal_incident_linked", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_delivery_payload( + terminal_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + terminal_verdict_status = str(terminal_verdict_report["release_terminal_verdict_status"]) + terminal_verdict_decision = str(terminal_verdict_report["release_terminal_verdict_decision"]) + terminal_verdict_exit_code = int(terminal_verdict_report["release_terminal_verdict_exit_code"]) + terminal_should_ship_release = bool(terminal_verdict_report["terminal_should_ship_release"]) + terminal_requires_follow_up = bool(terminal_verdict_report["terminal_requires_follow_up"]) + should_dispatch_incident = bool(terminal_verdict_report["should_dispatch_incident"]) + incident_dispatch_status = str(terminal_verdict_report["incident_dispatch_status"]) + incident_dispatch_exit_code = int(terminal_verdict_report["incident_dispatch_exit_code"]) + + reason_codes = list(terminal_verdict_report["reason_codes"]) + structural_issues = list(terminal_verdict_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_report["missing_artifacts"]) + evidence_manifest = list(terminal_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_delivery_evidence_missing") + + if terminal_verdict_status == "released": + if terminal_verdict_decision != "ship": + structural_issues.append("terminal_decision_mismatch_released") + if terminal_verdict_exit_code != 0: + structural_issues.append("terminal_exit_code_mismatch_released") + if not terminal_should_ship_release: + structural_issues.append("terminal_should_ship_mismatch_released") + if terminal_requires_follow_up: + structural_issues.append("terminal_follow_up_mismatch_released") + if incident_dispatch_status != "not_required": + structural_issues.append("incident_status_mismatch_released") + elif terminal_verdict_status == "awaiting_archive": + if terminal_verdict_decision != "hold": + structural_issues.append("terminal_decision_mismatch_awaiting_archive") + if terminal_should_ship_release: + structural_issues.append("terminal_should_ship_mismatch_awaiting_archive") + elif terminal_verdict_status == "blocked_incident_ready_dry_run": + if terminal_verdict_decision != "escalate": + structural_issues.append("terminal_decision_mismatch_incident_ready_dry_run") + if incident_dispatch_status != "ready_dry_run": + structural_issues.append("incident_status_mismatch_incident_ready_dry_run") + if not should_dispatch_incident: + structural_issues.append("should_dispatch_incident_mismatch_ready_dry_run") + elif terminal_verdict_status == "blocked_incident_dispatched": + if terminal_verdict_decision != "escalate": + structural_issues.append("terminal_decision_mismatch_incident_dispatched") + if incident_dispatch_status != "dispatched": + structural_issues.append("incident_status_mismatch_incident_dispatched") + elif terminal_verdict_status == "blocked_incident_failed": + if terminal_verdict_decision != "block": + structural_issues.append("terminal_decision_mismatch_incident_failed") + if incident_dispatch_status != "dispatch_failed": + structural_issues.append("incident_status_mismatch_incident_failed") + elif terminal_verdict_status == "blocked": + if terminal_verdict_decision != "block": + structural_issues.append("terminal_decision_mismatch_blocked") + elif terminal_verdict_status == "contract_failed": + if terminal_verdict_decision != "block": + structural_issues.append("terminal_decision_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + release_delivery_status = "contract_failed" + release_delivery_decision = "block" + release_delivery_exit_code = 1 + delivery_should_ship_release = False + delivery_requires_human_action = True + delivery_should_announce_blocker = True + reason_codes.extend(structural_issues) + elif terminal_verdict_status == "released": + release_delivery_status = "shipped" + release_delivery_decision = "deliver" + release_delivery_exit_code = 0 + delivery_should_ship_release = True + delivery_requires_human_action = False + delivery_should_announce_blocker = False + reason_codes = ["release_delivery_shipped"] + elif terminal_verdict_status == "awaiting_archive": + release_delivery_status = "pending_follow_up" + release_delivery_decision = "hold" + release_delivery_exit_code = max(1, terminal_verdict_exit_code) + delivery_should_ship_release = False + delivery_requires_human_action = True + delivery_should_announce_blocker = True + elif terminal_verdict_status in {"blocked_incident_ready_dry_run", "blocked_incident_dispatched"}: + release_delivery_status = "blocked_incident" + release_delivery_decision = "escalate" + release_delivery_exit_code = max(1, terminal_verdict_exit_code, incident_dispatch_exit_code) + delivery_should_ship_release = False + delivery_requires_human_action = True + delivery_should_announce_blocker = True + reason_codes.append("release_delivery_incident_escalation_required") + elif terminal_verdict_status == "blocked_incident_failed": + release_delivery_status = "blocked_incident_failed" + release_delivery_decision = "block" + release_delivery_exit_code = max(1, incident_dispatch_exit_code, terminal_verdict_exit_code) + delivery_should_ship_release = False + delivery_requires_human_action = True + delivery_should_announce_blocker = True + reason_codes.append("release_delivery_incident_dispatch_failed") + else: + release_delivery_status = "blocked" + release_delivery_decision = "hold" + release_delivery_exit_code = max(1, terminal_verdict_exit_code) + delivery_should_ship_release = False + delivery_requires_human_action = True + delivery_should_announce_blocker = True + reason_codes.append("release_delivery_blocked") + + if release_delivery_status not in ALLOWED_RELEASE_DELIVERY_STATUSES: + raise ValueError("internal: unsupported release_delivery_status computed") + if release_delivery_decision not in ALLOWED_RELEASE_DELIVERY_DECISIONS: + raise ValueError("internal: unsupported release_delivery_decision computed") + + reason_codes = _unique(reason_codes) + release_delivery_summary = ( + f"release_terminal_verdict_status={terminal_verdict_status} " + f"release_delivery_status={release_delivery_status} " + f"release_delivery_decision={release_delivery_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_terminal_verdict_report": str(source_path), + "source_release_incident_report": str(terminal_verdict_report["source_release_incident_report"]), + "source_release_verdict_report": str(terminal_verdict_report["source_release_verdict_report"]), + "source_release_archive_report": str(terminal_verdict_report["source_release_archive_report"]), + "release_archive_status": str(terminal_verdict_report["release_archive_status"]), + "release_archive_decision": str(terminal_verdict_report["release_archive_decision"]), + "release_archive_exit_code": int(terminal_verdict_report["release_archive_exit_code"]), + "should_publish_archive": bool(terminal_verdict_report["should_publish_archive"]), + "release_verdict_status": str(terminal_verdict_report["release_verdict_status"]), + "release_verdict_decision": str(terminal_verdict_report["release_verdict_decision"]), + "release_verdict_exit_code": int(terminal_verdict_report["release_verdict_exit_code"]), + "should_ship_release": bool(terminal_verdict_report["should_ship_release"]), + "should_open_incident": bool(terminal_verdict_report["should_open_incident"]), + "should_dispatch_incident": should_dispatch_incident, + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": bool(terminal_verdict_report["incident_dispatch_attempted"]), + "incident_url": str(terminal_verdict_report["incident_url"]), + "release_terminal_verdict_status": terminal_verdict_status, + "release_terminal_verdict_decision": terminal_verdict_decision, + "release_terminal_verdict_exit_code": terminal_verdict_exit_code, + "terminal_should_ship_release": terminal_should_ship_release, + "terminal_requires_follow_up": terminal_requires_follow_up, + "terminal_incident_linked": bool(terminal_verdict_report["terminal_incident_linked"]), + "release_delivery_status": release_delivery_status, + "release_delivery_decision": release_delivery_decision, + "release_delivery_exit_code": int(release_delivery_exit_code), + "delivery_should_ship_release": delivery_should_ship_release, + "delivery_requires_human_action": delivery_requires_human_action, + "delivery_should_announce_blocker": delivery_should_announce_blocker, + "release_run_id": terminal_verdict_report["release_run_id"], + "release_run_url": str(terminal_verdict_report["release_run_url"]), + "release_delivery_summary": release_delivery_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Delivery Report", + "", + f"- Release Delivery Status: **{str(payload['release_delivery_status']).upper()}**", + f"- Release Delivery Decision: `{payload['release_delivery_decision']}`", + f"- Release Delivery Exit Code: `{payload['release_delivery_exit_code']}`", + f"- Delivery Should Ship Release: `{payload['delivery_should_ship_release']}`", + f"- Delivery Requires Human Action: `{payload['delivery_requires_human_action']}`", + f"- Delivery Should Announce Blocker: `{payload['delivery_should_announce_blocker']}`", + f"- Release Terminal Verdict Status: `{payload['release_terminal_verdict_status']}`", + f"- Incident Dispatch Status: `{payload['incident_dispatch_status']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_delivery_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_delivery_status": str(payload["release_delivery_status"]), + "workflow_release_delivery_decision": str(payload["release_delivery_decision"]), + "workflow_release_delivery_exit_code": str(payload["release_delivery_exit_code"]), + "workflow_release_delivery_should_ship_release": ( + "true" if payload["delivery_should_ship_release"] else "false" + ), + "workflow_release_delivery_requires_human_action": ( + "true" if payload["delivery_requires_human_action"] else "false" + ), + "workflow_release_delivery_should_announce_blocker": ( + "true" if payload["delivery_should_announce_blocker"] else "false" + ), + "workflow_release_delivery_terminal_verdict_status": str( + payload["release_terminal_verdict_status"] + ), + "workflow_release_delivery_incident_status": str(payload["incident_dispatch_status"]), + "workflow_release_delivery_incident_url": str(payload["incident_url"]), + "workflow_release_delivery_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_delivery_run_url": str(payload["release_run_url"]), + "workflow_release_delivery_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_delivery_report_json": str(output_json), + "workflow_release_delivery_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release delivery contract from " + "P2-40 release terminal verdict report" + ) + ) + parser.add_argument( + "--release-terminal-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json", + help="P2-40 release terminal verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery.json", + help="Output release delivery JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery.md", + help="Output release delivery markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release delivery JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_verdict_report_path = Path(args.release_terminal_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_verdict_report = load_release_terminal_verdict_report(terminal_verdict_report_path) + payload = build_release_delivery_payload( + terminal_verdict_report, + source_path=terminal_verdict_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-delivery-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release delivery json: {output_json_path}") + print(f"release delivery markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release delivery summary: " + f"release_delivery_status={payload['release_delivery_status']} " + f"release_delivery_decision={payload['release_delivery_decision']} " + f"release_delivery_exit_code={payload['release_delivery_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_delivery_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py new file mode 100644 index 0000000..4055f6c --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py @@ -0,0 +1,780 @@ +"""Phase 2 card P2-42 gate for Linux CI workflow release delivery terminal publish. + +This script consumes the P2-41 release delivery artifact and converges +the terminal publish contract: +1) validate release delivery + incident consistency, +2) normalize final announce/hold/block publish semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_STATUSES: set[str] = { + "shipped", + "pending_follow_up", + "blocked_incident", + "blocked_incident_failed", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_DECISIONS: set[str] = {"deliver", "hold", "escalate", "block"} +ALLOWED_INCIDENT_DISPATCH_STATUSES: set[str] = { + "not_required", + "ready_dry_run", + "dispatched", + "dispatch_failed", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "pending_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release", + "announce_hold", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "hold", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_delivery_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release delivery report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release delivery payload must be object") + + required_fields = ( + "source_release_terminal_verdict_report", + "source_release_incident_report", + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_terminal_verdict_status", + "release_terminal_verdict_decision", + "release_terminal_verdict_exit_code", + "terminal_should_ship_release", + "terminal_requires_follow_up", + "terminal_incident_linked", + "release_delivery_status", + "release_delivery_decision", + "release_delivery_exit_code", + "delivery_should_ship_release", + "delivery_requires_human_action", + "delivery_should_announce_blocker", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_status = _coerce_str( + payload["release_delivery_status"], + field="release_delivery_status", + path=path, + ) + if release_delivery_status not in ALLOWED_RELEASE_DELIVERY_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_STATUSES)}" + ) + + release_delivery_decision = _coerce_str( + payload["release_delivery_decision"], + field="release_delivery_decision", + path=path, + ) + if release_delivery_decision not in ALLOWED_RELEASE_DELIVERY_DECISIONS: + raise ValueError( + f"{path}: field 'release_delivery_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_DECISIONS)}" + ) + + release_delivery_exit_code = _coerce_int( + payload["release_delivery_exit_code"], + field="release_delivery_exit_code", + path=path, + ) + if release_delivery_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_exit_code' must be >= 0") + + incident_dispatch_status = _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ) + if incident_dispatch_status not in ALLOWED_INCIDENT_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'incident_dispatch_status' must be one of " + f"{sorted(ALLOWED_INCIDENT_DISPATCH_STATUSES)}" + ) + + incident_dispatch_exit_code = _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ) + if incident_dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'incident_dispatch_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_terminal_verdict_report": _coerce_str( + payload["source_release_terminal_verdict_report"], + field="source_release_terminal_verdict_report", + path=path, + ), + "source_release_incident_report": _coerce_str( + payload["source_release_incident_report"], + field="source_release_incident_report", + path=path, + ), + "source_release_verdict_report": _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ), + "source_release_archive_report": _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ), + "release_archive_status": _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ), + "release_archive_decision": _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ), + "release_archive_exit_code": _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ), + "should_publish_archive": _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ), + "release_verdict_status": _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ), + "release_verdict_decision": _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ), + "release_verdict_exit_code": _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ), + "should_ship_release": _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ), + "should_open_incident": _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ), + "should_dispatch_incident": _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ), + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ), + "incident_url": _coerce_str( + payload["incident_url"], + field="incident_url", + path=path, + ), + "release_terminal_verdict_status": _coerce_str( + payload["release_terminal_verdict_status"], + field="release_terminal_verdict_status", + path=path, + ), + "release_terminal_verdict_decision": _coerce_str( + payload["release_terminal_verdict_decision"], + field="release_terminal_verdict_decision", + path=path, + ), + "release_terminal_verdict_exit_code": _coerce_int( + payload["release_terminal_verdict_exit_code"], + field="release_terminal_verdict_exit_code", + path=path, + ), + "terminal_should_ship_release": _coerce_bool( + payload["terminal_should_ship_release"], + field="terminal_should_ship_release", + path=path, + ), + "terminal_requires_follow_up": _coerce_bool( + payload["terminal_requires_follow_up"], + field="terminal_requires_follow_up", + path=path, + ), + "terminal_incident_linked": _coerce_bool( + payload["terminal_incident_linked"], + field="terminal_incident_linked", + path=path, + ), + "release_delivery_status": release_delivery_status, + "release_delivery_decision": release_delivery_decision, + "release_delivery_exit_code": release_delivery_exit_code, + "delivery_should_ship_release": _coerce_bool( + payload["delivery_should_ship_release"], + field="delivery_should_ship_release", + path=path, + ), + "delivery_requires_human_action": _coerce_bool( + payload["delivery_requires_human_action"], + field="delivery_requires_human_action", + path=path, + ), + "delivery_should_announce_blocker": _coerce_bool( + payload["delivery_should_announce_blocker"], + field="delivery_should_announce_blocker", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_delivery_terminal_publish_payload( + release_delivery_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_delivery_status = str(release_delivery_report["release_delivery_status"]) + release_delivery_decision = str(release_delivery_report["release_delivery_decision"]) + release_delivery_exit_code = int(release_delivery_report["release_delivery_exit_code"]) + delivery_should_ship_release = bool(release_delivery_report["delivery_should_ship_release"]) + delivery_requires_human_action = bool(release_delivery_report["delivery_requires_human_action"]) + delivery_should_announce_blocker = bool( + release_delivery_report["delivery_should_announce_blocker"] + ) + should_dispatch_incident = bool(release_delivery_report["should_dispatch_incident"]) + incident_dispatch_status = str(release_delivery_report["incident_dispatch_status"]) + incident_dispatch_exit_code = int(release_delivery_report["incident_dispatch_exit_code"]) + + reason_codes = list(release_delivery_report["reason_codes"]) + structural_issues = list(release_delivery_report["structural_issues"]) + missing_artifacts = list(release_delivery_report["missing_artifacts"]) + evidence_manifest = list(release_delivery_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_delivery_terminal_evidence_missing") + + if release_delivery_status == "shipped": + if release_delivery_decision != "deliver": + structural_issues.append("delivery_decision_mismatch_shipped") + if release_delivery_exit_code != 0: + structural_issues.append("delivery_exit_code_mismatch_shipped") + if not delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_shipped") + if delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_shipped") + if delivery_should_announce_blocker: + structural_issues.append("delivery_should_announce_blocker_mismatch_shipped") + if incident_dispatch_status != "not_required": + structural_issues.append("incident_status_mismatch_shipped") + elif release_delivery_status == "pending_follow_up": + if release_delivery_decision != "hold": + structural_issues.append("delivery_decision_mismatch_pending_follow_up") + if release_delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_pending_follow_up") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_pending_follow_up") + if not delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_pending_follow_up") + elif release_delivery_status == "blocked_incident": + if release_delivery_decision != "escalate": + structural_issues.append("delivery_decision_mismatch_blocked_incident") + if release_delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_blocked_incident") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_blocked_incident") + if not delivery_requires_human_action: + structural_issues.append("delivery_requires_human_action_mismatch_blocked_incident") + if incident_dispatch_status not in {"ready_dry_run", "dispatched"}: + structural_issues.append("incident_status_mismatch_blocked_incident") + if not should_dispatch_incident: + structural_issues.append("should_dispatch_incident_mismatch_blocked_incident") + elif release_delivery_status == "blocked_incident_failed": + if release_delivery_decision != "block": + structural_issues.append("delivery_decision_mismatch_blocked_incident_failed") + if release_delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_blocked_incident_failed") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_blocked_incident_failed") + if incident_dispatch_status != "dispatch_failed": + structural_issues.append("incident_status_mismatch_blocked_incident_failed") + elif release_delivery_status == "blocked": + if release_delivery_decision != "hold": + structural_issues.append("delivery_decision_mismatch_blocked") + if release_delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_blocked") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_blocked") + elif release_delivery_status == "contract_failed": + if release_delivery_decision != "block": + structural_issues.append("delivery_decision_mismatch_contract_failed") + if release_delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_contract_failed") + if delivery_should_ship_release: + structural_issues.append("delivery_should_ship_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + terminal_publish_status = "contract_failed" + terminal_publish_decision = "abort_publish" + terminal_publish_exit_code = 1 + terminal_publish_should_notify = True + terminal_publish_should_create_follow_up = True + terminal_publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif release_delivery_status == "shipped": + terminal_publish_status = "published" + terminal_publish_decision = "announce_release" + terminal_publish_exit_code = 0 + terminal_publish_should_notify = True + terminal_publish_should_create_follow_up = False + terminal_publish_channel = "release" + reason_codes = ["release_delivery_terminal_published"] + elif release_delivery_status == "pending_follow_up": + terminal_publish_status = "pending_follow_up" + terminal_publish_decision = "announce_hold" + terminal_publish_exit_code = max(1, release_delivery_exit_code) + terminal_publish_should_notify = True + terminal_publish_should_create_follow_up = True + terminal_publish_channel = "hold" + reason_codes.append("release_delivery_terminal_pending_follow_up") + elif release_delivery_status == "contract_failed": + terminal_publish_status = "contract_failed" + terminal_publish_decision = "abort_publish" + terminal_publish_exit_code = max(1, release_delivery_exit_code) + terminal_publish_should_notify = True + terminal_publish_should_create_follow_up = True + terminal_publish_channel = "blocker" + reason_codes.append("release_delivery_terminal_upstream_contract_failed") + else: + terminal_publish_status = "blocked" + terminal_publish_decision = "announce_blocker" + terminal_publish_exit_code = max( + 1, + release_delivery_exit_code, + incident_dispatch_exit_code, + ) + terminal_publish_should_notify = True + terminal_publish_should_create_follow_up = True + terminal_publish_channel = "blocker" + reason_codes.append("release_delivery_terminal_blocked") + + if terminal_publish_status not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_STATUSES: + raise ValueError("internal: unsupported release_delivery_terminal_publish_status computed") + if terminal_publish_decision not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported release_delivery_terminal_publish_decision computed") + if terminal_publish_channel not in ALLOWED_RELEASE_DELIVERY_TERMINAL_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported release_delivery_terminal_publish_channel computed") + + reason_codes = _unique(reason_codes) + release_delivery_terminal_publish_summary = ( + f"release_delivery_status={release_delivery_status} " + f"release_delivery_terminal_publish_status={terminal_publish_status} " + f"release_delivery_terminal_publish_decision={terminal_publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_delivery_report": str(source_path), + "source_release_terminal_verdict_report": str( + release_delivery_report["source_release_terminal_verdict_report"] + ), + "source_release_incident_report": str(release_delivery_report["source_release_incident_report"]), + "source_release_verdict_report": str(release_delivery_report["source_release_verdict_report"]), + "source_release_archive_report": str(release_delivery_report["source_release_archive_report"]), + "release_archive_status": str(release_delivery_report["release_archive_status"]), + "release_archive_decision": str(release_delivery_report["release_archive_decision"]), + "release_archive_exit_code": int(release_delivery_report["release_archive_exit_code"]), + "should_publish_archive": bool(release_delivery_report["should_publish_archive"]), + "release_verdict_status": str(release_delivery_report["release_verdict_status"]), + "release_verdict_decision": str(release_delivery_report["release_verdict_decision"]), + "release_verdict_exit_code": int(release_delivery_report["release_verdict_exit_code"]), + "should_ship_release": bool(release_delivery_report["should_ship_release"]), + "should_open_incident": bool(release_delivery_report["should_open_incident"]), + "should_dispatch_incident": should_dispatch_incident, + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": bool(release_delivery_report["incident_dispatch_attempted"]), + "incident_url": str(release_delivery_report["incident_url"]), + "release_terminal_verdict_status": str( + release_delivery_report["release_terminal_verdict_status"] + ), + "release_terminal_verdict_decision": str( + release_delivery_report["release_terminal_verdict_decision"] + ), + "release_terminal_verdict_exit_code": int( + release_delivery_report["release_terminal_verdict_exit_code"] + ), + "terminal_should_ship_release": bool(release_delivery_report["terminal_should_ship_release"]), + "terminal_requires_follow_up": bool(release_delivery_report["terminal_requires_follow_up"]), + "terminal_incident_linked": bool(release_delivery_report["terminal_incident_linked"]), + "release_delivery_status": release_delivery_status, + "release_delivery_decision": release_delivery_decision, + "release_delivery_exit_code": release_delivery_exit_code, + "delivery_should_ship_release": delivery_should_ship_release, + "delivery_requires_human_action": delivery_requires_human_action, + "delivery_should_announce_blocker": delivery_should_announce_blocker, + "release_delivery_terminal_publish_status": terminal_publish_status, + "release_delivery_terminal_publish_decision": terminal_publish_decision, + "release_delivery_terminal_publish_exit_code": int(terminal_publish_exit_code), + "terminal_publish_should_notify": terminal_publish_should_notify, + "terminal_publish_should_create_follow_up": terminal_publish_should_create_follow_up, + "terminal_publish_channel": terminal_publish_channel, + "release_run_id": release_delivery_report["release_run_id"], + "release_run_url": str(release_delivery_report["release_run_url"]), + "release_delivery_terminal_publish_summary": release_delivery_terminal_publish_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Delivery Terminal Publish Report", + "", + ( + "- Release Delivery Terminal Publish Status: " + f"**{str(payload['release_delivery_terminal_publish_status']).upper()}**" + ), + ( + "- Release Delivery Terminal Publish Decision: " + f"`{payload['release_delivery_terminal_publish_decision']}`" + ), + ( + "- Release Delivery Terminal Publish Exit Code: " + f"`{payload['release_delivery_terminal_publish_exit_code']}`" + ), + f"- Terminal Publish Should Notify: `{payload['terminal_publish_should_notify']}`", + ( + "- Terminal Publish Should Create Follow Up: " + f"`{payload['terminal_publish_should_create_follow_up']}`" + ), + f"- Terminal Publish Channel: `{payload['terminal_publish_channel']}`", + f"- Release Delivery Status: `{payload['release_delivery_status']}`", + f"- Incident Dispatch Status: `{payload['incident_dispatch_status']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_delivery_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_delivery_terminal_publish_status": str( + payload["release_delivery_terminal_publish_status"] + ), + "workflow_release_delivery_terminal_publish_decision": str( + payload["release_delivery_terminal_publish_decision"] + ), + "workflow_release_delivery_terminal_publish_exit_code": str( + payload["release_delivery_terminal_publish_exit_code"] + ), + "workflow_release_delivery_terminal_publish_should_notify": ( + "true" if payload["terminal_publish_should_notify"] else "false" + ), + "workflow_release_delivery_terminal_publish_should_create_follow_up": ( + "true" if payload["terminal_publish_should_create_follow_up"] else "false" + ), + "workflow_release_delivery_terminal_publish_channel": str( + payload["terminal_publish_channel"] + ), + "workflow_release_delivery_terminal_publish_delivery_status": str( + payload["release_delivery_status"] + ), + "workflow_release_delivery_terminal_publish_incident_status": str( + payload["incident_dispatch_status"] + ), + "workflow_release_delivery_terminal_publish_incident_url": str(payload["incident_url"]), + "workflow_release_delivery_terminal_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_delivery_terminal_publish_run_url": str(payload["release_run_url"]), + "workflow_release_delivery_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_delivery_terminal_publish_report_json": str(output_json), + "workflow_release_delivery_terminal_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release delivery terminal publish contract " + "from P2-41 release delivery report" + ) + ) + parser.add_argument( + "--release-delivery-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery.json", + help="P2-41 release delivery report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json", + help="Output release delivery terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md", + help="Output release delivery terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release delivery terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_delivery_report_path = Path(args.release_delivery_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_delivery_report = load_release_delivery_report(release_delivery_report_path) + payload = build_release_delivery_terminal_publish_payload( + release_delivery_report, + source_path=release_delivery_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-delivery-terminal-publish-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release delivery terminal publish json: {output_json_path}") + print(f"release delivery terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release delivery terminal publish summary: " + f"release_delivery_terminal_publish_status=" + f"{payload['release_delivery_terminal_publish_status']} " + f"release_delivery_terminal_publish_decision=" + f"{payload['release_delivery_terminal_publish_decision']} " + f"release_delivery_terminal_publish_exit_code=" + f"{payload['release_delivery_terminal_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_delivery_terminal_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py new file mode 100644 index 0000000..8f66cdb --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py @@ -0,0 +1,974 @@ +"""Phase 2 card P2-53 gate for Linux CI workflow release final archive. + +This script consumes the P2-52 release final closure publish artifact and +converges one terminal final-archive contract: +1) validate final closure publish + evidence consistency, +2) normalize archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release_closure", + "announce_release_with_follow_up", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: set[str] = { + "completed", + "completed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: set[str] = { + "handoff_release_closure", + "handoff_release_with_follow_up", + "handoff_blocker", + "abort_handoff", +} +ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: set[str] = { + "closed", + "closed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: set[str] = { + "close_release", + "close_with_follow_up", + "close_blocker", + "abort_close", +} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_DECISIONS: set[str] = { + "announce_release_closed", + "announce_release_closed_with_follow_up", + "announce_release_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_ARCHIVE_DECISIONS: set[str] = { + "archive_release_closed", + "archive_release_closed_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_RELEASE_FINAL_ARCHIVE_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_closure_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final closure publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final closure publish payload must be object") + + required_fields = ( + "source_release_final_closure_report", + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_publish_status", + "release_final_closure_publish_decision", + "release_final_closure_publish_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_closure_publish_should_notify", + "final_closure_publish_requires_manual_action", + "final_closure_publish_channel", + "release_final_closure_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_final_verdict_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if release_delivery_final_verdict_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + release_delivery_final_verdict_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if ( + release_delivery_final_verdict_decision + not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + release_follow_up_final_verdict_status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if release_follow_up_final_verdict_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + release_follow_up_final_verdict_decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if ( + release_follow_up_final_verdict_decision + not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + release_final_outcome_status = _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ) + if release_final_outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError( + f"{path}: field 'release_final_outcome_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES)}" + ) + release_final_outcome_decision = _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ) + if release_final_outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_outcome_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS)}" + ) + + release_final_terminal_publish_status = _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ) + if release_final_terminal_publish_status not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES)}" + ) + release_final_terminal_publish_decision = _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ) + if ( + release_final_terminal_publish_decision + not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_final_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS)}" + ) + + release_final_handoff_status = _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ) + if release_final_handoff_status not in ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: + raise ValueError( + f"{path}: field 'release_final_handoff_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES)}" + ) + release_final_handoff_decision = _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ) + if release_final_handoff_decision not in ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_handoff_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS)}" + ) + + release_final_closure_status = _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ) + if release_final_closure_status not in ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: + raise ValueError( + f"{path}: field 'release_final_closure_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES)}" + ) + release_final_closure_decision = _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ) + if release_final_closure_decision not in ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_closure_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS)}" + ) + + release_final_closure_publish_status = _coerce_str( + payload["release_final_closure_publish_status"], + field="release_final_closure_publish_status", + path=path, + ) + if release_final_closure_publish_status not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_closure_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_STATUSES)}" + ) + release_final_closure_publish_decision = _coerce_str( + payload["release_final_closure_publish_decision"], + field="release_final_closure_publish_decision", + path=path, + ) + if ( + release_final_closure_publish_decision + not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_final_closure_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_DECISIONS)}" + ) + final_closure_publish_channel = _coerce_str( + payload["final_closure_publish_channel"], + field="final_closure_publish_channel", + path=path, + ) + if final_closure_publish_channel not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'final_closure_publish_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_CHANNELS)}" + ) + + release_delivery_final_verdict_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + release_follow_up_final_verdict_exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + release_final_outcome_exit_code = _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ) + release_final_terminal_publish_exit_code = _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ) + release_final_handoff_exit_code = _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ) + release_final_closure_exit_code = _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ) + release_final_closure_publish_exit_code = _coerce_int( + payload["release_final_closure_publish_exit_code"], + field="release_final_closure_publish_exit_code", + path=path, + ) + if release_delivery_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + if release_follow_up_final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + if release_final_outcome_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_outcome_exit_code' must be >= 0") + if release_final_terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_exit_code' must be >= 0" + ) + if release_final_handoff_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_handoff_exit_code' must be >= 0") + if release_final_closure_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_closure_exit_code' must be >= 0") + if release_final_closure_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_final_closure_publish_exit_code' must be >= 0" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_closure_report": _coerce_str( + payload["source_release_final_closure_report"], + field="source_release_final_closure_report", + path=path, + ), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_final_outcome_status": release_final_outcome_status, + "release_final_outcome_decision": release_final_outcome_decision, + "release_final_outcome_exit_code": release_final_outcome_exit_code, + "release_final_terminal_publish_status": release_final_terminal_publish_status, + "release_final_terminal_publish_decision": release_final_terminal_publish_decision, + "release_final_terminal_publish_exit_code": release_final_terminal_publish_exit_code, + "release_final_handoff_status": release_final_handoff_status, + "release_final_handoff_decision": release_final_handoff_decision, + "release_final_handoff_exit_code": release_final_handoff_exit_code, + "release_final_closure_status": release_final_closure_status, + "release_final_closure_decision": release_final_closure_decision, + "release_final_closure_exit_code": release_final_closure_exit_code, + "release_final_closure_publish_status": release_final_closure_publish_status, + "release_final_closure_publish_decision": release_final_closure_publish_decision, + "release_final_closure_publish_exit_code": release_final_closure_publish_exit_code, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_closure_publish_should_notify": _coerce_bool( + payload["final_closure_publish_should_notify"], + field="final_closure_publish_should_notify", + path=path, + ), + "final_closure_publish_requires_manual_action": _coerce_bool( + payload["final_closure_publish_requires_manual_action"], + field="final_closure_publish_requires_manual_action", + path=path, + ), + "final_closure_publish_channel": final_closure_publish_channel, + "release_final_closure_publish_summary": _coerce_str( + payload["release_final_closure_publish_summary"], + field="release_final_closure_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_archive_payload( + release_final_closure_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_closure_publish_status = str( + release_final_closure_publish_report["release_final_closure_publish_status"] + ) + final_closure_publish_decision = str( + release_final_closure_publish_report["release_final_closure_publish_decision"] + ) + final_closure_publish_exit_code = int( + release_final_closure_publish_report["release_final_closure_publish_exit_code"] + ) + final_closure_publish_should_notify = bool( + release_final_closure_publish_report["final_closure_publish_should_notify"] + ) + final_closure_publish_requires_manual_action = bool( + release_final_closure_publish_report["final_closure_publish_requires_manual_action"] + ) + final_closure_publish_channel = str( + release_final_closure_publish_report["final_closure_publish_channel"] + ) + + reason_codes = list(release_final_closure_publish_report["reason_codes"]) + structural_issues = list(release_final_closure_publish_report["structural_issues"]) + missing_artifacts = list(release_final_closure_publish_report["missing_artifacts"]) + evidence_manifest = list(release_final_closure_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_archive_evidence_missing") + + expected = { + "published": ( + "announce_release_closed", + 0, + True, + False, + "release", + ), + "published_with_follow_up": ( + "announce_release_closed_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_release_blocker", + None, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_channel, + ) = expected[final_closure_publish_status] + + if final_closure_publish_decision != expected_decision: + structural_issues.append("final_closure_publish_decision_mismatch") + if final_closure_publish_should_notify != expected_should_notify: + structural_issues.append("final_closure_publish_should_notify_mismatch") + if final_closure_publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append("final_closure_publish_requires_manual_action_mismatch") + if final_closure_publish_channel != expected_channel: + structural_issues.append("final_closure_publish_channel_mismatch") + if expected_exit_code == 0 and final_closure_publish_exit_code != 0: + structural_issues.append("final_closure_publish_exit_code_mismatch_published") + if expected_exit_code is None and final_closure_publish_exit_code == 0: + structural_issues.append("final_closure_publish_exit_code_mismatch_non_published") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + archive_status = "contract_failed" + archive_decision = "abort_archive" + archive_exit_code = max(1, final_closure_publish_exit_code) + archive_should_publish = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_closure_publish_status == "published": + archive_status = "archived" + archive_decision = "archive_release_closed" + archive_exit_code = 0 + archive_should_publish = True + archive_requires_manual_action = False + archive_channel = "release" + reason_codes = ["release_final_archive_archived"] + elif final_closure_publish_status == "published_with_follow_up": + archive_status = "archived_with_follow_up" + archive_decision = "archive_release_closed_with_follow_up" + archive_exit_code = max(1, final_closure_publish_exit_code) + archive_should_publish = True + archive_requires_manual_action = False + archive_channel = "follow_up" + reason_codes.append("release_final_archive_archived_with_follow_up") + elif final_closure_publish_status == "blocked": + archive_status = "blocked" + archive_decision = "archive_release_blocker" + archive_exit_code = max(1, final_closure_publish_exit_code) + archive_should_publish = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.append("release_final_archive_blocked") + else: + archive_status = "contract_failed" + archive_decision = "abort_archive" + archive_exit_code = max(1, final_closure_publish_exit_code) + archive_should_publish = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.append("release_final_archive_upstream_contract_failed") + + if archive_status not in ALLOWED_RELEASE_FINAL_ARCHIVE_STATUSES: + raise ValueError("internal: unsupported release_final_archive_status computed") + if archive_decision not in ALLOWED_RELEASE_FINAL_ARCHIVE_DECISIONS: + raise ValueError("internal: unsupported release_final_archive_decision computed") + if archive_channel not in ALLOWED_RELEASE_FINAL_ARCHIVE_CHANNELS: + raise ValueError("internal: unsupported release_final_archive_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_closure_publish_status={final_closure_publish_status} " + f"release_final_archive_status={archive_status} " + f"release_final_archive_decision={archive_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_closure_publish_report": str(source_path), + "source_release_final_closure_report": str( + release_final_closure_publish_report["source_release_final_closure_report"] + ), + "source_release_final_handoff_report": str( + release_final_closure_publish_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_closure_publish_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_closure_publish_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_closure_publish_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_closure_publish_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_closure_publish_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_closure_publish_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_closure_publish_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_closure_publish_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_closure_publish_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_closure_publish_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_closure_publish_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_closure_publish_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_closure_publish_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_closure_publish_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_closure_publish_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_closure_publish_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str( + release_final_closure_publish_report["release_final_handoff_status"] + ), + "release_final_handoff_decision": str( + release_final_closure_publish_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_closure_publish_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": str( + release_final_closure_publish_report["release_final_closure_status"] + ), + "release_final_closure_decision": str( + release_final_closure_publish_report["release_final_closure_decision"] + ), + "release_final_closure_exit_code": int( + release_final_closure_publish_report["release_final_closure_exit_code"] + ), + "release_final_closure_publish_status": final_closure_publish_status, + "release_final_closure_publish_decision": final_closure_publish_decision, + "release_final_closure_publish_exit_code": final_closure_publish_exit_code, + "release_run_id": release_final_closure_publish_report["release_run_id"], + "release_run_url": str(release_final_closure_publish_report["release_run_url"]), + "follow_up_queue_url": str(release_final_closure_publish_report["follow_up_queue_url"]), + "final_archive_should_publish": archive_should_publish, + "final_archive_requires_manual_action": archive_requires_manual_action, + "final_archive_channel": archive_channel, + "release_final_archive_status": archive_status, + "release_final_archive_decision": archive_decision, + "release_final_archive_exit_code": archive_exit_code, + "release_final_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Archive Report", + "", + f"- Release Final Archive Status: **{str(payload['release_final_archive_status']).upper()}**", + f"- Release Final Archive Decision: `{payload['release_final_archive_decision']}`", + f"- Release Final Archive Exit Code: `{payload['release_final_archive_exit_code']}`", + f"- Final Archive Should Publish: `{payload['final_archive_should_publish']}`", + ( + f"- Final Archive Requires Manual Action: " + f"`{payload['final_archive_requires_manual_action']}`" + ), + f"- Final Archive Channel: `{payload['final_archive_channel']}`", + f"- Release Final Closure Publish Status: `{payload['release_final_closure_publish_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_archive_status": str(payload["release_final_archive_status"]), + "workflow_release_final_archive_decision": str(payload["release_final_archive_decision"]), + "workflow_release_final_archive_exit_code": str(payload["release_final_archive_exit_code"]), + "workflow_release_final_archive_should_publish": ( + "true" if payload["final_archive_should_publish"] else "false" + ), + "workflow_release_final_archive_requires_manual_action": ( + "true" if payload["final_archive_requires_manual_action"] else "false" + ), + "workflow_release_final_archive_channel": str(payload["final_archive_channel"]), + "workflow_release_final_archive_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_archive_run_url": str(payload["release_run_url"]), + "workflow_release_final_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_archive_report_json": str(output_json), + "workflow_release_final_archive_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final archive contract " + "from P2-52 release final closure publish report" + ) + ) + parser.add_argument( + "--release-final-closure-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json", + help="P2-52 release final closure publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json", + help="Output release final archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md", + help="Output release final archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_closure_publish_report_path = Path(args.release_final_closure_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_closure_publish_report = load_release_final_closure_publish_report( + release_final_closure_publish_report_path + ) + payload = build_release_final_archive_payload( + release_final_closure_publish_report, + source_path=release_final_closure_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-release-final-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final archive json: {output_json_path}") + print(f"release final archive markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final archive summary: " + f"release_final_archive_status={payload['release_final_archive_status']} " + f"release_final_archive_decision={payload['release_final_archive_decision']} " + f"release_final_archive_exit_code={payload['release_final_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_archive_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py new file mode 100644 index 0000000..03fdd39 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py @@ -0,0 +1,857 @@ +"""Phase 2 card P2-51 gate for Linux CI workflow release final closure. + +This script consumes the P2-50 release final handoff artifact and converges +one terminal closure contract: +1) validate release final handoff + evidence consistency, +2) normalize release closure semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release_closure", + "announce_release_with_follow_up", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} +ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: set[str] = { + "completed", + "completed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: set[str] = { + "handoff_release_closure", + "handoff_release_with_follow_up", + "handoff_blocker", + "abort_handoff", +} +ALLOWED_RELEASE_FINAL_HANDOFF_TARGETS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: set[str] = { + "closed", + "closed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: set[str] = { + "close_release", + "close_with_follow_up", + "close_blocker", + "abort_close", +} +ALLOWED_RELEASE_FINAL_CLOSURE_TARGETS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_handoff_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final handoff report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final handoff payload must be object") + + required_fields = ( + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_handoff_should_archive_release", + "final_handoff_keep_follow_up_open", + "final_handoff_should_page_owner", + "final_handoff_target", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_handoff_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_final_verdict_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if release_delivery_final_verdict_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + release_delivery_final_verdict_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if ( + release_delivery_final_verdict_decision + not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + release_follow_up_final_verdict_status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if release_follow_up_final_verdict_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + release_follow_up_final_verdict_decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if ( + release_follow_up_final_verdict_decision + not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + release_final_outcome_status = _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ) + if release_final_outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError( + f"{path}: field 'release_final_outcome_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES)}" + ) + release_final_outcome_decision = _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ) + if release_final_outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_outcome_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS)}" + ) + + release_final_terminal_publish_status = _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ) + if release_final_terminal_publish_status not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES)}" + ) + release_final_terminal_publish_decision = _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ) + if ( + release_final_terminal_publish_decision + not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_final_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS)}" + ) + + final_handoff_status = _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ) + if final_handoff_status not in ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: + raise ValueError( + f"{path}: field 'release_final_handoff_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES)}" + ) + final_handoff_decision = _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ) + if final_handoff_decision not in ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_handoff_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS)}" + ) + final_handoff_target = _coerce_str( + payload["final_handoff_target"], + field="final_handoff_target", + path=path, + ) + if final_handoff_target not in ALLOWED_RELEASE_FINAL_HANDOFF_TARGETS: + raise ValueError( + f"{path}: field 'final_handoff_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_TARGETS)}" + ) + + release_delivery_final_verdict_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + release_follow_up_final_verdict_exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + release_final_outcome_exit_code = _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ) + release_final_terminal_publish_exit_code = _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ) + release_final_handoff_exit_code = _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ) + if release_delivery_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + if release_follow_up_final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + if release_final_outcome_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_outcome_exit_code' must be >= 0") + if release_final_terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_exit_code' must be >= 0" + ) + if release_final_handoff_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_handoff_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_final_outcome_status": release_final_outcome_status, + "release_final_outcome_decision": release_final_outcome_decision, + "release_final_outcome_exit_code": release_final_outcome_exit_code, + "release_final_terminal_publish_status": release_final_terminal_publish_status, + "release_final_terminal_publish_decision": release_final_terminal_publish_decision, + "release_final_terminal_publish_exit_code": release_final_terminal_publish_exit_code, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_handoff_should_archive_release": _coerce_bool( + payload["final_handoff_should_archive_release"], + field="final_handoff_should_archive_release", + path=path, + ), + "final_handoff_keep_follow_up_open": _coerce_bool( + payload["final_handoff_keep_follow_up_open"], + field="final_handoff_keep_follow_up_open", + path=path, + ), + "final_handoff_should_page_owner": _coerce_bool( + payload["final_handoff_should_page_owner"], + field="final_handoff_should_page_owner", + path=path, + ), + "final_handoff_target": final_handoff_target, + "release_final_handoff_status": final_handoff_status, + "release_final_handoff_decision": final_handoff_decision, + "release_final_handoff_exit_code": release_final_handoff_exit_code, + "release_final_handoff_summary": _coerce_str( + payload["release_final_handoff_summary"], + field="release_final_handoff_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_closure_payload( + release_final_handoff_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_handoff_status = str(release_final_handoff_report["release_final_handoff_status"]) + final_handoff_decision = str(release_final_handoff_report["release_final_handoff_decision"]) + final_handoff_exit_code = int(release_final_handoff_report["release_final_handoff_exit_code"]) + final_handoff_should_archive_release = bool( + release_final_handoff_report["final_handoff_should_archive_release"] + ) + final_handoff_keep_follow_up_open = bool( + release_final_handoff_report["final_handoff_keep_follow_up_open"] + ) + final_handoff_should_page_owner = bool( + release_final_handoff_report["final_handoff_should_page_owner"] + ) + final_handoff_target = str(release_final_handoff_report["final_handoff_target"]) + + reason_codes = list(release_final_handoff_report["reason_codes"]) + structural_issues = list(release_final_handoff_report["structural_issues"]) + missing_artifacts = list(release_final_handoff_report["missing_artifacts"]) + evidence_manifest = list(release_final_handoff_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_closure_evidence_missing") + + expected = { + "completed": ( + "handoff_release_closure", + 0, + True, + False, + False, + "release", + ), + "completed_with_follow_up": ( + "handoff_release_with_follow_up", + None, + True, + True, + False, + "follow_up", + ), + "blocked": ( + "handoff_blocker", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_handoff", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_archive, + expected_follow_up_open, + expected_page_owner, + expected_target, + ) = expected[final_handoff_status] + + if final_handoff_decision != expected_decision: + structural_issues.append("final_handoff_decision_mismatch") + if final_handoff_should_archive_release != expected_archive: + structural_issues.append("final_handoff_should_archive_release_mismatch") + if final_handoff_keep_follow_up_open != expected_follow_up_open: + structural_issues.append("final_handoff_keep_follow_up_open_mismatch") + if final_handoff_should_page_owner != expected_page_owner: + structural_issues.append("final_handoff_should_page_owner_mismatch") + if final_handoff_target != expected_target: + structural_issues.append("final_handoff_target_mismatch") + + if expected_exit_code == 0 and final_handoff_exit_code != 0: + structural_issues.append("final_handoff_exit_code_mismatch_completed") + if expected_exit_code is None and final_handoff_exit_code == 0: + structural_issues.append("final_handoff_exit_code_mismatch_non_completed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + final_closure_status = "contract_failed" + final_closure_decision = "abort_close" + final_closure_exit_code = max(1, final_handoff_exit_code) + final_closure_is_closed = False + final_closure_has_open_follow_up = True + final_closure_should_page_owner = True + final_closure_target = "blocker" + reason_codes.extend(structural_issues) + elif final_handoff_status == "completed": + final_closure_status = "closed" + final_closure_decision = "close_release" + final_closure_exit_code = 0 + final_closure_is_closed = True + final_closure_has_open_follow_up = False + final_closure_should_page_owner = False + final_closure_target = "release" + reason_codes = ["release_final_closure_closed"] + elif final_handoff_status == "completed_with_follow_up": + final_closure_status = "closed_with_follow_up" + final_closure_decision = "close_with_follow_up" + final_closure_exit_code = max(1, final_handoff_exit_code) + final_closure_is_closed = True + final_closure_has_open_follow_up = True + final_closure_should_page_owner = False + final_closure_target = "follow_up" + reason_codes.append("release_final_closure_closed_with_follow_up") + elif final_handoff_status == "blocked": + final_closure_status = "blocked" + final_closure_decision = "close_blocker" + final_closure_exit_code = max(1, final_handoff_exit_code) + final_closure_is_closed = False + final_closure_has_open_follow_up = True + final_closure_should_page_owner = True + final_closure_target = "blocker" + reason_codes.append("release_final_closure_blocked") + else: + final_closure_status = "contract_failed" + final_closure_decision = "abort_close" + final_closure_exit_code = max(1, final_handoff_exit_code) + final_closure_is_closed = False + final_closure_has_open_follow_up = True + final_closure_should_page_owner = True + final_closure_target = "blocker" + reason_codes.append("release_final_closure_upstream_contract_failed") + + if final_closure_status not in ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: + raise ValueError("internal: unsupported release_final_closure_status computed") + if final_closure_decision not in ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: + raise ValueError("internal: unsupported release_final_closure_decision computed") + if final_closure_target not in ALLOWED_RELEASE_FINAL_CLOSURE_TARGETS: + raise ValueError("internal: unsupported release_final_closure_target computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_handoff_status={final_handoff_status} " + f"release_final_closure_status={final_closure_status} " + f"release_final_closure_decision={final_closure_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_handoff_report": str(source_path), + "source_release_final_terminal_publish_report": str( + release_final_handoff_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_handoff_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_handoff_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_handoff_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_handoff_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_handoff_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_handoff_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_handoff_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_handoff_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_handoff_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str(release_final_handoff_report["release_final_outcome_status"]), + "release_final_outcome_decision": str( + release_final_handoff_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_handoff_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_handoff_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_handoff_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_handoff_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": final_handoff_status, + "release_final_handoff_decision": final_handoff_decision, + "release_final_handoff_exit_code": final_handoff_exit_code, + "release_run_id": release_final_handoff_report["release_run_id"], + "release_run_url": str(release_final_handoff_report["release_run_url"]), + "follow_up_queue_url": str(release_final_handoff_report["follow_up_queue_url"]), + "final_closure_is_closed": final_closure_is_closed, + "final_closure_has_open_follow_up": final_closure_has_open_follow_up, + "final_closure_should_page_owner": final_closure_should_page_owner, + "final_closure_target": final_closure_target, + "release_final_closure_status": final_closure_status, + "release_final_closure_decision": final_closure_decision, + "release_final_closure_exit_code": final_closure_exit_code, + "release_final_closure_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Closure Report", + "", + f"- Release Final Closure Status: **{str(payload['release_final_closure_status']).upper()}**", + f"- Release Final Closure Decision: `{payload['release_final_closure_decision']}`", + f"- Release Final Closure Exit Code: `{payload['release_final_closure_exit_code']}`", + f"- Final Closure Is Closed: `{payload['final_closure_is_closed']}`", + f"- Final Closure Has Open Follow-Up: `{payload['final_closure_has_open_follow_up']}`", + f"- Final Closure Should Page Owner: `{payload['final_closure_should_page_owner']}`", + f"- Final Closure Target: `{payload['final_closure_target']}`", + f"- Release Final Handoff Status: `{payload['release_final_handoff_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_closure_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_closure_status": str(payload["release_final_closure_status"]), + "workflow_release_final_closure_decision": str(payload["release_final_closure_decision"]), + "workflow_release_final_closure_exit_code": str(payload["release_final_closure_exit_code"]), + "workflow_release_final_closure_is_closed": ( + "true" if payload["final_closure_is_closed"] else "false" + ), + "workflow_release_final_closure_has_open_follow_up": ( + "true" if payload["final_closure_has_open_follow_up"] else "false" + ), + "workflow_release_final_closure_should_page_owner": ( + "true" if payload["final_closure_should_page_owner"] else "false" + ), + "workflow_release_final_closure_target": str(payload["final_closure_target"]), + "workflow_release_final_closure_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_closure_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_closure_run_url": str(payload["release_run_url"]), + "workflow_release_final_closure_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_closure_report_json": str(output_json), + "workflow_release_final_closure_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final closure contract " + "from P2-50 release final handoff report" + ) + ) + parser.add_argument( + "--release-final-handoff-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json", + help="P2-50 release final handoff report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json", + help="Output release final closure JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md", + help="Output release final closure markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final closure JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_handoff_report_path = Path(args.release_final_handoff_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_handoff_report = load_release_final_handoff_report( + release_final_handoff_report_path + ) + payload = build_release_final_closure_payload( + release_final_handoff_report, + source_path=release_final_handoff_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-final-closure-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final closure json: {output_json_path}") + print(f"release final closure markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final closure summary: " + f"release_final_closure_status={payload['release_final_closure_status']} " + f"release_final_closure_decision={payload['release_final_closure_decision']} " + f"release_final_closure_exit_code={payload['release_final_closure_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_closure_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py new file mode 100644 index 0000000..a86f4b5 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py @@ -0,0 +1,924 @@ +"""Phase 2 card P2-52 gate for Linux CI workflow release final closure publish. + +This script consumes the P2-51 release final closure artifact and converges +one terminal closure publish contract: +1) validate release final closure + evidence consistency, +2) normalize closure-publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release_closure", + "announce_release_with_follow_up", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: set[str] = { + "completed", + "completed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: set[str] = { + "handoff_release_closure", + "handoff_release_with_follow_up", + "handoff_blocker", + "abort_handoff", +} +ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: set[str] = { + "closed", + "closed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: set[str] = { + "close_release", + "close_with_follow_up", + "close_blocker", + "abort_close", +} +ALLOWED_RELEASE_FINAL_CLOSURE_TARGETS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_DECISIONS: set[str] = { + "announce_release_closed", + "announce_release_closed_with_follow_up", + "announce_release_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_closure_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final closure report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final closure payload must be object") + + required_fields = ( + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_closure_is_closed", + "final_closure_has_open_follow_up", + "final_closure_should_page_owner", + "final_closure_target", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_final_verdict_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if release_delivery_final_verdict_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + release_delivery_final_verdict_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if ( + release_delivery_final_verdict_decision + not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + release_follow_up_final_verdict_status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if release_follow_up_final_verdict_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + release_follow_up_final_verdict_decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if ( + release_follow_up_final_verdict_decision + not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + release_final_outcome_status = _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ) + if release_final_outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError( + f"{path}: field 'release_final_outcome_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES)}" + ) + release_final_outcome_decision = _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ) + if release_final_outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_outcome_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS)}" + ) + + release_final_terminal_publish_status = _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ) + if release_final_terminal_publish_status not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES)}" + ) + release_final_terminal_publish_decision = _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ) + if ( + release_final_terminal_publish_decision + not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_final_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS)}" + ) + + release_final_handoff_status = _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ) + if release_final_handoff_status not in ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: + raise ValueError( + f"{path}: field 'release_final_handoff_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES)}" + ) + release_final_handoff_decision = _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ) + if release_final_handoff_decision not in ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_handoff_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS)}" + ) + + release_final_closure_status = _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ) + if release_final_closure_status not in ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES: + raise ValueError( + f"{path}: field 'release_final_closure_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_STATUSES)}" + ) + release_final_closure_decision = _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ) + if release_final_closure_decision not in ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_closure_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_DECISIONS)}" + ) + final_closure_target = _coerce_str( + payload["final_closure_target"], + field="final_closure_target", + path=path, + ) + if final_closure_target not in ALLOWED_RELEASE_FINAL_CLOSURE_TARGETS: + raise ValueError( + f"{path}: field 'final_closure_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_CLOSURE_TARGETS)}" + ) + + release_delivery_final_verdict_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + release_follow_up_final_verdict_exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + release_final_outcome_exit_code = _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ) + release_final_terminal_publish_exit_code = _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ) + release_final_handoff_exit_code = _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ) + release_final_closure_exit_code = _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ) + if release_delivery_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + if release_follow_up_final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + if release_final_outcome_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_outcome_exit_code' must be >= 0") + if release_final_terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_exit_code' must be >= 0" + ) + if release_final_handoff_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_handoff_exit_code' must be >= 0") + if release_final_closure_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_closure_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_final_outcome_status": release_final_outcome_status, + "release_final_outcome_decision": release_final_outcome_decision, + "release_final_outcome_exit_code": release_final_outcome_exit_code, + "release_final_terminal_publish_status": release_final_terminal_publish_status, + "release_final_terminal_publish_decision": release_final_terminal_publish_decision, + "release_final_terminal_publish_exit_code": release_final_terminal_publish_exit_code, + "release_final_handoff_status": release_final_handoff_status, + "release_final_handoff_decision": release_final_handoff_decision, + "release_final_handoff_exit_code": release_final_handoff_exit_code, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_closure_is_closed": _coerce_bool( + payload["final_closure_is_closed"], + field="final_closure_is_closed", + path=path, + ), + "final_closure_has_open_follow_up": _coerce_bool( + payload["final_closure_has_open_follow_up"], + field="final_closure_has_open_follow_up", + path=path, + ), + "final_closure_should_page_owner": _coerce_bool( + payload["final_closure_should_page_owner"], + field="final_closure_should_page_owner", + path=path, + ), + "final_closure_target": final_closure_target, + "release_final_closure_status": release_final_closure_status, + "release_final_closure_decision": release_final_closure_decision, + "release_final_closure_exit_code": release_final_closure_exit_code, + "release_final_closure_summary": _coerce_str( + payload["release_final_closure_summary"], + field="release_final_closure_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_closure_publish_payload( + release_final_closure_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_closure_status = str(release_final_closure_report["release_final_closure_status"]) + final_closure_decision = str(release_final_closure_report["release_final_closure_decision"]) + final_closure_exit_code = int(release_final_closure_report["release_final_closure_exit_code"]) + final_closure_is_closed = bool(release_final_closure_report["final_closure_is_closed"]) + final_closure_has_open_follow_up = bool( + release_final_closure_report["final_closure_has_open_follow_up"] + ) + final_closure_should_page_owner = bool( + release_final_closure_report["final_closure_should_page_owner"] + ) + final_closure_target = str(release_final_closure_report["final_closure_target"]) + + reason_codes = list(release_final_closure_report["reason_codes"]) + structural_issues = list(release_final_closure_report["structural_issues"]) + missing_artifacts = list(release_final_closure_report["missing_artifacts"]) + evidence_manifest = list(release_final_closure_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_closure_publish_evidence_missing") + + expected = { + "closed": ( + "close_release", + 0, + True, + False, + False, + "release", + ), + "closed_with_follow_up": ( + "close_with_follow_up", + None, + True, + True, + False, + "follow_up", + ), + "blocked": ( + "close_blocker", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_close", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_is_closed, + expected_follow_up_open, + expected_page_owner, + expected_target, + ) = expected[final_closure_status] + + if final_closure_decision != expected_decision: + structural_issues.append("final_closure_decision_mismatch") + if final_closure_is_closed != expected_is_closed: + structural_issues.append("final_closure_is_closed_mismatch") + if final_closure_has_open_follow_up != expected_follow_up_open: + structural_issues.append("final_closure_has_open_follow_up_mismatch") + if final_closure_should_page_owner != expected_page_owner: + structural_issues.append("final_closure_should_page_owner_mismatch") + if final_closure_target != expected_target: + structural_issues.append("final_closure_target_mismatch") + + if expected_exit_code == 0 and final_closure_exit_code != 0: + structural_issues.append("final_closure_exit_code_mismatch_closed") + if expected_exit_code is None and final_closure_exit_code == 0: + structural_issues.append("final_closure_exit_code_mismatch_non_closed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, final_closure_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_closure_status == "closed": + publish_status = "published" + publish_decision = "announce_release_closed" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["release_final_closure_publish_published"] + elif final_closure_status == "closed_with_follow_up": + publish_status = "published_with_follow_up" + publish_decision = "announce_release_closed_with_follow_up" + publish_exit_code = max(1, final_closure_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("release_final_closure_publish_with_follow_up") + elif final_closure_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_release_blocker" + publish_exit_code = max(1, final_closure_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("release_final_closure_publish_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, final_closure_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("release_final_closure_publish_upstream_contract_failed") + + if publish_status not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_STATUSES: + raise ValueError("internal: unsupported release_final_closure_publish_status computed") + if publish_decision not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported release_final_closure_publish_decision computed") + if publish_channel not in ALLOWED_RELEASE_FINAL_CLOSURE_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported release_final_closure_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_closure_status={final_closure_status} " + f"release_final_closure_publish_status={publish_status} " + f"release_final_closure_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_closure_report": str(source_path), + "source_release_final_handoff_report": str( + release_final_closure_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_closure_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_closure_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_closure_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_closure_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_closure_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_closure_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_closure_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_closure_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_closure_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_closure_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str(release_final_closure_report["release_final_outcome_status"]), + "release_final_outcome_decision": str( + release_final_closure_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_closure_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_closure_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_closure_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_closure_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str(release_final_closure_report["release_final_handoff_status"]), + "release_final_handoff_decision": str( + release_final_closure_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_closure_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": final_closure_status, + "release_final_closure_decision": final_closure_decision, + "release_final_closure_exit_code": final_closure_exit_code, + "release_run_id": release_final_closure_report["release_run_id"], + "release_run_url": str(release_final_closure_report["release_run_url"]), + "follow_up_queue_url": str(release_final_closure_report["follow_up_queue_url"]), + "final_closure_publish_should_notify": publish_should_notify, + "final_closure_publish_requires_manual_action": publish_requires_manual_action, + "final_closure_publish_channel": publish_channel, + "release_final_closure_publish_status": publish_status, + "release_final_closure_publish_decision": publish_decision, + "release_final_closure_publish_exit_code": publish_exit_code, + "release_final_closure_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Closure Publish Report", + "", + ( + "- Release Final Closure Publish Status: " + f"**{str(payload['release_final_closure_publish_status']).upper()}**" + ), + f"- Release Final Closure Publish Decision: `{payload['release_final_closure_publish_decision']}`", + f"- Release Final Closure Publish Exit Code: `{payload['release_final_closure_publish_exit_code']}`", + ( + f"- Final Closure Publish Should Notify: " + f"`{payload['final_closure_publish_should_notify']}`" + ), + ( + f"- Final Closure Publish Requires Manual Action: " + f"`{payload['final_closure_publish_requires_manual_action']}`" + ), + f"- Final Closure Publish Channel: `{payload['final_closure_publish_channel']}`", + f"- Release Final Closure Status: `{payload['release_final_closure_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_closure_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_closure_publish_status": str( + payload["release_final_closure_publish_status"] + ), + "workflow_release_final_closure_publish_decision": str( + payload["release_final_closure_publish_decision"] + ), + "workflow_release_final_closure_publish_exit_code": str( + payload["release_final_closure_publish_exit_code"] + ), + "workflow_release_final_closure_publish_should_notify": ( + "true" if payload["final_closure_publish_should_notify"] else "false" + ), + "workflow_release_final_closure_publish_requires_manual_action": ( + "true" if payload["final_closure_publish_requires_manual_action"] else "false" + ), + "workflow_release_final_closure_publish_channel": str( + payload["final_closure_publish_channel"] + ), + "workflow_release_final_closure_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_release_final_closure_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_closure_publish_run_url": str(payload["release_run_url"]), + "workflow_release_final_closure_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_closure_publish_report_json": str(output_json), + "workflow_release_final_closure_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final closure publish contract " + "from P2-51 release final closure report" + ) + ) + parser.add_argument( + "--release-final-closure-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json", + help="P2-51 release final closure report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json", + help="Output release final closure publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md", + help="Output release final closure publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final closure publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_closure_report_path = Path(args.release_final_closure_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_closure_report = load_release_final_closure_report( + release_final_closure_report_path + ) + payload = build_release_final_closure_publish_payload( + release_final_closure_report, + source_path=release_final_closure_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-release-final-closure-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final closure publish json: {output_json_path}") + print(f"release final closure publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final closure publish summary: " + f"release_final_closure_publish_status={payload['release_final_closure_publish_status']} " + f"release_final_closure_publish_decision={payload['release_final_closure_publish_decision']} " + f"release_final_closure_publish_exit_code={payload['release_final_closure_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_closure_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py new file mode 100644 index 0000000..21f39fe --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py @@ -0,0 +1,864 @@ +"""Phase 2 card P2-50 gate for Linux CI workflow release final handoff. + +This script consumes the P2-49 release final terminal publish artifact and +converges one terminal handoff contract: +1) validate release final terminal publish + evidence consistency, +2) normalize release handoff semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release_closure", + "announce_release_with_follow_up", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} +ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: set[str] = { + "completed", + "completed_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: set[str] = { + "handoff_release_closure", + "handoff_release_with_follow_up", + "handoff_blocker", + "abort_handoff", +} +ALLOWED_RELEASE_FINAL_HANDOFF_TARGETS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final terminal publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final terminal publish payload must be object") + + required_fields = ( + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_should_ship_release", + "final_follow_up_open", + "final_should_page_owner", + "final_outcome_target", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "final_terminal_should_notify", + "final_terminal_requires_manual_action", + "final_terminal_channel", + "release_final_terminal_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_final_verdict_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if release_delivery_final_verdict_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + release_delivery_final_verdict_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if ( + release_delivery_final_verdict_decision + not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + release_follow_up_final_verdict_status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if release_follow_up_final_verdict_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + release_follow_up_final_verdict_decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if ( + release_follow_up_final_verdict_decision + not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + release_final_outcome_status = _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ) + if release_final_outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError( + f"{path}: field 'release_final_outcome_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES)}" + ) + release_final_outcome_decision = _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ) + if release_final_outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_outcome_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS)}" + ) + final_outcome_target = _coerce_str( + payload["final_outcome_target"], + field="final_outcome_target", + path=path, + ) + if final_outcome_target not in ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: + raise ValueError( + f"{path}: field 'final_outcome_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS)}" + ) + + release_final_terminal_publish_status = _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ) + if release_final_terminal_publish_status not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES)}" + ) + + release_final_terminal_publish_decision = _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ) + if ( + release_final_terminal_publish_decision + not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_final_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS)}" + ) + final_terminal_channel = _coerce_str( + payload["final_terminal_channel"], + field="final_terminal_channel", + path=path, + ) + if final_terminal_channel not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'final_terminal_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS)}" + ) + + release_delivery_final_verdict_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + release_follow_up_final_verdict_exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + release_final_outcome_exit_code = _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ) + release_final_terminal_publish_exit_code = _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ) + if release_delivery_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + if release_follow_up_final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + if release_final_outcome_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_outcome_exit_code' must be >= 0") + if release_final_terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_final_terminal_publish_exit_code' must be >= 0" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_final_outcome_status": release_final_outcome_status, + "release_final_outcome_decision": release_final_outcome_decision, + "release_final_outcome_exit_code": release_final_outcome_exit_code, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_should_ship_release": _coerce_bool( + payload["final_should_ship_release"], + field="final_should_ship_release", + path=path, + ), + "final_follow_up_open": _coerce_bool( + payload["final_follow_up_open"], + field="final_follow_up_open", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_outcome_target": final_outcome_target, + "release_final_terminal_publish_status": release_final_terminal_publish_status, + "release_final_terminal_publish_decision": release_final_terminal_publish_decision, + "release_final_terminal_publish_exit_code": release_final_terminal_publish_exit_code, + "final_terminal_should_notify": _coerce_bool( + payload["final_terminal_should_notify"], + field="final_terminal_should_notify", + path=path, + ), + "final_terminal_requires_manual_action": _coerce_bool( + payload["final_terminal_requires_manual_action"], + field="final_terminal_requires_manual_action", + path=path, + ), + "final_terminal_channel": final_terminal_channel, + "release_final_terminal_publish_summary": _coerce_str( + payload["release_final_terminal_publish_summary"], + field="release_final_terminal_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_handoff_payload( + release_final_terminal_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_terminal_status = str( + release_final_terminal_publish_report["release_final_terminal_publish_status"] + ) + final_terminal_decision = str( + release_final_terminal_publish_report["release_final_terminal_publish_decision"] + ) + final_terminal_exit_code = int( + release_final_terminal_publish_report["release_final_terminal_publish_exit_code"] + ) + final_terminal_should_notify = bool( + release_final_terminal_publish_report["final_terminal_should_notify"] + ) + final_terminal_requires_manual_action = bool( + release_final_terminal_publish_report["final_terminal_requires_manual_action"] + ) + final_terminal_channel = str(release_final_terminal_publish_report["final_terminal_channel"]) + + final_should_ship_release = bool( + release_final_terminal_publish_report["final_should_ship_release"] + ) + final_follow_up_open = bool(release_final_terminal_publish_report["final_follow_up_open"]) + final_should_page_owner = bool( + release_final_terminal_publish_report["final_should_page_owner"] + ) + final_outcome_target = str(release_final_terminal_publish_report["final_outcome_target"]) + + reason_codes = list(release_final_terminal_publish_report["reason_codes"]) + structural_issues = list(release_final_terminal_publish_report["structural_issues"]) + missing_artifacts = list(release_final_terminal_publish_report["missing_artifacts"]) + evidence_manifest = list(release_final_terminal_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_handoff_evidence_missing") + + expected = { + "published": ( + "announce_release_closure", + 0, + True, + False, + "release", + True, + False, + False, + "release", + ), + "published_with_follow_up": ( + "announce_release_with_follow_up", + None, + True, + False, + "follow_up", + True, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_blocker", + None, + True, + True, + "blocker", + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_notify, + expected_manual, + expected_channel, + expected_ship, + expected_follow_up, + expected_page, + expected_target, + ) = expected[final_terminal_status] + + if final_terminal_decision != expected_decision: + structural_issues.append("final_terminal_decision_mismatch") + if final_terminal_should_notify != expected_notify: + structural_issues.append("final_terminal_should_notify_mismatch") + if final_terminal_requires_manual_action != expected_manual: + structural_issues.append("final_terminal_requires_manual_action_mismatch") + if final_terminal_channel != expected_channel: + structural_issues.append("final_terminal_channel_mismatch") + if final_should_ship_release != expected_ship: + structural_issues.append("final_should_ship_release_mismatch") + if final_follow_up_open != expected_follow_up: + structural_issues.append("final_follow_up_open_mismatch") + if final_should_page_owner != expected_page: + structural_issues.append("final_should_page_owner_mismatch") + if final_outcome_target != expected_target: + structural_issues.append("final_outcome_target_mismatch") + + if expected_exit_code == 0 and final_terminal_exit_code != 0: + structural_issues.append("final_terminal_exit_code_mismatch_published") + if expected_exit_code is None and final_terminal_exit_code == 0: + structural_issues.append("final_terminal_exit_code_mismatch_non_published") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + handoff_status = "contract_failed" + handoff_decision = "abort_handoff" + handoff_exit_code = max(1, final_terminal_exit_code) + final_handoff_should_archive_release = False + final_handoff_keep_follow_up_open = True + final_handoff_should_page_owner = True + final_handoff_target = "blocker" + reason_codes.extend(structural_issues) + elif final_terminal_status == "published": + handoff_status = "completed" + handoff_decision = "handoff_release_closure" + handoff_exit_code = 0 + final_handoff_should_archive_release = True + final_handoff_keep_follow_up_open = False + final_handoff_should_page_owner = False + final_handoff_target = "release" + reason_codes = ["release_final_handoff_completed"] + elif final_terminal_status == "published_with_follow_up": + handoff_status = "completed_with_follow_up" + handoff_decision = "handoff_release_with_follow_up" + handoff_exit_code = max(1, final_terminal_exit_code) + final_handoff_should_archive_release = True + final_handoff_keep_follow_up_open = True + final_handoff_should_page_owner = False + final_handoff_target = "follow_up" + reason_codes.append("release_final_handoff_completed_with_follow_up") + elif final_terminal_status == "blocked": + handoff_status = "blocked" + handoff_decision = "handoff_blocker" + handoff_exit_code = max(1, final_terminal_exit_code) + final_handoff_should_archive_release = False + final_handoff_keep_follow_up_open = True + final_handoff_should_page_owner = True + final_handoff_target = "blocker" + reason_codes.append("release_final_handoff_blocked") + else: + handoff_status = "contract_failed" + handoff_decision = "abort_handoff" + handoff_exit_code = max(1, final_terminal_exit_code) + final_handoff_should_archive_release = False + final_handoff_keep_follow_up_open = True + final_handoff_should_page_owner = True + final_handoff_target = "blocker" + reason_codes.append("release_final_handoff_upstream_contract_failed") + + if handoff_status not in ALLOWED_RELEASE_FINAL_HANDOFF_STATUSES: + raise ValueError("internal: unsupported release_final_handoff_status computed") + if handoff_decision not in ALLOWED_RELEASE_FINAL_HANDOFF_DECISIONS: + raise ValueError("internal: unsupported release_final_handoff_decision computed") + if final_handoff_target not in ALLOWED_RELEASE_FINAL_HANDOFF_TARGETS: + raise ValueError("internal: unsupported release_final_handoff_target computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_terminal_publish_status={final_terminal_status} " + f"release_final_handoff_status={handoff_status} " + f"release_final_handoff_decision={handoff_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_terminal_publish_report": str(source_path), + "source_release_final_outcome_report": str( + release_final_terminal_publish_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_terminal_publish_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_terminal_publish_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_terminal_publish_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_terminal_publish_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_terminal_publish_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_terminal_publish_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_terminal_publish_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_terminal_publish_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_terminal_publish_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_terminal_publish_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_terminal_publish_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": final_terminal_status, + "release_final_terminal_publish_decision": final_terminal_decision, + "release_final_terminal_publish_exit_code": final_terminal_exit_code, + "release_run_id": release_final_terminal_publish_report["release_run_id"], + "release_run_url": str(release_final_terminal_publish_report["release_run_url"]), + "follow_up_queue_url": str(release_final_terminal_publish_report["follow_up_queue_url"]), + "final_handoff_should_archive_release": final_handoff_should_archive_release, + "final_handoff_keep_follow_up_open": final_handoff_keep_follow_up_open, + "final_handoff_should_page_owner": final_handoff_should_page_owner, + "final_handoff_target": final_handoff_target, + "release_final_handoff_status": handoff_status, + "release_final_handoff_decision": handoff_decision, + "release_final_handoff_exit_code": handoff_exit_code, + "release_final_handoff_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Handoff Report", + "", + f"- Release Final Handoff Status: **{str(payload['release_final_handoff_status']).upper()}**", + f"- Release Final Handoff Decision: `{payload['release_final_handoff_decision']}`", + f"- Release Final Handoff Exit Code: `{payload['release_final_handoff_exit_code']}`", + ( + f"- Final Handoff Should Archive Release: " + f"`{payload['final_handoff_should_archive_release']}`" + ), + ( + f"- Final Handoff Keep Follow-Up Open: " + f"`{payload['final_handoff_keep_follow_up_open']}`" + ), + ( + f"- Final Handoff Should Page Owner: " + f"`{payload['final_handoff_should_page_owner']}`" + ), + f"- Final Handoff Target: `{payload['final_handoff_target']}`", + ( + f"- Release Final Terminal Publish Status: " + f"`{payload['release_final_terminal_publish_status']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_handoff_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_handoff_status": str(payload["release_final_handoff_status"]), + "workflow_release_final_handoff_decision": str(payload["release_final_handoff_decision"]), + "workflow_release_final_handoff_exit_code": str(payload["release_final_handoff_exit_code"]), + "workflow_release_final_handoff_should_archive_release": ( + "true" if payload["final_handoff_should_archive_release"] else "false" + ), + "workflow_release_final_handoff_keep_follow_up_open": ( + "true" if payload["final_handoff_keep_follow_up_open"] else "false" + ), + "workflow_release_final_handoff_should_page_owner": ( + "true" if payload["final_handoff_should_page_owner"] else "false" + ), + "workflow_release_final_handoff_target": str(payload["final_handoff_target"]), + "workflow_release_final_handoff_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_handoff_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_handoff_run_url": str(payload["release_run_url"]), + "workflow_release_final_handoff_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_handoff_report_json": str(output_json), + "workflow_release_final_handoff_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final handoff contract " + "from P2-49 release final terminal publish report" + ) + ) + parser.add_argument( + "--release-final-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json", + help="P2-49 release final terminal publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json", + help="Output release final handoff JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md", + help="Output release final handoff markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final handoff JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + final_terminal_publish_report_path = Path(args.release_final_terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + final_terminal_publish_report = load_release_final_terminal_publish_report( + final_terminal_publish_report_path + ) + payload = build_release_final_handoff_payload( + final_terminal_publish_report, + source_path=final_terminal_publish_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-final-handoff-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final handoff json: {output_json_path}") + print(f"release final handoff markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final handoff summary: " + f"release_final_handoff_status={payload['release_final_handoff_status']} " + f"release_final_handoff_decision={payload['release_final_handoff_decision']} " + f"release_final_handoff_exit_code={payload['release_final_handoff_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_handoff_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py new file mode 100644 index 0000000..db641e3 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py @@ -0,0 +1,803 @@ +"""Phase 2 card P2-48 gate for Linux CI workflow release final outcome. + +This script consumes the P2-43 release delivery final verdict artifact and +P2-47 release follow-up final verdict artifact, then converges one final +release outcome contract: +1) validate delivery/follow-up verdict consistency, +2) normalize terminal release outcome semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: set[str] = { + "release", + "hold", + "blocker", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_ANNOUNCEMENT_TARGETS: set[str] = { + "release", + "follow_up", + "blocker", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing path/exists") + + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append({"path": entry_path, "exists": entry_exists}) + return evidence_manifest + + +def load_release_delivery_final_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release delivery final verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release delivery final verdict payload must be object") + + required_fields = ( + "source_release_delivery_terminal_publish_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "final_should_close_release", + "final_should_open_follow_up", + "final_should_page_owner", + "final_announcement_target", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + + decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if decision not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + if exit_code < 0: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0" + ) + + announcement_target = _coerce_str( + payload["final_announcement_target"], + field="final_announcement_target", + path=path, + ) + if announcement_target not in ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: + raise ValueError( + f"{path}: field 'final_announcement_target' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_delivery_terminal_publish_report": _coerce_str( + payload["source_release_delivery_terminal_publish_report"], + field="source_release_delivery_terminal_publish_report", + path=path, + ), + "release_delivery_final_verdict_status": status, + "release_delivery_final_verdict_decision": decision, + "release_delivery_final_verdict_exit_code": exit_code, + "final_should_close_release": _coerce_bool( + payload["final_should_close_release"], + field="final_should_close_release", + path=path, + ), + "final_should_open_follow_up": _coerce_bool( + payload["final_should_open_follow_up"], + field="final_should_open_follow_up", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_announcement_target": announcement_target, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def load_release_follow_up_final_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release follow-up final verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release follow-up final verdict payload must be object") + + required_fields = ( + "source_release_follow_up_terminal_publish_report", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "final_should_close_follow_up", + "final_should_open_follow_up", + "final_should_page_owner", + "final_announcement_target", + "follow_up_queue_url", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + + decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if decision not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + if exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + + announcement_target = _coerce_str( + payload["final_announcement_target"], + field="final_announcement_target", + path=path, + ) + if announcement_target not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_ANNOUNCEMENT_TARGETS: + raise ValueError( + f"{path}: field 'final_announcement_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_ANNOUNCEMENT_TARGETS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_follow_up_terminal_publish_report": _coerce_str( + payload["source_release_follow_up_terminal_publish_report"], + field="source_release_follow_up_terminal_publish_report", + path=path, + ), + "release_follow_up_final_verdict_status": status, + "release_follow_up_final_verdict_decision": decision, + "release_follow_up_final_verdict_exit_code": exit_code, + "final_should_close_follow_up": _coerce_bool( + payload["final_should_close_follow_up"], + field="final_should_close_follow_up", + path=path, + ), + "final_should_open_follow_up": _coerce_bool( + payload["final_should_open_follow_up"], + field="final_should_open_follow_up", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_announcement_target": announcement_target, + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_outcome_payload( + delivery_final_verdict_report: dict[str, Any], + follow_up_final_verdict_report: dict[str, Any], + *, + delivery_source_path: Path, + follow_up_source_path: Path, +) -> dict[str, Any]: + delivery_status = str(delivery_final_verdict_report["release_delivery_final_verdict_status"]) + delivery_decision = str(delivery_final_verdict_report["release_delivery_final_verdict_decision"]) + delivery_exit_code = int(delivery_final_verdict_report["release_delivery_final_verdict_exit_code"]) + delivery_close_release = bool(delivery_final_verdict_report["final_should_close_release"]) + delivery_open_follow_up = bool(delivery_final_verdict_report["final_should_open_follow_up"]) + delivery_page_owner = bool(delivery_final_verdict_report["final_should_page_owner"]) + delivery_target = str(delivery_final_verdict_report["final_announcement_target"]) + delivery_run_id = delivery_final_verdict_report["release_run_id"] + delivery_run_url = str(delivery_final_verdict_report["release_run_url"]) + + follow_status = str(follow_up_final_verdict_report["release_follow_up_final_verdict_status"]) + follow_decision = str(follow_up_final_verdict_report["release_follow_up_final_verdict_decision"]) + follow_exit_code = int(follow_up_final_verdict_report["release_follow_up_final_verdict_exit_code"]) + follow_close_follow_up = bool(follow_up_final_verdict_report["final_should_close_follow_up"]) + follow_open_follow_up = bool(follow_up_final_verdict_report["final_should_open_follow_up"]) + follow_page_owner = bool(follow_up_final_verdict_report["final_should_page_owner"]) + follow_target = str(follow_up_final_verdict_report["final_announcement_target"]) + follow_queue_url = str(follow_up_final_verdict_report["follow_up_queue_url"]) + follow_run_id = follow_up_final_verdict_report["release_run_id"] + follow_run_url = str(follow_up_final_verdict_report["release_run_url"]) + + reason_codes = list(delivery_final_verdict_report["reason_codes"]) + list( + follow_up_final_verdict_report["reason_codes"] + ) + structural_issues = list(delivery_final_verdict_report["structural_issues"]) + list( + follow_up_final_verdict_report["structural_issues"] + ) + missing_artifacts = list(delivery_final_verdict_report["missing_artifacts"]) + list( + follow_up_final_verdict_report["missing_artifacts"] + ) + evidence_manifest = [ + {"source": "delivery", "path": str(item["path"]), "exists": bool(item["exists"])} + for item in delivery_final_verdict_report["evidence_manifest"] + ] + [ + {"source": "follow_up", "path": str(item["path"]), "exists": bool(item["exists"])} + for item in follow_up_final_verdict_report["evidence_manifest"] + ] + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_outcome_evidence_missing") + + expected_delivery = { + "completed": ("close_release", True, False, False), + "requires_follow_up": ("open_follow_up", False, True, False), + "blocked": ("escalate_blocker", False, True, True), + "contract_failed": ("abort_close", False, True, True), + } + expected_follow_up = { + "completed": ("close_follow_up", True, False, False), + "requires_follow_up": ("keep_follow_up_open", False, True, False), + "blocked": ("escalate_queue_failure", False, True, True), + "contract_failed": ("abort_close", False, True, True), + } + + expected_delivery_decision, expected_delivery_close, expected_delivery_open, expected_delivery_page = expected_delivery[ + delivery_status + ] + if delivery_decision != expected_delivery_decision: + structural_issues.append("delivery_decision_mismatch") + if delivery_close_release != expected_delivery_close: + structural_issues.append("delivery_close_release_mismatch") + if delivery_open_follow_up != expected_delivery_open: + structural_issues.append("delivery_open_follow_up_mismatch") + if delivery_page_owner != expected_delivery_page: + structural_issues.append("delivery_page_owner_mismatch") + if delivery_status == "completed": + if delivery_exit_code != 0: + structural_issues.append("delivery_exit_code_mismatch_completed") + if delivery_target != "release": + structural_issues.append("delivery_target_mismatch_completed") + else: + if delivery_exit_code == 0: + structural_issues.append("delivery_exit_code_mismatch_non_completed") + if delivery_target == "release": + structural_issues.append("delivery_target_mismatch_non_completed") + + expected_follow_decision, expected_follow_close, expected_follow_open, expected_follow_page = expected_follow_up[ + follow_status + ] + if follow_decision != expected_follow_decision: + structural_issues.append("follow_up_decision_mismatch") + if follow_close_follow_up != expected_follow_close: + structural_issues.append("follow_up_close_flag_mismatch") + if follow_open_follow_up != expected_follow_open: + structural_issues.append("follow_up_open_flag_mismatch") + if follow_page_owner != expected_follow_page: + structural_issues.append("follow_up_page_owner_mismatch") + if follow_status == "completed": + if follow_exit_code != 0: + structural_issues.append("follow_up_exit_code_mismatch_completed") + if follow_target not in {"release", "follow_up"}: + structural_issues.append("follow_up_target_mismatch_completed") + else: + if follow_exit_code == 0: + structural_issues.append("follow_up_exit_code_mismatch_non_completed") + if follow_target == "release": + structural_issues.append("follow_up_target_mismatch_non_completed") + + if delivery_run_id is not None and follow_run_id is not None and delivery_run_id != follow_run_id: + structural_issues.append("release_run_id_mismatch") + if delivery_run_url and follow_run_url and delivery_run_url != follow_run_url: + structural_issues.append("release_run_url_mismatch") + + if delivery_status == "blocked" and follow_status == "completed": + structural_issues.append("follow_up_completed_while_delivery_blocked") + if delivery_status == "contract_failed" and follow_status != "contract_failed": + structural_issues.append("follow_up_status_mismatch_delivery_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + outcome_status = "contract_failed" + outcome_decision = "abort_outcome" + outcome_exit_code = max(1, delivery_exit_code, follow_exit_code) + final_should_ship_release = False + final_follow_up_open = True + final_should_page_owner = True + final_outcome_target = "blocker" + reason_codes.extend(structural_issues) + elif delivery_status == "contract_failed" or follow_status == "contract_failed": + outcome_status = "contract_failed" + outcome_decision = "abort_outcome" + outcome_exit_code = max(1, delivery_exit_code, follow_exit_code) + final_should_ship_release = False + final_follow_up_open = True + final_should_page_owner = True + final_outcome_target = "blocker" + reason_codes.append("release_final_outcome_upstream_contract_failed") + elif delivery_status == "blocked" or follow_status == "blocked": + outcome_status = "blocked" + outcome_decision = "escalate_blocker" + outcome_exit_code = max(1, delivery_exit_code, follow_exit_code) + final_should_ship_release = False + final_follow_up_open = True + final_should_page_owner = True + final_outcome_target = "blocker" + reason_codes.append("release_final_outcome_blocked") + elif follow_status == "completed" and delivery_status in {"completed", "requires_follow_up"}: + outcome_status = "released" + outcome_decision = "ship_and_close" + outcome_exit_code = 0 + final_should_ship_release = True + final_follow_up_open = False + final_should_page_owner = False + final_outcome_target = "release" + reason_codes.append("release_final_outcome_released") + elif follow_status == "requires_follow_up" or delivery_status == "requires_follow_up": + outcome_status = "released_with_follow_up" + outcome_decision = "ship_with_follow_up_open" + outcome_exit_code = max(1, delivery_exit_code, follow_exit_code) + final_should_ship_release = True + final_follow_up_open = True + final_should_page_owner = False + final_outcome_target = "follow_up" + reason_codes.append("release_final_outcome_released_with_follow_up") + elif delivery_status == "completed" and follow_status == "completed": + outcome_status = "released" + outcome_decision = "ship_and_close" + outcome_exit_code = 0 + final_should_ship_release = True + final_follow_up_open = False + final_should_page_owner = False + final_outcome_target = "release" + reason_codes.append("release_final_outcome_released") + else: + outcome_status = "contract_failed" + outcome_decision = "abort_outcome" + outcome_exit_code = max(1, delivery_exit_code, follow_exit_code) + final_should_ship_release = False + final_follow_up_open = True + final_should_page_owner = True + final_outcome_target = "blocker" + reason_codes.append("release_final_outcome_status_combination_unknown") + + if outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError("internal: unsupported release_final_outcome_status computed") + if outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError("internal: unsupported release_final_outcome_decision computed") + if final_outcome_target not in ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: + raise ValueError("internal: unsupported release_final_outcome_target computed") + + reason_codes = _unique(reason_codes) + release_run_id = follow_run_id if follow_run_id is not None else delivery_run_id + release_run_url = follow_run_url if follow_run_url else delivery_run_url + summary = ( + f"release_delivery_final_verdict_status={delivery_status} " + f"release_follow_up_final_verdict_status={follow_status} " + f"release_final_outcome_status={outcome_status} " + f"release_final_outcome_decision={outcome_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_delivery_final_verdict_report": str(delivery_source_path), + "source_release_follow_up_final_verdict_report": str(follow_up_source_path), + "release_delivery_final_verdict_status": delivery_status, + "release_delivery_final_verdict_decision": delivery_decision, + "release_delivery_final_verdict_exit_code": delivery_exit_code, + "release_follow_up_final_verdict_status": follow_status, + "release_follow_up_final_verdict_decision": follow_decision, + "release_follow_up_final_verdict_exit_code": follow_exit_code, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "follow_up_queue_url": follow_queue_url, + "release_final_outcome_status": outcome_status, + "release_final_outcome_decision": outcome_decision, + "release_final_outcome_exit_code": outcome_exit_code, + "final_should_ship_release": final_should_ship_release, + "final_follow_up_open": final_follow_up_open, + "final_should_page_owner": final_should_page_owner, + "final_outcome_target": final_outcome_target, + "release_final_outcome_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Outcome Report", + "", + f"- Release Final Outcome Status: **{str(payload['release_final_outcome_status']).upper()}**", + f"- Release Final Outcome Decision: `{payload['release_final_outcome_decision']}`", + f"- Release Final Outcome Exit Code: `{payload['release_final_outcome_exit_code']}`", + f"- Final Should Ship Release: `{payload['final_should_ship_release']}`", + f"- Final Follow-Up Open: `{payload['final_follow_up_open']}`", + f"- Final Should Page Owner: `{payload['final_should_page_owner']}`", + f"- Final Outcome Target: `{payload['final_outcome_target']}`", + f"- Delivery Final Verdict Status: `{payload['release_delivery_final_verdict_status']}`", + f"- Follow-Up Final Verdict Status: `{payload['release_follow_up_final_verdict_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_outcome_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_outcome_status": str(payload["release_final_outcome_status"]), + "workflow_release_final_outcome_decision": str(payload["release_final_outcome_decision"]), + "workflow_release_final_outcome_exit_code": str(payload["release_final_outcome_exit_code"]), + "workflow_release_final_should_ship_release": ( + "true" if payload["final_should_ship_release"] else "false" + ), + "workflow_release_final_follow_up_open": ( + "true" if payload["final_follow_up_open"] else "false" + ), + "workflow_release_final_should_page_owner": ( + "true" if payload["final_should_page_owner"] else "false" + ), + "workflow_release_final_target": str(payload["final_outcome_target"]), + "workflow_release_final_delivery_status": str( + payload["release_delivery_final_verdict_status"] + ), + "workflow_release_final_follow_up_status": str( + payload["release_follow_up_final_verdict_status"] + ), + "workflow_release_final_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_final_run_url": str(payload["release_run_url"]), + "workflow_release_final_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_report_json": str(output_json), + "workflow_release_final_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final outcome contract " + "from P2-43 and P2-47 final verdict reports" + ) + ) + parser.add_argument( + "--release-delivery-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json", + help="P2-43 release delivery final verdict report JSON path", + ) + parser.add_argument( + "--release-follow-up-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json", + help="P2-47 release follow-up final verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json", + help="Output release final outcome JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md", + help="Output release final outcome markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final outcome JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + delivery_report_path = Path(args.release_delivery_final_verdict_report) + follow_up_report_path = Path(args.release_follow_up_final_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + delivery_report = load_release_delivery_final_verdict_report(delivery_report_path) + follow_up_report = load_release_follow_up_final_verdict_report(follow_up_report_path) + payload = build_release_final_outcome_payload( + delivery_report, + follow_up_report, + delivery_source_path=delivery_report_path.resolve(), + follow_up_source_path=follow_up_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-final-outcome-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final outcome json: {output_json_path}") + print(f"release final outcome markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final outcome summary: " + f"release_final_outcome_status={payload['release_final_outcome_status']} " + f"release_final_outcome_decision={payload['release_final_outcome_decision']} " + f"release_final_outcome_exit_code={payload['release_final_outcome_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_outcome_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py new file mode 100644 index 0000000..e475714 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py @@ -0,0 +1,943 @@ +"""Phase 2 card P2-56 gate for Linux CI workflow release final verdict publish. + +This script consumes the P2-55 release final verdict publish artifact and converges +one terminal final-publish-archive contract: +1) validate release final verdict publish + evidence consistency, +2) normalize final publish archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_release_shipped", + "announce_release_shipped_with_follow_up", + "announce_release_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_release_shipped", + "archive_release_shipped_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_release_final_verdict_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final verdict publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final verdict publish payload must be object") + + required_fields = ( + "source_release_final_verdict_report", + "source_release_final_archive_report", + "source_release_final_closure_publish_report", + "source_release_final_closure_report", + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_publish_status", + "release_final_closure_publish_decision", + "release_final_closure_publish_exit_code", + "release_final_archive_status", + "release_final_archive_decision", + "release_final_archive_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_verdict_publish_should_notify", + "final_verdict_publish_requires_manual_action", + "final_verdict_publish_channel", + "release_final_verdict_status", + "release_final_verdict_decision", + "release_final_verdict_exit_code", + "release_final_verdict_publish_status", + "release_final_verdict_publish_decision", + "release_final_verdict_publish_exit_code", + "release_final_verdict_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_final_verdict_publish_status = _coerce_str( + payload["release_final_verdict_publish_status"], + field="release_final_verdict_publish_status", + path=path, + ) + if release_final_verdict_publish_status not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_final_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_STATUSES)}" + ) + release_final_verdict_publish_decision = _coerce_str( + payload["release_final_verdict_publish_decision"], + field="release_final_verdict_publish_decision", + path=path, + ) + if release_final_verdict_publish_decision not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_DECISIONS)}" + ) + release_final_verdict_publish_exit_code = _coerce_int( + payload["release_final_verdict_publish_exit_code"], + field="release_final_verdict_publish_exit_code", + path=path, + ) + if release_final_verdict_publish_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_verdict_publish_exit_code' must be >= 0") + final_verdict_publish_channel = _coerce_str( + payload["final_verdict_publish_channel"], + field="final_verdict_publish_channel", + path=path, + ) + if final_verdict_publish_channel not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'final_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_verdict_report": _coerce_str( + payload["source_release_final_verdict_report"], + field="source_release_final_verdict_report", + path=path, + ), + "source_release_final_archive_report": _coerce_str( + payload["source_release_final_archive_report"], + field="source_release_final_archive_report", + path=path, + ), + "source_release_final_closure_publish_report": _coerce_str( + payload["source_release_final_closure_publish_report"], + field="source_release_final_closure_publish_report", + path=path, + ), + "source_release_final_closure_report": _coerce_str( + payload["source_release_final_closure_report"], + field="source_release_final_closure_report", + path=path, + ), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ), + "release_delivery_final_verdict_decision": _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ), + "release_delivery_final_verdict_exit_code": _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ), + "release_follow_up_final_verdict_status": _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ), + "release_follow_up_final_verdict_decision": _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ), + "release_follow_up_final_verdict_exit_code": _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ), + "release_final_outcome_status": _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ), + "release_final_outcome_decision": _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ), + "release_final_outcome_exit_code": _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ), + "release_final_terminal_publish_status": _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ), + "release_final_terminal_publish_decision": _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ), + "release_final_terminal_publish_exit_code": _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ), + "release_final_handoff_status": _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ), + "release_final_handoff_decision": _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ), + "release_final_handoff_exit_code": _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ), + "release_final_closure_status": _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ), + "release_final_closure_decision": _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ), + "release_final_closure_exit_code": _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ), + "release_final_closure_publish_status": _coerce_str( + payload["release_final_closure_publish_status"], + field="release_final_closure_publish_status", + path=path, + ), + "release_final_closure_publish_decision": _coerce_str( + payload["release_final_closure_publish_decision"], + field="release_final_closure_publish_decision", + path=path, + ), + "release_final_closure_publish_exit_code": _coerce_int( + payload["release_final_closure_publish_exit_code"], + field="release_final_closure_publish_exit_code", + path=path, + ), + "release_final_archive_status": _coerce_str( + payload["release_final_archive_status"], + field="release_final_archive_status", + path=path, + ), + "release_final_archive_decision": _coerce_str( + payload["release_final_archive_decision"], + field="release_final_archive_decision", + path=path, + ), + "release_final_archive_exit_code": _coerce_int( + payload["release_final_archive_exit_code"], + field="release_final_archive_exit_code", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_verdict_publish_should_notify": _coerce_bool( + payload["final_verdict_publish_should_notify"], + field="final_verdict_publish_should_notify", + path=path, + ), + "final_verdict_publish_requires_manual_action": _coerce_bool( + payload["final_verdict_publish_requires_manual_action"], + field="final_verdict_publish_requires_manual_action", + path=path, + ), + "final_verdict_publish_channel": final_verdict_publish_channel, + "release_final_verdict_status": _coerce_str( + payload["release_final_verdict_status"], + field="release_final_verdict_status", + path=path, + ), + "release_final_verdict_decision": _coerce_str( + payload["release_final_verdict_decision"], + field="release_final_verdict_decision", + path=path, + ), + "release_final_verdict_exit_code": _coerce_int( + payload["release_final_verdict_exit_code"], + field="release_final_verdict_exit_code", + path=path, + ), + "release_final_verdict_publish_status": release_final_verdict_publish_status, + "release_final_verdict_publish_decision": release_final_verdict_publish_decision, + "release_final_verdict_publish_exit_code": release_final_verdict_publish_exit_code, + "release_final_verdict_publish_summary": _coerce_str( + payload["release_final_verdict_publish_summary"], + field="release_final_verdict_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_publish_archive_payload( + release_final_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_verdict_publish_status = str( + release_final_verdict_publish_report["release_final_verdict_publish_status"] + ) + final_verdict_publish_decision = str( + release_final_verdict_publish_report["release_final_verdict_publish_decision"] + ) + final_verdict_publish_exit_code = int( + release_final_verdict_publish_report["release_final_verdict_publish_exit_code"] + ) + final_verdict_publish_should_notify = bool( + release_final_verdict_publish_report["final_verdict_publish_should_notify"] + ) + final_verdict_publish_requires_manual_action = bool( + release_final_verdict_publish_report["final_verdict_publish_requires_manual_action"] + ) + final_verdict_publish_channel = str( + release_final_verdict_publish_report["final_verdict_publish_channel"] + ) + + reason_codes = list(release_final_verdict_publish_report["reason_codes"]) + structural_issues = list(release_final_verdict_publish_report["structural_issues"]) + missing_artifacts = list(release_final_verdict_publish_report["missing_artifacts"]) + evidence_manifest = list(release_final_verdict_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_publish_archive_evidence_missing") + + expected = { + "published": ( + "announce_release_shipped", + 0, + True, + False, + "release", + ), + "published_with_follow_up": ( + "announce_release_shipped_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_release_blocker", + None, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_channel, + ) = expected[final_verdict_publish_status] + + if final_verdict_publish_decision != expected_decision: + structural_issues.append("final_verdict_publish_decision_mismatch") + if final_verdict_publish_should_notify != expected_should_notify: + structural_issues.append("final_verdict_publish_should_notify_mismatch") + if final_verdict_publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append("final_verdict_publish_requires_manual_action_mismatch") + if final_verdict_publish_channel != expected_channel: + structural_issues.append("final_verdict_publish_channel_mismatch") + if expected_exit_code == 0 and final_verdict_publish_exit_code != 0: + structural_issues.append("final_verdict_publish_exit_code_mismatch_published") + if expected_exit_code is None and final_verdict_publish_exit_code == 0: + structural_issues.append("final_verdict_publish_exit_code_mismatch_non_published") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + archive_status = "contract_failed" + archive_decision = "abort_archive" + archive_exit_code = max(1, final_verdict_publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_verdict_publish_status == "published": + archive_status = "archived" + archive_decision = "archive_release_shipped" + archive_exit_code = 0 + archive_should_archive = True + archive_requires_manual_action = False + archive_channel = "release" + reason_codes = ["release_final_publish_archive_archived"] + elif final_verdict_publish_status == "published_with_follow_up": + archive_status = "archived_with_follow_up" + archive_decision = "archive_release_shipped_with_follow_up" + archive_exit_code = max(1, final_verdict_publish_exit_code) + archive_should_archive = True + archive_requires_manual_action = False + archive_channel = "follow_up" + reason_codes.append("release_final_publish_archive_archived_with_follow_up") + elif final_verdict_publish_status == "blocked": + archive_status = "blocked" + archive_decision = "archive_release_blocker" + archive_exit_code = max(1, final_verdict_publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.append("release_final_publish_archive_blocked") + else: + archive_status = "contract_failed" + archive_decision = "abort_archive" + archive_exit_code = max(1, final_verdict_publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_channel = "blocker" + reason_codes.append("release_final_publish_archive_upstream_contract_failed") + + if archive_status not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_STATUSES: + raise ValueError("internal: unsupported release_final_publish_archive_status computed") + if archive_decision not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_DECISIONS: + raise ValueError("internal: unsupported release_final_publish_archive_decision computed") + if archive_channel not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_CHANNELS: + raise ValueError("internal: unsupported release_final_publish_archive_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_verdict_publish_status={final_verdict_publish_status} " + f"release_final_publish_archive_status={archive_status} " + f"release_final_publish_archive_decision={archive_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_verdict_publish_report": str(source_path), + "source_release_final_verdict_report": str( + release_final_verdict_publish_report["source_release_final_verdict_report"] + ), + "source_release_final_archive_report": str( + release_final_verdict_publish_report["source_release_final_archive_report"] + ), + "source_release_final_closure_publish_report": str( + release_final_verdict_publish_report["source_release_final_closure_publish_report"] + ), + "source_release_final_closure_report": str( + release_final_verdict_publish_report["source_release_final_closure_report"] + ), + "source_release_final_handoff_report": str( + release_final_verdict_publish_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_verdict_publish_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_verdict_publish_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_verdict_publish_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_verdict_publish_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_verdict_publish_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_verdict_publish_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_verdict_publish_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_verdict_publish_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_verdict_publish_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_verdict_publish_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_verdict_publish_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_verdict_publish_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_verdict_publish_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_verdict_publish_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_verdict_publish_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_verdict_publish_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str( + release_final_verdict_publish_report["release_final_handoff_status"] + ), + "release_final_handoff_decision": str( + release_final_verdict_publish_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_verdict_publish_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": str( + release_final_verdict_publish_report["release_final_closure_status"] + ), + "release_final_closure_decision": str( + release_final_verdict_publish_report["release_final_closure_decision"] + ), + "release_final_closure_exit_code": int( + release_final_verdict_publish_report["release_final_closure_exit_code"] + ), + "release_final_closure_publish_status": str( + release_final_verdict_publish_report["release_final_closure_publish_status"] + ), + "release_final_closure_publish_decision": str( + release_final_verdict_publish_report["release_final_closure_publish_decision"] + ), + "release_final_closure_publish_exit_code": int( + release_final_verdict_publish_report["release_final_closure_publish_exit_code"] + ), + "release_final_archive_status": str( + release_final_verdict_publish_report["release_final_archive_status"] + ), + "release_final_archive_decision": str( + release_final_verdict_publish_report["release_final_archive_decision"] + ), + "release_final_archive_exit_code": int( + release_final_verdict_publish_report["release_final_archive_exit_code"] + ), + "release_final_verdict_status": str( + release_final_verdict_publish_report["release_final_verdict_status"] + ), + "release_final_verdict_decision": str( + release_final_verdict_publish_report["release_final_verdict_decision"] + ), + "release_final_verdict_exit_code": int( + release_final_verdict_publish_report["release_final_verdict_exit_code"] + ), + "release_final_verdict_publish_status": final_verdict_publish_status, + "release_final_verdict_publish_decision": final_verdict_publish_decision, + "release_final_verdict_publish_exit_code": final_verdict_publish_exit_code, + "release_run_id": release_final_verdict_publish_report["release_run_id"], + "release_run_url": str(release_final_verdict_publish_report["release_run_url"]), + "follow_up_queue_url": str(release_final_verdict_publish_report["follow_up_queue_url"]), + "final_publish_archive_should_archive": archive_should_archive, + "final_publish_archive_requires_manual_action": archive_requires_manual_action, + "final_publish_archive_channel": archive_channel, + "release_final_publish_archive_status": archive_status, + "release_final_publish_archive_decision": archive_decision, + "release_final_publish_archive_exit_code": archive_exit_code, + "release_final_publish_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Publish Archive Report", + "", + ( + "- Release Final Publish Archive Status: " + f"**{str(payload['release_final_publish_archive_status']).upper()}**" + ), + ( + "- Release Final Publish Archive Decision: " + f"`{payload['release_final_publish_archive_decision']}`" + ), + ( + "- Release Final Publish Archive Exit Code: " + f"`{payload['release_final_publish_archive_exit_code']}`" + ), + ( + "- Final Publish Archive Should Archive: " + f"`{payload['final_publish_archive_should_archive']}`" + ), + ( + "- Final Publish Archive Requires Manual Action: " + f"`{payload['final_publish_archive_requires_manual_action']}`" + ), + f"- Final Publish Archive Channel: `{payload['final_publish_archive_channel']}`", + f"- Release Final Verdict Publish Status: `{payload['release_final_verdict_publish_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_publish_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_publish_archive_status": str( + payload["release_final_publish_archive_status"] + ), + "workflow_release_final_publish_archive_decision": str( + payload["release_final_publish_archive_decision"] + ), + "workflow_release_final_publish_archive_exit_code": str( + payload["release_final_publish_archive_exit_code"] + ), + "workflow_release_final_publish_archive_should_archive": ( + "true" if payload["final_publish_archive_should_archive"] else "false" + ), + "workflow_release_final_publish_archive_requires_manual_action": ( + "true" if payload["final_publish_archive_requires_manual_action"] else "false" + ), + "workflow_release_final_publish_archive_channel": str( + payload["final_publish_archive_channel"] + ), + "workflow_release_final_publish_archive_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_release_final_publish_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_publish_archive_run_url": str(payload["release_run_url"]), + "workflow_release_final_publish_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_publish_archive_report_json": str(output_json), + "workflow_release_final_publish_archive_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final publish archive contract " + "from P2-55 release final verdict publish report" + ) + ) + parser.add_argument( + "--release-final-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json", + help="P2-55 release final verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json", + help="Output release final publish archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md", + help="Output release final publish archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final publish archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_verdict_publish_report_path = Path(args.release_final_verdict_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_verdict_publish_report = load_release_final_verdict_publish_report( + release_final_verdict_publish_report_path + ) + payload = build_release_final_publish_archive_payload( + release_final_verdict_publish_report, + source_path=release_final_verdict_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-release-final-publish-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final publish archive json: {output_json_path}") + print(f"release final publish archive markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final publish archive summary: " + f"release_final_publish_archive_status={payload['release_final_publish_archive_status']} " + f"release_final_publish_archive_decision={payload['release_final_publish_archive_decision']} " + f"release_final_publish_archive_exit_code={payload['release_final_publish_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_publish_archive_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py new file mode 100644 index 0000000..b08c3d5 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py @@ -0,0 +1,746 @@ +"""Phase 2 card P2-49 gate for Linux CI workflow release final terminal publish. + +This script consumes the P2-48 release final outcome artifact and converges +the terminal publish contract: +1) validate release final outcome + evidence consistency, +2) normalize terminal publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: set[str] = { + "ship_and_close", + "ship_with_follow_up_open", + "escalate_blocker", + "abort_outcome", +} +ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_release_closure", + "announce_release_with_follow_up", + "announce_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_outcome_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final outcome report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final outcome payload must be object") + + required_fields = ( + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "final_should_ship_release", + "final_follow_up_open", + "final_should_page_owner", + "final_outcome_target", + "release_final_outcome_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_delivery_final_verdict_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if release_delivery_final_verdict_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + + release_delivery_final_verdict_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if ( + release_delivery_final_verdict_decision + not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + release_follow_up_final_verdict_status = _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ) + if release_follow_up_final_verdict_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES)}" + ) + + release_follow_up_final_verdict_decision = _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ) + if ( + release_follow_up_final_verdict_decision + not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS)}" + ) + + release_final_outcome_status = _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ) + if release_final_outcome_status not in ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES: + raise ValueError( + f"{path}: field 'release_final_outcome_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_STATUSES)}" + ) + + release_final_outcome_decision = _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ) + if release_final_outcome_decision not in ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_outcome_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_DECISIONS)}" + ) + + final_outcome_target = _coerce_str( + payload["final_outcome_target"], + field="final_outcome_target", + path=path, + ) + if final_outcome_target not in ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS: + raise ValueError( + f"{path}: field 'final_outcome_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_OUTCOME_TARGETS)}" + ) + + release_delivery_final_verdict_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + release_follow_up_final_verdict_exit_code = _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ) + release_final_outcome_exit_code = _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ) + if release_delivery_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + if release_follow_up_final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_final_verdict_exit_code' must be >= 0" + ) + if release_final_outcome_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_outcome_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "release_final_outcome_status": release_final_outcome_status, + "release_final_outcome_decision": release_final_outcome_decision, + "release_final_outcome_exit_code": release_final_outcome_exit_code, + "final_should_ship_release": _coerce_bool( + payload["final_should_ship_release"], + field="final_should_ship_release", + path=path, + ), + "final_follow_up_open": _coerce_bool( + payload["final_follow_up_open"], + field="final_follow_up_open", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_outcome_target": final_outcome_target, + "release_final_outcome_summary": _coerce_str( + payload["release_final_outcome_summary"], + field="release_final_outcome_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_terminal_publish_payload( + release_final_outcome_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_outcome_status = str(release_final_outcome_report["release_final_outcome_status"]) + final_outcome_decision = str(release_final_outcome_report["release_final_outcome_decision"]) + final_outcome_exit_code = int(release_final_outcome_report["release_final_outcome_exit_code"]) + final_should_ship_release = bool(release_final_outcome_report["final_should_ship_release"]) + final_follow_up_open = bool(release_final_outcome_report["final_follow_up_open"]) + final_should_page_owner = bool(release_final_outcome_report["final_should_page_owner"]) + final_outcome_target = str(release_final_outcome_report["final_outcome_target"]) + + release_delivery_final_verdict_status = str( + release_final_outcome_report["release_delivery_final_verdict_status"] + ) + release_delivery_final_verdict_decision = str( + release_final_outcome_report["release_delivery_final_verdict_decision"] + ) + release_delivery_final_verdict_exit_code = int( + release_final_outcome_report["release_delivery_final_verdict_exit_code"] + ) + + release_follow_up_final_verdict_status = str( + release_final_outcome_report["release_follow_up_final_verdict_status"] + ) + release_follow_up_final_verdict_decision = str( + release_final_outcome_report["release_follow_up_final_verdict_decision"] + ) + release_follow_up_final_verdict_exit_code = int( + release_final_outcome_report["release_follow_up_final_verdict_exit_code"] + ) + + reason_codes = list(release_final_outcome_report["reason_codes"]) + structural_issues = list(release_final_outcome_report["structural_issues"]) + missing_artifacts = list(release_final_outcome_report["missing_artifacts"]) + evidence_manifest = list(release_final_outcome_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_terminal_publish_evidence_missing") + + expected = { + "released": ("ship_and_close", 0, True, False, False, "release"), + "released_with_follow_up": ( + "ship_with_follow_up_open", + None, + True, + True, + False, + "follow_up", + ), + "blocked": ("escalate_blocker", None, False, True, True, "blocker"), + "contract_failed": ("abort_outcome", None, False, True, True, "blocker"), + } + expected_decision, expected_exit_code, expected_ship, expected_follow_up, expected_page, expected_target = ( + expected[final_outcome_status] + ) + + if final_outcome_decision != expected_decision: + structural_issues.append("final_outcome_decision_mismatch") + if final_should_ship_release != expected_ship: + structural_issues.append("final_should_ship_release_mismatch") + if final_follow_up_open != expected_follow_up: + structural_issues.append("final_follow_up_open_mismatch") + if final_should_page_owner != expected_page: + structural_issues.append("final_should_page_owner_mismatch") + if final_outcome_target != expected_target: + structural_issues.append("final_outcome_target_mismatch") + + if expected_exit_code == 0 and final_outcome_exit_code != 0: + structural_issues.append("final_outcome_exit_code_mismatch_released") + if expected_exit_code is None and final_outcome_exit_code == 0: + structural_issues.append("final_outcome_exit_code_mismatch_non_released") + + if final_outcome_status == "released": + if release_delivery_final_verdict_status not in {"completed", "requires_follow_up"}: + structural_issues.append("delivery_status_mismatch_released") + if release_follow_up_final_verdict_status != "completed": + structural_issues.append("follow_up_status_mismatch_released") + elif final_outcome_status == "released_with_follow_up": + if release_delivery_final_verdict_status not in {"completed", "requires_follow_up"}: + structural_issues.append("delivery_status_mismatch_released_with_follow_up") + if release_follow_up_final_verdict_status != "requires_follow_up": + structural_issues.append("follow_up_status_mismatch_released_with_follow_up") + elif final_outcome_status == "blocked": + if ( + release_delivery_final_verdict_status != "blocked" + and release_follow_up_final_verdict_status != "blocked" + ): + structural_issues.append("upstream_blocked_status_missing") + else: + if ( + release_delivery_final_verdict_status != "contract_failed" + and release_follow_up_final_verdict_status != "contract_failed" + and not structural_issues + ): + structural_issues.append("upstream_contract_failed_status_missing") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + terminal_publish_status = "contract_failed" + terminal_publish_decision = "abort_publish" + terminal_publish_exit_code = max( + 1, + final_outcome_exit_code, + release_delivery_final_verdict_exit_code, + release_follow_up_final_verdict_exit_code, + ) + final_terminal_should_notify = True + final_terminal_requires_manual_action = True + final_terminal_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_outcome_status == "released": + terminal_publish_status = "published" + terminal_publish_decision = "announce_release_closure" + terminal_publish_exit_code = 0 + final_terminal_should_notify = True + final_terminal_requires_manual_action = False + final_terminal_channel = "release" + reason_codes = ["release_final_terminal_published"] + elif final_outcome_status == "released_with_follow_up": + terminal_publish_status = "published_with_follow_up" + terminal_publish_decision = "announce_release_with_follow_up" + terminal_publish_exit_code = max(1, final_outcome_exit_code) + final_terminal_should_notify = True + final_terminal_requires_manual_action = False + final_terminal_channel = "follow_up" + reason_codes.append("release_final_terminal_published_with_follow_up") + elif final_outcome_status == "blocked": + terminal_publish_status = "blocked" + terminal_publish_decision = "announce_blocker" + terminal_publish_exit_code = max(1, final_outcome_exit_code) + final_terminal_should_notify = True + final_terminal_requires_manual_action = True + final_terminal_channel = "blocker" + reason_codes.append("release_final_terminal_blocked") + else: + terminal_publish_status = "contract_failed" + terminal_publish_decision = "abort_publish" + terminal_publish_exit_code = max(1, final_outcome_exit_code) + final_terminal_should_notify = True + final_terminal_requires_manual_action = True + final_terminal_channel = "blocker" + reason_codes.append("release_final_terminal_upstream_contract_failed") + + if terminal_publish_status not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_STATUSES: + raise ValueError("internal: unsupported release_final_terminal_publish_status computed") + if terminal_publish_decision not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported release_final_terminal_publish_decision computed") + if final_terminal_channel not in ALLOWED_RELEASE_FINAL_TERMINAL_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported release_final_terminal_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_outcome_status={final_outcome_status} " + f"release_final_terminal_publish_status={terminal_publish_status} " + f"release_final_terminal_publish_decision={terminal_publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_outcome_report": str(source_path), + "source_release_delivery_final_verdict_report": str( + release_final_outcome_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_outcome_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": release_delivery_final_verdict_status, + "release_delivery_final_verdict_decision": release_delivery_final_verdict_decision, + "release_delivery_final_verdict_exit_code": release_delivery_final_verdict_exit_code, + "release_follow_up_final_verdict_status": release_follow_up_final_verdict_status, + "release_follow_up_final_verdict_decision": release_follow_up_final_verdict_decision, + "release_follow_up_final_verdict_exit_code": release_follow_up_final_verdict_exit_code, + "release_final_outcome_status": final_outcome_status, + "release_final_outcome_decision": final_outcome_decision, + "release_final_outcome_exit_code": final_outcome_exit_code, + "release_run_id": release_final_outcome_report["release_run_id"], + "release_run_url": str(release_final_outcome_report["release_run_url"]), + "follow_up_queue_url": str(release_final_outcome_report["follow_up_queue_url"]), + "final_should_ship_release": final_should_ship_release, + "final_follow_up_open": final_follow_up_open, + "final_should_page_owner": final_should_page_owner, + "final_outcome_target": final_outcome_target, + "release_final_terminal_publish_status": terminal_publish_status, + "release_final_terminal_publish_decision": terminal_publish_decision, + "release_final_terminal_publish_exit_code": terminal_publish_exit_code, + "final_terminal_should_notify": final_terminal_should_notify, + "final_terminal_requires_manual_action": final_terminal_requires_manual_action, + "final_terminal_channel": final_terminal_channel, + "release_final_terminal_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Terminal Publish Report", + "", + ( + "- Release Final Terminal Publish Status: " + f"**{str(payload['release_final_terminal_publish_status']).upper()}**" + ), + ( + "- Release Final Terminal Publish Decision: " + f"`{payload['release_final_terminal_publish_decision']}`" + ), + ( + "- Release Final Terminal Publish Exit Code: " + f"`{payload['release_final_terminal_publish_exit_code']}`" + ), + f"- Final Terminal Should Notify: `{payload['final_terminal_should_notify']}`", + f"- Final Terminal Requires Manual Action: `{payload['final_terminal_requires_manual_action']}`", + f"- Final Terminal Channel: `{payload['final_terminal_channel']}`", + f"- Release Final Outcome Status: `{payload['release_final_outcome_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_terminal_publish_status": str( + payload["release_final_terminal_publish_status"] + ), + "workflow_release_final_terminal_publish_decision": str( + payload["release_final_terminal_publish_decision"] + ), + "workflow_release_final_terminal_publish_exit_code": str( + payload["release_final_terminal_publish_exit_code"] + ), + "workflow_release_final_terminal_should_notify": ( + "true" if payload["final_terminal_should_notify"] else "false" + ), + "workflow_release_final_terminal_requires_manual_action": ( + "true" if payload["final_terminal_requires_manual_action"] else "false" + ), + "workflow_release_final_terminal_channel": str(payload["final_terminal_channel"]), + "workflow_release_final_terminal_outcome_status": str(payload["release_final_outcome_status"]), + "workflow_release_final_terminal_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_terminal_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_terminal_run_url": str(payload["release_run_url"]), + "workflow_release_final_terminal_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_terminal_report_json": str(output_json), + "workflow_release_final_terminal_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final terminal publish contract " + "from P2-48 release final outcome report" + ) + ) + parser.add_argument( + "--release-final-outcome-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json", + help="P2-48 release final outcome report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json", + help="Output release final terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md", + help="Output release final terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_outcome_report_path = Path(args.release_final_outcome_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_outcome_report = load_release_final_outcome_report( + release_final_outcome_report_path + ) + payload = build_release_final_terminal_publish_payload( + release_final_outcome_report, + source_path=release_final_outcome_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-final-terminal-publish-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final terminal publish json: {output_json_path}") + print(f"release final terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final terminal publish summary: " + f"release_final_terminal_publish_status={payload['release_final_terminal_publish_status']} " + f"release_final_terminal_publish_decision={payload['release_final_terminal_publish_decision']} " + f"release_final_terminal_publish_exit_code={payload['release_final_terminal_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_terminal_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py new file mode 100644 index 0000000..cf80edc --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py @@ -0,0 +1,817 @@ +"""Phase 2 card P2-54 gate for Linux CI workflow release final verdict. + +This script consumes the P2-53 release final archive artifact and converges +one terminal final-verdict contract: +1) validate release final archive + evidence consistency, +2) normalize final release verdict semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FINAL_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_ARCHIVE_DECISIONS: set[str] = { + "archive_release_closed", + "archive_release_closed_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_RELEASE_FINAL_ARCHIVE_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_RELEASE_FINAL_VERDICT_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_VERDICT_DECISIONS: set[str] = { + "ship_release", + "ship_release_with_follow_up", + "escalate_release_blocker", + "abort_verdict", +} +ALLOWED_RELEASE_FINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + entry_source = _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path) + entry_path = _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path) + entry_exists = _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path) + evidence_manifest.append( + {"source": entry_source, "path": entry_path, "exists": entry_exists} + ) + return evidence_manifest + + +def load_release_final_archive_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final archive report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final archive payload must be object") + + required_fields = ( + "source_release_final_closure_publish_report", + "source_release_final_closure_report", + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_publish_status", + "release_final_closure_publish_decision", + "release_final_closure_publish_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_archive_should_publish", + "final_archive_requires_manual_action", + "final_archive_channel", + "release_final_archive_status", + "release_final_archive_decision", + "release_final_archive_exit_code", + "release_final_archive_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_final_archive_status = _coerce_str( + payload["release_final_archive_status"], + field="release_final_archive_status", + path=path, + ) + if release_final_archive_status not in ALLOWED_RELEASE_FINAL_ARCHIVE_STATUSES: + raise ValueError( + f"{path}: field 'release_final_archive_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_ARCHIVE_STATUSES)}" + ) + + release_final_archive_decision = _coerce_str( + payload["release_final_archive_decision"], + field="release_final_archive_decision", + path=path, + ) + if release_final_archive_decision not in ALLOWED_RELEASE_FINAL_ARCHIVE_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_archive_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_ARCHIVE_DECISIONS)}" + ) + + final_archive_channel = _coerce_str( + payload["final_archive_channel"], + field="final_archive_channel", + path=path, + ) + if final_archive_channel not in ALLOWED_RELEASE_FINAL_ARCHIVE_CHANNELS: + raise ValueError( + f"{path}: field 'final_archive_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_ARCHIVE_CHANNELS)}" + ) + + release_final_archive_exit_code = _coerce_int( + payload["release_final_archive_exit_code"], + field="release_final_archive_exit_code", + path=path, + ) + if release_final_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_archive_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_closure_publish_report": _coerce_str( + payload["source_release_final_closure_publish_report"], + field="source_release_final_closure_publish_report", + path=path, + ), + "source_release_final_closure_report": _coerce_str( + payload["source_release_final_closure_report"], + field="source_release_final_closure_report", + path=path, + ), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ), + "release_delivery_final_verdict_decision": _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ), + "release_delivery_final_verdict_exit_code": _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ), + "release_follow_up_final_verdict_status": _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ), + "release_follow_up_final_verdict_decision": _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ), + "release_follow_up_final_verdict_exit_code": _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ), + "release_final_outcome_status": _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ), + "release_final_outcome_decision": _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ), + "release_final_outcome_exit_code": _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ), + "release_final_terminal_publish_status": _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ), + "release_final_terminal_publish_decision": _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ), + "release_final_terminal_publish_exit_code": _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ), + "release_final_handoff_status": _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ), + "release_final_handoff_decision": _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ), + "release_final_handoff_exit_code": _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ), + "release_final_closure_status": _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ), + "release_final_closure_decision": _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ), + "release_final_closure_exit_code": _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ), + "release_final_closure_publish_status": _coerce_str( + payload["release_final_closure_publish_status"], + field="release_final_closure_publish_status", + path=path, + ), + "release_final_closure_publish_decision": _coerce_str( + payload["release_final_closure_publish_decision"], + field="release_final_closure_publish_decision", + path=path, + ), + "release_final_closure_publish_exit_code": _coerce_int( + payload["release_final_closure_publish_exit_code"], + field="release_final_closure_publish_exit_code", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_archive_should_publish": _coerce_bool( + payload["final_archive_should_publish"], + field="final_archive_should_publish", + path=path, + ), + "final_archive_requires_manual_action": _coerce_bool( + payload["final_archive_requires_manual_action"], + field="final_archive_requires_manual_action", + path=path, + ), + "final_archive_channel": final_archive_channel, + "release_final_archive_status": release_final_archive_status, + "release_final_archive_decision": release_final_archive_decision, + "release_final_archive_exit_code": release_final_archive_exit_code, + "release_final_archive_summary": _coerce_str( + payload["release_final_archive_summary"], + field="release_final_archive_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_verdict_payload( + release_final_archive_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_archive_status = str(release_final_archive_report["release_final_archive_status"]) + final_archive_decision = str(release_final_archive_report["release_final_archive_decision"]) + final_archive_exit_code = int(release_final_archive_report["release_final_archive_exit_code"]) + final_archive_should_publish = bool(release_final_archive_report["final_archive_should_publish"]) + final_archive_requires_manual_action = bool( + release_final_archive_report["final_archive_requires_manual_action"] + ) + final_archive_channel = str(release_final_archive_report["final_archive_channel"]) + + reason_codes = list(release_final_archive_report["reason_codes"]) + structural_issues = list(release_final_archive_report["structural_issues"]) + missing_artifacts = list(release_final_archive_report["missing_artifacts"]) + evidence_manifest = list(release_final_archive_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_verdict_evidence_missing") + + expected = { + "archived": ( + "archive_release_closed", + 0, + True, + False, + "release", + ), + "archived_with_follow_up": ( + "archive_release_closed_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "archive_release_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_archive", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_publish, + expected_requires_manual_action, + expected_channel, + ) = expected[final_archive_status] + + if final_archive_decision != expected_decision: + structural_issues.append("final_archive_decision_mismatch") + if final_archive_should_publish != expected_should_publish: + structural_issues.append("final_archive_should_publish_mismatch") + if final_archive_requires_manual_action != expected_requires_manual_action: + structural_issues.append("final_archive_requires_manual_action_mismatch") + if final_archive_channel != expected_channel: + structural_issues.append("final_archive_channel_mismatch") + if expected_exit_code == 0 and final_archive_exit_code != 0: + structural_issues.append("final_archive_exit_code_mismatch_archived") + if expected_exit_code is None and final_archive_exit_code == 0: + structural_issues.append("final_archive_exit_code_mismatch_non_archived") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + final_verdict_status = "contract_failed" + final_verdict_decision = "abort_verdict" + final_verdict_exit_code = max(1, final_archive_exit_code) + final_verdict_should_ship = False + final_verdict_requires_manual_action = True + final_verdict_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_archive_status == "archived": + final_verdict_status = "released" + final_verdict_decision = "ship_release" + final_verdict_exit_code = 0 + final_verdict_should_ship = True + final_verdict_requires_manual_action = False + final_verdict_channel = "release" + reason_codes = ["release_final_verdict_released"] + elif final_archive_status == "archived_with_follow_up": + final_verdict_status = "released_with_follow_up" + final_verdict_decision = "ship_release_with_follow_up" + final_verdict_exit_code = max(1, final_archive_exit_code) + final_verdict_should_ship = True + final_verdict_requires_manual_action = False + final_verdict_channel = "follow_up" + reason_codes.append("release_final_verdict_released_with_follow_up") + elif final_archive_status == "blocked": + final_verdict_status = "blocked" + final_verdict_decision = "escalate_release_blocker" + final_verdict_exit_code = max(1, final_archive_exit_code) + final_verdict_should_ship = False + final_verdict_requires_manual_action = True + final_verdict_channel = "blocker" + reason_codes.append("release_final_verdict_blocked") + else: + final_verdict_status = "contract_failed" + final_verdict_decision = "abort_verdict" + final_verdict_exit_code = max(1, final_archive_exit_code) + final_verdict_should_ship = False + final_verdict_requires_manual_action = True + final_verdict_channel = "blocker" + reason_codes.append("release_final_verdict_upstream_contract_failed") + + if final_verdict_status not in ALLOWED_RELEASE_FINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported release_final_verdict_status computed") + if final_verdict_decision not in ALLOWED_RELEASE_FINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported release_final_verdict_decision computed") + if final_verdict_channel not in ALLOWED_RELEASE_FINAL_VERDICT_CHANNELS: + raise ValueError("internal: unsupported release_final_verdict_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_archive_status={final_archive_status} " + f"release_final_verdict_status={final_verdict_status} " + f"release_final_verdict_decision={final_verdict_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_archive_report": str(source_path), + "source_release_final_closure_publish_report": str( + release_final_archive_report["source_release_final_closure_publish_report"] + ), + "source_release_final_closure_report": str( + release_final_archive_report["source_release_final_closure_report"] + ), + "source_release_final_handoff_report": str( + release_final_archive_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_archive_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_archive_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_archive_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_archive_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_archive_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_archive_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_archive_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_archive_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_archive_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_archive_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_archive_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_archive_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_archive_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_archive_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_archive_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_archive_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str( + release_final_archive_report["release_final_handoff_status"] + ), + "release_final_handoff_decision": str( + release_final_archive_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_archive_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": str( + release_final_archive_report["release_final_closure_status"] + ), + "release_final_closure_decision": str( + release_final_archive_report["release_final_closure_decision"] + ), + "release_final_closure_exit_code": int( + release_final_archive_report["release_final_closure_exit_code"] + ), + "release_final_closure_publish_status": str( + release_final_archive_report["release_final_closure_publish_status"] + ), + "release_final_closure_publish_decision": str( + release_final_archive_report["release_final_closure_publish_decision"] + ), + "release_final_closure_publish_exit_code": int( + release_final_archive_report["release_final_closure_publish_exit_code"] + ), + "release_final_archive_status": final_archive_status, + "release_final_archive_decision": final_archive_decision, + "release_final_archive_exit_code": final_archive_exit_code, + "release_run_id": release_final_archive_report["release_run_id"], + "release_run_url": str(release_final_archive_report["release_run_url"]), + "follow_up_queue_url": str(release_final_archive_report["follow_up_queue_url"]), + "final_verdict_should_ship": final_verdict_should_ship, + "final_verdict_requires_manual_action": final_verdict_requires_manual_action, + "final_verdict_channel": final_verdict_channel, + "release_final_verdict_status": final_verdict_status, + "release_final_verdict_decision": final_verdict_decision, + "release_final_verdict_exit_code": final_verdict_exit_code, + "release_final_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Verdict Report", + "", + f"- Release Final Verdict Status: **{str(payload['release_final_verdict_status']).upper()}**", + f"- Release Final Verdict Decision: `{payload['release_final_verdict_decision']}`", + f"- Release Final Verdict Exit Code: `{payload['release_final_verdict_exit_code']}`", + f"- Final Verdict Should Ship: `{payload['final_verdict_should_ship']}`", + ( + "- Final Verdict Requires Manual Action: " + f"`{payload['final_verdict_requires_manual_action']}`" + ), + f"- Final Verdict Channel: `{payload['final_verdict_channel']}`", + f"- Release Final Archive Status: `{payload['release_final_archive_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_verdict_status": str(payload["release_final_verdict_status"]), + "workflow_release_final_verdict_decision": str(payload["release_final_verdict_decision"]), + "workflow_release_final_verdict_exit_code": str(payload["release_final_verdict_exit_code"]), + "workflow_release_final_verdict_should_ship": ( + "true" if payload["final_verdict_should_ship"] else "false" + ), + "workflow_release_final_verdict_requires_manual_action": ( + "true" if payload["final_verdict_requires_manual_action"] else "false" + ), + "workflow_release_final_verdict_channel": str(payload["final_verdict_channel"]), + "workflow_release_final_verdict_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_final_verdict_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_verdict_run_url": str(payload["release_run_url"]), + "workflow_release_final_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_verdict_report_json": str(output_json), + "workflow_release_final_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final verdict contract " + "from P2-53 release final archive report" + ) + ) + parser.add_argument( + "--release-final-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json", + help="P2-53 release final archive report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json", + help="Output release final verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md", + help="Output release final verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_archive_report_path = Path(args.release_final_archive_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_archive_report = load_release_final_archive_report( + release_final_archive_report_path + ) + payload = build_release_final_verdict_payload( + release_final_archive_report, + source_path=release_final_archive_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-release-final-verdict-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final verdict json: {output_json_path}") + print(f"release final verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final verdict summary: " + f"release_final_verdict_status={payload['release_final_verdict_status']} " + f"release_final_verdict_decision={payload['release_final_verdict_decision']} " + f"release_final_verdict_exit_code={payload['release_final_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py new file mode 100644 index 0000000..51ceb44 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py @@ -0,0 +1,863 @@ +"""Phase 2 card P2-55 gate for Linux CI workflow release final verdict publish. + +This script consumes the P2-54 release final verdict artifact and converges +one terminal final-verdict-publish contract: +1) validate release final verdict + evidence consistency, +2) normalize final publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FINAL_VERDICT_STATUSES: set[str] = { + "released", + "released_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_VERDICT_DECISIONS: set[str] = { + "ship_release", + "ship_release_with_follow_up", + "escalate_release_blocker", + "abort_verdict", +} +ALLOWED_RELEASE_FINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_release_shipped", + "announce_release_shipped_with_follow_up", + "announce_release_blocker", + "abort_publish", +} +ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_release_final_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final verdict payload must be object") + + required_fields = ( + "source_release_final_archive_report", + "source_release_final_closure_publish_report", + "source_release_final_closure_report", + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_publish_status", + "release_final_closure_publish_decision", + "release_final_closure_publish_exit_code", + "release_final_archive_status", + "release_final_archive_decision", + "release_final_archive_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_verdict_should_ship", + "final_verdict_requires_manual_action", + "final_verdict_channel", + "release_final_verdict_status", + "release_final_verdict_decision", + "release_final_verdict_exit_code", + "release_final_verdict_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_final_verdict_status = _coerce_str( + payload["release_final_verdict_status"], + field="release_final_verdict_status", + path=path, + ) + if release_final_verdict_status not in ALLOWED_RELEASE_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_STATUSES)}" + ) + release_final_verdict_decision = _coerce_str( + payload["release_final_verdict_decision"], + field="release_final_verdict_decision", + path=path, + ) + if release_final_verdict_decision not in ALLOWED_RELEASE_FINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_DECISIONS)}" + ) + release_final_verdict_exit_code = _coerce_int( + payload["release_final_verdict_exit_code"], + field="release_final_verdict_exit_code", + path=path, + ) + if release_final_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_verdict_exit_code' must be >= 0") + final_verdict_channel = _coerce_str( + payload["final_verdict_channel"], + field="final_verdict_channel", + path=path, + ) + if final_verdict_channel not in ALLOWED_RELEASE_FINAL_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'final_verdict_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_VERDICT_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_archive_report": _coerce_str( + payload["source_release_final_archive_report"], + field="source_release_final_archive_report", + path=path, + ), + "source_release_final_closure_publish_report": _coerce_str( + payload["source_release_final_closure_publish_report"], + field="source_release_final_closure_publish_report", + path=path, + ), + "source_release_final_closure_report": _coerce_str( + payload["source_release_final_closure_report"], + field="source_release_final_closure_report", + path=path, + ), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ), + "release_delivery_final_verdict_decision": _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ), + "release_delivery_final_verdict_exit_code": _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ), + "release_follow_up_final_verdict_status": _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ), + "release_follow_up_final_verdict_decision": _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ), + "release_follow_up_final_verdict_exit_code": _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ), + "release_final_outcome_status": _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ), + "release_final_outcome_decision": _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ), + "release_final_outcome_exit_code": _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ), + "release_final_terminal_publish_status": _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ), + "release_final_terminal_publish_decision": _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ), + "release_final_terminal_publish_exit_code": _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ), + "release_final_handoff_status": _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ), + "release_final_handoff_decision": _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ), + "release_final_handoff_exit_code": _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ), + "release_final_closure_status": _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ), + "release_final_closure_decision": _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ), + "release_final_closure_exit_code": _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ), + "release_final_closure_publish_status": _coerce_str( + payload["release_final_closure_publish_status"], + field="release_final_closure_publish_status", + path=path, + ), + "release_final_closure_publish_decision": _coerce_str( + payload["release_final_closure_publish_decision"], + field="release_final_closure_publish_decision", + path=path, + ), + "release_final_closure_publish_exit_code": _coerce_int( + payload["release_final_closure_publish_exit_code"], + field="release_final_closure_publish_exit_code", + path=path, + ), + "release_final_archive_status": _coerce_str( + payload["release_final_archive_status"], + field="release_final_archive_status", + path=path, + ), + "release_final_archive_decision": _coerce_str( + payload["release_final_archive_decision"], + field="release_final_archive_decision", + path=path, + ), + "release_final_archive_exit_code": _coerce_int( + payload["release_final_archive_exit_code"], + field="release_final_archive_exit_code", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_verdict_should_ship": _coerce_bool( + payload["final_verdict_should_ship"], + field="final_verdict_should_ship", + path=path, + ), + "final_verdict_requires_manual_action": _coerce_bool( + payload["final_verdict_requires_manual_action"], + field="final_verdict_requires_manual_action", + path=path, + ), + "final_verdict_channel": final_verdict_channel, + "release_final_verdict_status": release_final_verdict_status, + "release_final_verdict_decision": release_final_verdict_decision, + "release_final_verdict_exit_code": release_final_verdict_exit_code, + "release_final_verdict_summary": _coerce_str( + payload["release_final_verdict_summary"], + field="release_final_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_release_final_verdict_publish_payload( + release_final_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_verdict_status = str(release_final_verdict_report["release_final_verdict_status"]) + final_verdict_decision = str(release_final_verdict_report["release_final_verdict_decision"]) + final_verdict_exit_code = int(release_final_verdict_report["release_final_verdict_exit_code"]) + final_verdict_should_ship = bool(release_final_verdict_report["final_verdict_should_ship"]) + final_verdict_requires_manual_action = bool( + release_final_verdict_report["final_verdict_requires_manual_action"] + ) + final_verdict_channel = str(release_final_verdict_report["final_verdict_channel"]) + + reason_codes = list(release_final_verdict_report["reason_codes"]) + structural_issues = list(release_final_verdict_report["structural_issues"]) + missing_artifacts = list(release_final_verdict_report["missing_artifacts"]) + evidence_manifest = list(release_final_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_final_verdict_publish_evidence_missing") + + expected = { + "released": ( + "ship_release", + 0, + True, + False, + "release", + ), + "released_with_follow_up": ( + "ship_release_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "escalate_release_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_verdict", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_ship, + expected_requires_manual_action, + expected_channel, + ) = expected[final_verdict_status] + + if final_verdict_decision != expected_decision: + structural_issues.append("final_verdict_decision_mismatch") + if final_verdict_should_ship != expected_should_ship: + structural_issues.append("final_verdict_should_ship_mismatch") + if final_verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append("final_verdict_requires_manual_action_mismatch") + if final_verdict_channel != expected_channel: + structural_issues.append("final_verdict_channel_mismatch") + if expected_exit_code == 0 and final_verdict_exit_code != 0: + structural_issues.append("final_verdict_exit_code_mismatch_released") + if expected_exit_code is None and final_verdict_exit_code == 0: + structural_issues.append("final_verdict_exit_code_mismatch_non_released") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_verdict_status == "released": + publish_status = "published" + publish_decision = "announce_release_shipped" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["release_final_verdict_publish_published"] + elif final_verdict_status == "released_with_follow_up": + publish_status = "published_with_follow_up" + publish_decision = "announce_release_shipped_with_follow_up" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("release_final_verdict_publish_with_follow_up") + elif final_verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_release_blocker" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("release_final_verdict_publish_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("release_final_verdict_publish_upstream_contract_failed") + + if publish_status not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError("internal: unsupported release_final_verdict_publish_status computed") + if publish_decision not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported release_final_verdict_publish_decision computed") + if publish_channel not in ALLOWED_RELEASE_FINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported release_final_verdict_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_verdict_status={final_verdict_status} " + f"release_final_verdict_publish_status={publish_status} " + f"release_final_verdict_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_verdict_report": str(source_path), + "source_release_final_archive_report": str( + release_final_verdict_report["source_release_final_archive_report"] + ), + "source_release_final_closure_publish_report": str( + release_final_verdict_report["source_release_final_closure_publish_report"] + ), + "source_release_final_closure_report": str( + release_final_verdict_report["source_release_final_closure_report"] + ), + "source_release_final_handoff_report": str( + release_final_verdict_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_verdict_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_verdict_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_verdict_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_verdict_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_verdict_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_verdict_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_verdict_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_verdict_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_verdict_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_verdict_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_verdict_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_verdict_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_verdict_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_verdict_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_verdict_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_verdict_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str(release_final_verdict_report["release_final_handoff_status"]), + "release_final_handoff_decision": str( + release_final_verdict_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_verdict_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": str(release_final_verdict_report["release_final_closure_status"]), + "release_final_closure_decision": str( + release_final_verdict_report["release_final_closure_decision"] + ), + "release_final_closure_exit_code": int( + release_final_verdict_report["release_final_closure_exit_code"] + ), + "release_final_closure_publish_status": str( + release_final_verdict_report["release_final_closure_publish_status"] + ), + "release_final_closure_publish_decision": str( + release_final_verdict_report["release_final_closure_publish_decision"] + ), + "release_final_closure_publish_exit_code": int( + release_final_verdict_report["release_final_closure_publish_exit_code"] + ), + "release_final_archive_status": str(release_final_verdict_report["release_final_archive_status"]), + "release_final_archive_decision": str(release_final_verdict_report["release_final_archive_decision"]), + "release_final_archive_exit_code": int(release_final_verdict_report["release_final_archive_exit_code"]), + "release_final_verdict_status": final_verdict_status, + "release_final_verdict_decision": final_verdict_decision, + "release_final_verdict_exit_code": final_verdict_exit_code, + "release_run_id": release_final_verdict_report["release_run_id"], + "release_run_url": str(release_final_verdict_report["release_run_url"]), + "follow_up_queue_url": str(release_final_verdict_report["follow_up_queue_url"]), + "final_verdict_publish_should_notify": publish_should_notify, + "final_verdict_publish_requires_manual_action": publish_requires_manual_action, + "final_verdict_publish_channel": publish_channel, + "release_final_verdict_publish_status": publish_status, + "release_final_verdict_publish_decision": publish_decision, + "release_final_verdict_publish_exit_code": publish_exit_code, + "release_final_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Final Verdict Publish Report", + "", + ( + "- Release Final Verdict Publish Status: " + f"**{str(payload['release_final_verdict_publish_status']).upper()}**" + ), + ( + "- Release Final Verdict Publish Decision: " + f"`{payload['release_final_verdict_publish_decision']}`" + ), + ( + "- Release Final Verdict Publish Exit Code: " + f"`{payload['release_final_verdict_publish_exit_code']}`" + ), + ( + "- Final Verdict Publish Should Notify: " + f"`{payload['final_verdict_publish_should_notify']}`" + ), + ( + "- Final Verdict Publish Requires Manual Action: " + f"`{payload['final_verdict_publish_requires_manual_action']}`" + ), + f"- Final Verdict Publish Channel: `{payload['final_verdict_publish_channel']}`", + f"- Release Final Verdict Status: `{payload['release_final_verdict_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_final_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_final_verdict_publish_status": str( + payload["release_final_verdict_publish_status"] + ), + "workflow_release_final_verdict_publish_decision": str( + payload["release_final_verdict_publish_decision"] + ), + "workflow_release_final_verdict_publish_exit_code": str( + payload["release_final_verdict_publish_exit_code"] + ), + "workflow_release_final_verdict_publish_should_notify": ( + "true" if payload["final_verdict_publish_should_notify"] else "false" + ), + "workflow_release_final_verdict_publish_requires_manual_action": ( + "true" if payload["final_verdict_publish_requires_manual_action"] else "false" + ), + "workflow_release_final_verdict_publish_channel": str( + payload["final_verdict_publish_channel"] + ), + "workflow_release_final_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_release_final_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_final_verdict_publish_run_url": str(payload["release_run_url"]), + "workflow_release_final_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_final_verdict_publish_report_json": str(output_json), + "workflow_release_final_verdict_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release final verdict publish contract " + "from P2-54 release final verdict report" + ) + ) + parser.add_argument( + "--release-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json", + help="P2-54 release final verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json", + help="Output release final verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md", + help="Output release final verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release final verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_final_verdict_report_path = Path(args.release_final_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_final_verdict_report = load_release_final_verdict_report( + release_final_verdict_report_path + ) + payload = build_release_final_verdict_publish_payload( + release_final_verdict_report, + source_path=release_final_verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-release-final-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release final verdict publish json: {output_json_path}") + print(f"release final verdict publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release final verdict publish summary: " + f"release_final_verdict_publish_status={payload['release_final_verdict_publish_status']} " + f"release_final_verdict_publish_decision={payload['release_final_verdict_publish_decision']} " + f"release_final_verdict_publish_exit_code={payload['release_final_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_final_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_finalization_gate.py b/scripts/run_p2_linux_ci_workflow_release_finalization_gate.py new file mode 100644 index 0000000..89842b0 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_finalization_gate.py @@ -0,0 +1,566 @@ +"""Phase 2 card P2-35 gate for Linux CI workflow release finalization. + +This script consumes the P2-34 release terminal publish artifact and converges +an explicit finalization contract: +1) validate release terminal publish contract consistency, +2) normalize finalization decision (finalize/hold/abort), +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_PUBLISH_STATUSES: set[str] = { + "passed", + "blocked", + "in_progress", + "failed", + "contract_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release terminal publish report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release terminal publish payload must be object") + + required_fields = ( + "release_trigger_status", + "release_tracking_status", + "release_completion_status", + "release_completion_exit_code", + "release_publish_status", + "release_publish_exit_code", + "should_finalize_release", + "release_terminal_publish_summary", + "allow_in_progress", + "should_poll_release_run", + "release_run_id", + "release_run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_release_completion_report", + "source_release_trace_report", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_trigger_status = _coerce_str( + payload["release_trigger_status"], + field="release_trigger_status", + path=path, + ) + release_tracking_status = _coerce_str( + payload["release_tracking_status"], + field="release_tracking_status", + path=path, + ) + release_completion_status = _coerce_str( + payload["release_completion_status"], + field="release_completion_status", + path=path, + ) + release_completion_exit_code = _coerce_int( + payload["release_completion_exit_code"], + field="release_completion_exit_code", + path=path, + ) + if release_completion_exit_code < 0: + raise ValueError(f"{path}: field 'release_completion_exit_code' must be >= 0") + + release_publish_status = _coerce_str( + payload["release_publish_status"], + field="release_publish_status", + path=path, + ) + if release_publish_status not in ALLOWED_RELEASE_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_PUBLISH_STATUSES)}" + ) + + release_publish_exit_code = _coerce_int( + payload["release_publish_exit_code"], + field="release_publish_exit_code", + path=path, + ) + if release_publish_exit_code < 0: + raise ValueError(f"{path}: field 'release_publish_exit_code' must be >= 0") + + should_finalize_release = _coerce_bool( + payload["should_finalize_release"], + field="should_finalize_release", + path=path, + ) + release_terminal_publish_summary = _coerce_str( + payload["release_terminal_publish_summary"], + field="release_terminal_publish_summary", + path=path, + ) + allow_in_progress = _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ) + should_poll_release_run = _coerce_bool( + payload["should_poll_release_run"], + field="should_poll_release_run", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_release_completion_report = _coerce_str( + payload["source_release_completion_report"], + field="source_release_completion_report", + path=path, + ) + source_release_trace_report = _coerce_str( + payload["source_release_trace_report"], + field="source_release_trace_report", + path=path, + ) + source_release_trigger_report = _coerce_str( + payload["source_release_trigger_report"], + field="source_release_trigger_report", + path=path, + ) + source_release_handoff_report = _coerce_str( + payload["source_release_handoff_report"], + field="source_release_handoff_report", + path=path, + ) + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "release_trigger_status": release_trigger_status, + "release_tracking_status": release_tracking_status, + "release_completion_status": release_completion_status, + "release_completion_exit_code": release_completion_exit_code, + "release_publish_status": release_publish_status, + "release_publish_exit_code": release_publish_exit_code, + "should_finalize_release": should_finalize_release, + "release_terminal_publish_summary": release_terminal_publish_summary, + "allow_in_progress": allow_in_progress, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_release_completion_report": source_release_completion_report, + "source_release_trace_report": source_release_trace_report, + "source_release_trigger_report": source_release_trigger_report, + "source_release_handoff_report": source_release_handoff_report, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_release_finalization_payload( + release_terminal_publish_report: dict[str, Any], + *, + source_path: Path, + hold_policy: str, +) -> dict[str, Any]: + release_publish_status = str(release_terminal_publish_report["release_publish_status"]) + release_publish_exit_code = int(release_terminal_publish_report["release_publish_exit_code"]) + should_finalize_release = bool(release_terminal_publish_report["should_finalize_release"]) + + structural_issues = list(release_terminal_publish_report["structural_issues"]) + reason_codes = list(release_terminal_publish_report["reason_codes"]) + + if release_publish_status == "passed": + if release_publish_exit_code != 0: + structural_issues.append("release_publish_exit_code_mismatch_passed") + if not should_finalize_release: + structural_issues.append("should_finalize_release_mismatch_passed") + else: + if should_finalize_release: + structural_issues.append("should_finalize_release_mismatch_non_passed") + + if release_publish_status in {"blocked", "in_progress"} and release_publish_exit_code != 0: + structural_issues.append("release_publish_exit_code_mismatch_hold") + if release_publish_status in {"failed", "contract_failed"} and release_publish_exit_code == 0: + structural_issues.append("release_publish_exit_code_mismatch_failed") + + structural_issues = _unique(structural_issues) + + if structural_issues: + release_finalization_status = "contract_failed" + release_finalization_decision = "abort" + finalization_should_finalize_release = False + release_finalization_exit_code = 1 + reason_codes.extend(structural_issues) + elif release_publish_status == "passed": + release_finalization_status = "finalized" + release_finalization_decision = "finalize" + finalization_should_finalize_release = True + release_finalization_exit_code = 0 + reason_codes = ["release_finalized"] + elif release_publish_status in {"blocked", "in_progress"}: + release_finalization_status = "awaiting_release" + release_finalization_decision = "hold" + finalization_should_finalize_release = False + release_finalization_exit_code = 0 if hold_policy == "pass" else 1 + if hold_policy == "fail": + reason_codes.append("on_hold_policy_fail") + else: + release_finalization_status = "failed" + release_finalization_decision = "abort" + finalization_should_finalize_release = False + release_finalization_exit_code = ( + 1 if release_publish_exit_code == 0 else release_publish_exit_code + ) + + release_finalization_summary = ( + f"release_publish_status={release_publish_status} " + f"release_finalization_status={release_finalization_status} " + f"release_finalization_decision={release_finalization_decision} " + f"on_hold_policy={hold_policy}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_terminal_publish_report": str(source_path), + "release_trigger_status": str(release_terminal_publish_report["release_trigger_status"]), + "release_tracking_status": str(release_terminal_publish_report["release_tracking_status"]), + "release_completion_status": str( + release_terminal_publish_report["release_completion_status"] + ), + "release_completion_exit_code": int( + release_terminal_publish_report["release_completion_exit_code"] + ), + "release_publish_status": release_publish_status, + "release_publish_exit_code": release_publish_exit_code, + "release_finalization_status": release_finalization_status, + "release_finalization_decision": release_finalization_decision, + "release_finalization_exit_code": int(release_finalization_exit_code), + "should_finalize_release": finalization_should_finalize_release, + "on_hold_policy": hold_policy, + "allow_in_progress": bool(release_terminal_publish_report["allow_in_progress"]), + "should_poll_release_run": bool(release_terminal_publish_report["should_poll_release_run"]), + "release_run_id": release_terminal_publish_report["release_run_id"], + "release_run_url": str(release_terminal_publish_report["release_run_url"]), + "release_finalization_summary": release_finalization_summary, + "reason_codes": _unique(reason_codes), + "failed_checks": list(release_terminal_publish_report["failed_checks"]), + "structural_issues": structural_issues, + "missing_artifacts": list(release_terminal_publish_report["missing_artifacts"]), + "source_release_completion_report": str( + release_terminal_publish_report["source_release_completion_report"] + ), + "source_release_trace_report": str( + release_terminal_publish_report["source_release_trace_report"] + ), + "source_release_trigger_report": str( + release_terminal_publish_report["source_release_trigger_report"] + ), + "source_release_handoff_report": str( + release_terminal_publish_report["source_release_handoff_report"] + ), + "source_terminal_publish_report": str( + release_terminal_publish_report["source_terminal_publish_report"] + ), + "source_dispatch_completion_report": str( + release_terminal_publish_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str( + release_terminal_publish_report["source_dispatch_trace_report"] + ), + "source_dispatch_execution_report": str( + release_terminal_publish_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(release_terminal_publish_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Finalization Report", + "", + f"- Release Finalization Status: **{str(payload['release_finalization_status']).upper()}**", + f"- Release Finalization Decision: `{payload['release_finalization_decision']}`", + f"- Should Finalize Release: `{payload['should_finalize_release']}`", + f"- Release Finalization Exit Code: `{payload['release_finalization_exit_code']}`", + f"- Release Publish Status: `{payload['release_publish_status']}`", + f"- Release Publish Exit Code: `{payload['release_publish_exit_code']}`", + f"- Release Completion Status: `{payload['release_completion_status']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- On Hold Policy: `{payload['on_hold_policy']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Terminal Publish Report: `{payload['source_release_terminal_publish_report']}`", + f"- Source Release Completion Report: `{payload['source_release_completion_report']}`", + f"- Source Release Trace Report: `{payload['source_release_trace_report']}`", + f"- Source Release Trigger Report: `{payload['source_release_trigger_report']}`", + "", + "## Summary", + "", + f"- {payload['release_finalization_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_finalization_status": str(payload["release_finalization_status"]), + "workflow_release_finalization_decision": str(payload["release_finalization_decision"]), + "workflow_release_finalization_should_finalize_release": ( + "true" if payload["should_finalize_release"] else "false" + ), + "workflow_release_finalization_exit_code": str(payload["release_finalization_exit_code"]), + "workflow_release_finalization_publish_status": str(payload["release_publish_status"]), + "workflow_release_finalization_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_finalization_run_url": str(payload["release_run_url"]), + "workflow_release_finalization_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_finalization_report_json": str(output_json), + "workflow_release_finalization_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Finalize Linux CI workflow release lifecycle from P2-34 terminal publish report" + ) + ) + parser.add_argument( + "--release-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json", + help="P2-34 release terminal publish report JSON path", + ) + parser.add_argument( + "--on-hold", + choices=("pass", "fail"), + default="pass", + help="Exit policy when release is blocked/in_progress (hold path)", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_finalization.json", + help="Output release finalization JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_finalization.md", + help="Output release finalization markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release finalization JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_terminal_publish_report_path = Path(args.release_terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_terminal_publish_report = load_release_terminal_publish_report( + release_terminal_publish_report_path + ) + payload = build_release_finalization_payload( + release_terminal_publish_report, + source_path=release_terminal_publish_report_path.resolve(), + hold_policy=args.on_hold, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-finalization-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release finalization json: {output_json_path}") + print(f"release finalization markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release finalization summary: " + f"release_finalization_status={payload['release_finalization_status']} " + f"release_finalization_decision={payload['release_finalization_decision']} " + f"release_finalization_exit_code={payload['release_finalization_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_finalization_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py b/scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py new file mode 100644 index 0000000..83aa91f --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py @@ -0,0 +1,1174 @@ +"""Phase 2 card P2-45 gate for Linux CI workflow release follow-up closure. + +This script consumes the P2-44 release follow-up dispatch artifact and +converges the follow-up closure contract: +1) validate follow-up dispatch + evidence consistency, +2) optionally execute follow-up/escalation queue command, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: set[str] = { + "closed", + "follow_up_required", + "escalated", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: set[str] = { + "no_action", + "dispatch_follow_up", + "dispatch_escalation", + "abort_dispatch", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: set[str] = { + "none", + "follow_up_queue", + "incident_commander", +} + +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: set[str] = { + "closed", + "queued_dry_run", + "queued", + "queue_failed", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: set[str] = { + "no_action", + "queue_follow_up", + "queue_escalation", + "abort_queue", +} + +URL_PATTERN = re.compile(r"https?://[^\s]+") + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _extract_url(text: str) -> str: + for line in text.splitlines(): + match = URL_PATTERN.search(line) + if match: + return match.group(0) + return "" + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_follow_up_dispatch_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release follow-up dispatch report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release follow-up dispatch payload must be object") + + required_fields = ( + "source_release_delivery_final_verdict_report", + "source_release_delivery_terminal_publish_report", + "source_release_delivery_report", + "source_release_terminal_verdict_report", + "source_release_incident_report", + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_terminal_verdict_status", + "release_terminal_verdict_decision", + "release_terminal_verdict_exit_code", + "terminal_should_ship_release", + "terminal_requires_follow_up", + "terminal_incident_linked", + "release_delivery_status", + "release_delivery_decision", + "release_delivery_exit_code", + "delivery_should_ship_release", + "delivery_requires_human_action", + "delivery_should_announce_blocker", + "release_delivery_terminal_publish_status", + "release_delivery_terminal_publish_decision", + "release_delivery_terminal_publish_exit_code", + "terminal_publish_should_notify", + "terminal_publish_should_create_follow_up", + "terminal_publish_channel", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "final_should_close_release", + "final_should_open_follow_up", + "final_should_page_owner", + "final_announcement_target", + "release_follow_up_dispatch_status", + "release_follow_up_dispatch_decision", + "release_follow_up_dispatch_exit_code", + "follow_up_required", + "escalation_required", + "dispatch_target", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + dispatch_status = _coerce_str( + payload["release_follow_up_dispatch_status"], + field="release_follow_up_dispatch_status", + path=path, + ) + if dispatch_status not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES)}" + ) + + dispatch_decision = _coerce_str( + payload["release_follow_up_dispatch_decision"], + field="release_follow_up_dispatch_decision", + path=path, + ) + if dispatch_decision not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS)}" + ) + + dispatch_exit_code = _coerce_int( + payload["release_follow_up_dispatch_exit_code"], + field="release_follow_up_dispatch_exit_code", + path=path, + ) + if dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'release_follow_up_dispatch_exit_code' must be >= 0") + + dispatch_target = _coerce_str(payload["dispatch_target"], field="dispatch_target", path=path) + if dispatch_target not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: + raise ValueError( + f"{path}: field 'dispatch_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_delivery_terminal_publish_report": _coerce_str( + payload["source_release_delivery_terminal_publish_report"], + field="source_release_delivery_terminal_publish_report", + path=path, + ), + "source_release_delivery_report": _coerce_str( + payload["source_release_delivery_report"], + field="source_release_delivery_report", + path=path, + ), + "source_release_terminal_verdict_report": _coerce_str( + payload["source_release_terminal_verdict_report"], + field="source_release_terminal_verdict_report", + path=path, + ), + "source_release_incident_report": _coerce_str( + payload["source_release_incident_report"], + field="source_release_incident_report", + path=path, + ), + "source_release_verdict_report": _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ), + "source_release_archive_report": _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ), + "release_archive_status": _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ), + "release_archive_decision": _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ), + "release_archive_exit_code": _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ), + "should_publish_archive": _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ), + "release_verdict_status": _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ), + "release_verdict_decision": _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ), + "release_verdict_exit_code": _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ), + "should_ship_release": _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ), + "should_open_incident": _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ), + "should_dispatch_incident": _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ), + "incident_dispatch_status": _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ), + "incident_dispatch_exit_code": _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ), + "incident_dispatch_attempted": _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ), + "incident_url": _coerce_str(payload["incident_url"], field="incident_url", path=path), + "release_terminal_verdict_status": _coerce_str( + payload["release_terminal_verdict_status"], + field="release_terminal_verdict_status", + path=path, + ), + "release_terminal_verdict_decision": _coerce_str( + payload["release_terminal_verdict_decision"], + field="release_terminal_verdict_decision", + path=path, + ), + "release_terminal_verdict_exit_code": _coerce_int( + payload["release_terminal_verdict_exit_code"], + field="release_terminal_verdict_exit_code", + path=path, + ), + "terminal_should_ship_release": _coerce_bool( + payload["terminal_should_ship_release"], + field="terminal_should_ship_release", + path=path, + ), + "terminal_requires_follow_up": _coerce_bool( + payload["terminal_requires_follow_up"], + field="terminal_requires_follow_up", + path=path, + ), + "terminal_incident_linked": _coerce_bool( + payload["terminal_incident_linked"], + field="terminal_incident_linked", + path=path, + ), + "release_delivery_status": _coerce_str( + payload["release_delivery_status"], + field="release_delivery_status", + path=path, + ), + "release_delivery_decision": _coerce_str( + payload["release_delivery_decision"], + field="release_delivery_decision", + path=path, + ), + "release_delivery_exit_code": _coerce_int( + payload["release_delivery_exit_code"], + field="release_delivery_exit_code", + path=path, + ), + "delivery_should_ship_release": _coerce_bool( + payload["delivery_should_ship_release"], + field="delivery_should_ship_release", + path=path, + ), + "delivery_requires_human_action": _coerce_bool( + payload["delivery_requires_human_action"], + field="delivery_requires_human_action", + path=path, + ), + "delivery_should_announce_blocker": _coerce_bool( + payload["delivery_should_announce_blocker"], + field="delivery_should_announce_blocker", + path=path, + ), + "release_delivery_terminal_publish_status": _coerce_str( + payload["release_delivery_terminal_publish_status"], + field="release_delivery_terminal_publish_status", + path=path, + ), + "release_delivery_terminal_publish_decision": _coerce_str( + payload["release_delivery_terminal_publish_decision"], + field="release_delivery_terminal_publish_decision", + path=path, + ), + "release_delivery_terminal_publish_exit_code": _coerce_int( + payload["release_delivery_terminal_publish_exit_code"], + field="release_delivery_terminal_publish_exit_code", + path=path, + ), + "terminal_publish_should_notify": _coerce_bool( + payload["terminal_publish_should_notify"], + field="terminal_publish_should_notify", + path=path, + ), + "terminal_publish_should_create_follow_up": _coerce_bool( + payload["terminal_publish_should_create_follow_up"], + field="terminal_publish_should_create_follow_up", + path=path, + ), + "terminal_publish_channel": _coerce_str( + payload["terminal_publish_channel"], + field="terminal_publish_channel", + path=path, + ), + "release_delivery_final_verdict_status": _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ), + "release_delivery_final_verdict_decision": _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ), + "release_delivery_final_verdict_exit_code": _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ), + "final_should_close_release": _coerce_bool( + payload["final_should_close_release"], + field="final_should_close_release", + path=path, + ), + "final_should_open_follow_up": _coerce_bool( + payload["final_should_open_follow_up"], + field="final_should_open_follow_up", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_announcement_target": _coerce_str( + payload["final_announcement_target"], + field="final_announcement_target", + path=path, + ), + "release_follow_up_dispatch_status": dispatch_status, + "release_follow_up_dispatch_decision": dispatch_decision, + "release_follow_up_dispatch_exit_code": dispatch_exit_code, + "follow_up_required": _coerce_bool( + payload["follow_up_required"], + field="follow_up_required", + path=path, + ), + "escalation_required": _coerce_bool( + payload["escalation_required"], + field="escalation_required", + path=path, + ), + "dispatch_target": dispatch_target, + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_follow_up_command_parts( + dispatch_report: dict[str, Any], + *, + source_report_path: Path, + gh_executable: str, + follow_up_command: str, + follow_up_repo: str, + follow_up_label: str, + follow_up_title_prefix: str, +) -> list[str]: + if follow_up_command.strip(): + try: + custom_parts = shlex.split(follow_up_command) + except ValueError as exc: + raise ValueError(f"invalid --follow-up-command ({exc})") from exc + if not custom_parts: + raise ValueError("invalid --follow-up-command (empty command)") + return custom_parts + + dispatch_status = str(dispatch_report["release_follow_up_dispatch_status"]) + dispatch_decision = str(dispatch_report["release_follow_up_dispatch_decision"]) + dispatch_target = str(dispatch_report["dispatch_target"]) + release_run_url = str(dispatch_report["release_run_url"]) + reason_codes = list(dispatch_report["reason_codes"]) + reason_codes_text = ", ".join(reason_codes) if reason_codes else "none" + + title = ( + f"{follow_up_title_prefix} release_follow_up_dispatch_status={dispatch_status} " + f"release_follow_up_dispatch_decision={dispatch_decision}" + ) + body = "\n".join( + [ + "Automated follow-up opened by P2-45 release follow-up closure gate.", + "", + f"- release_follow_up_dispatch_status: {dispatch_status}", + f"- release_follow_up_dispatch_decision: {dispatch_decision}", + f"- release_follow_up_dispatch_exit_code: {dispatch_report['release_follow_up_dispatch_exit_code']}", + f"- dispatch_target: {dispatch_target}", + f"- release_run_id: {dispatch_report['release_run_id']}", + f"- release_run_url: {release_run_url}", + f"- source_release_follow_up_dispatch_report: {source_report_path}", + f"- reason_codes: {reason_codes_text}", + ] + ) + + parts = [ + gh_executable, + "issue", + "create", + "--title", + title, + "--body", + body, + ] + if follow_up_label.strip(): + parts.extend(["--label", follow_up_label]) + if follow_up_repo.strip(): + parts.extend(["--repo", follow_up_repo]) + return parts + + +def execute_follow_up_command( + command_parts: list[str], *, cwd: Path, timeout_seconds: int +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"follow-up command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_release_follow_up_closure_payload( + dispatch_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + follow_up_repo: str, + follow_up_label: str, + follow_up_title_prefix: str, + follow_up_command_parts: list[str], + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + dispatch_status = str(dispatch_report["release_follow_up_dispatch_status"]) + dispatch_decision = str(dispatch_report["release_follow_up_dispatch_decision"]) + dispatch_exit_code = int(dispatch_report["release_follow_up_dispatch_exit_code"]) + follow_up_required = bool(dispatch_report["follow_up_required"]) + escalation_required = bool(dispatch_report["escalation_required"]) + dispatch_target = str(dispatch_report["dispatch_target"]) + + reason_codes = list(dispatch_report["reason_codes"]) + structural_issues = list(dispatch_report["structural_issues"]) + missing_artifacts = list(dispatch_report["missing_artifacts"]) + evidence_manifest = list(dispatch_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_follow_up_closure_evidence_missing") + + if dispatch_status == "closed": + if dispatch_decision != "no_action": + structural_issues.append("dispatch_decision_mismatch_closed") + if dispatch_exit_code != 0: + structural_issues.append("dispatch_exit_code_mismatch_closed") + if follow_up_required: + structural_issues.append("follow_up_required_mismatch_closed") + if escalation_required: + structural_issues.append("escalation_required_mismatch_closed") + if dispatch_target != "none": + structural_issues.append("dispatch_target_mismatch_closed") + elif dispatch_status == "follow_up_required": + if dispatch_decision != "dispatch_follow_up": + structural_issues.append("dispatch_decision_mismatch_follow_up_required") + if dispatch_exit_code == 0: + structural_issues.append("dispatch_exit_code_mismatch_follow_up_required") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_follow_up_required") + if escalation_required: + structural_issues.append("escalation_required_mismatch_follow_up_required") + if dispatch_target != "follow_up_queue": + structural_issues.append("dispatch_target_mismatch_follow_up_required") + elif dispatch_status == "escalated": + if dispatch_decision != "dispatch_escalation": + structural_issues.append("dispatch_decision_mismatch_escalated") + if dispatch_exit_code == 0: + structural_issues.append("dispatch_exit_code_mismatch_escalated") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_escalated") + if not escalation_required: + structural_issues.append("escalation_required_mismatch_escalated") + if dispatch_target != "incident_commander": + structural_issues.append("dispatch_target_mismatch_escalated") + else: + if dispatch_decision != "abort_dispatch": + structural_issues.append("dispatch_decision_mismatch_contract_failed") + if dispatch_exit_code == 0: + structural_issues.append("dispatch_exit_code_mismatch_contract_failed") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_contract_failed") + if not escalation_required: + structural_issues.append("escalation_required_mismatch_contract_failed") + if dispatch_target != "incident_commander": + structural_issues.append("dispatch_target_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + reason_codes = _unique(reason_codes) + + follow_up_command = ( + _format_shell_command(follow_up_command_parts) if follow_up_command_parts else "" + ) + should_queue_follow_up = follow_up_required and not structural_issues and not missing_artifacts + follow_up_queue_attempted = False + follow_up_task_queued = False + escalation_task_queued = False + follow_up_queue_url = "" + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + follow_up_error_type = "" + follow_up_error_message = "" + + closure_status = "closed" + closure_decision = "no_action" + closure_exit_code = 0 + + if structural_issues or missing_artifacts: + closure_status = "contract_failed" + closure_decision = "abort_queue" + closure_exit_code = 1 + reason_codes.extend(structural_issues) + elif not should_queue_follow_up: + closure_status = "closed" + closure_decision = "no_action" + closure_exit_code = 0 + if dispatch_status == "closed": + reason_codes = ["release_follow_up_closed_no_action"] + elif dry_run: + closure_status = "queued_dry_run" + closure_decision = ( + "queue_escalation" if escalation_required else "queue_follow_up" + ) + closure_exit_code = 0 + else: + follow_up_queue_attempted = True + if command_result is None: + closure_status = "queue_failed" + closure_decision = "abort_queue" + closure_exit_code = 1 + reason_codes.append("follow_up_command_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + follow_up_error_type = str(command_result.get("error_type", "")) + follow_up_error_message = str(command_result.get("error_message", "")) + + if command_returncode == 0: + closure_status = "queued" + closure_decision = ( + "queue_escalation" if escalation_required else "queue_follow_up" + ) + closure_exit_code = 0 + follow_up_task_queued = True + escalation_task_queued = escalation_required + follow_up_queue_url = _extract_url(command_stdout_tail) + reason_codes = ( + ["release_follow_up_escalation_queued"] + if escalation_required + else ["release_follow_up_queued"] + ) + else: + closure_status = "queue_failed" + closure_decision = "abort_queue" + closure_exit_code = 1 + if follow_up_error_type == "command_not_found": + reason_codes.append("release_follow_up_cli_unavailable") + elif follow_up_error_type == "timeout": + reason_codes.append("release_follow_up_timeout") + else: + reason_codes.append("release_follow_up_command_failed") + + reason_codes = _unique(reason_codes) + + if closure_status not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: + raise ValueError("internal: unsupported follow-up closure status computed") + if closure_decision not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: + raise ValueError("internal: unsupported follow-up closure decision computed") + + closure_summary = ( + f"release_follow_up_dispatch_status={dispatch_status} " + f"release_follow_up_closure_status={closure_status} " + f"should_queue_follow_up={should_queue_follow_up}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_follow_up_dispatch_report": str(source_path), + "project_root": str(project_root), + "source_release_delivery_final_verdict_report": str( + dispatch_report["source_release_delivery_final_verdict_report"] + ), + "source_release_delivery_terminal_publish_report": str( + dispatch_report["source_release_delivery_terminal_publish_report"] + ), + "source_release_delivery_report": str(dispatch_report["source_release_delivery_report"]), + "source_release_terminal_verdict_report": str( + dispatch_report["source_release_terminal_verdict_report"] + ), + "source_release_incident_report": str(dispatch_report["source_release_incident_report"]), + "source_release_verdict_report": str(dispatch_report["source_release_verdict_report"]), + "source_release_archive_report": str(dispatch_report["source_release_archive_report"]), + "release_archive_status": str(dispatch_report["release_archive_status"]), + "release_archive_decision": str(dispatch_report["release_archive_decision"]), + "release_archive_exit_code": int(dispatch_report["release_archive_exit_code"]), + "should_publish_archive": bool(dispatch_report["should_publish_archive"]), + "release_verdict_status": str(dispatch_report["release_verdict_status"]), + "release_verdict_decision": str(dispatch_report["release_verdict_decision"]), + "release_verdict_exit_code": int(dispatch_report["release_verdict_exit_code"]), + "should_ship_release": bool(dispatch_report["should_ship_release"]), + "should_open_incident": bool(dispatch_report["should_open_incident"]), + "should_dispatch_incident": bool(dispatch_report["should_dispatch_incident"]), + "incident_dispatch_status": str(dispatch_report["incident_dispatch_status"]), + "incident_dispatch_exit_code": int(dispatch_report["incident_dispatch_exit_code"]), + "incident_dispatch_attempted": bool(dispatch_report["incident_dispatch_attempted"]), + "incident_url": str(dispatch_report["incident_url"]), + "release_terminal_verdict_status": str(dispatch_report["release_terminal_verdict_status"]), + "release_terminal_verdict_decision": str( + dispatch_report["release_terminal_verdict_decision"] + ), + "release_terminal_verdict_exit_code": int( + dispatch_report["release_terminal_verdict_exit_code"] + ), + "terminal_should_ship_release": bool(dispatch_report["terminal_should_ship_release"]), + "terminal_requires_follow_up": bool(dispatch_report["terminal_requires_follow_up"]), + "terminal_incident_linked": bool(dispatch_report["terminal_incident_linked"]), + "release_delivery_status": str(dispatch_report["release_delivery_status"]), + "release_delivery_decision": str(dispatch_report["release_delivery_decision"]), + "release_delivery_exit_code": int(dispatch_report["release_delivery_exit_code"]), + "delivery_should_ship_release": bool(dispatch_report["delivery_should_ship_release"]), + "delivery_requires_human_action": bool(dispatch_report["delivery_requires_human_action"]), + "delivery_should_announce_blocker": bool( + dispatch_report["delivery_should_announce_blocker"] + ), + "release_delivery_terminal_publish_status": str( + dispatch_report["release_delivery_terminal_publish_status"] + ), + "release_delivery_terminal_publish_decision": str( + dispatch_report["release_delivery_terminal_publish_decision"] + ), + "release_delivery_terminal_publish_exit_code": int( + dispatch_report["release_delivery_terminal_publish_exit_code"] + ), + "terminal_publish_should_notify": bool(dispatch_report["terminal_publish_should_notify"]), + "terminal_publish_should_create_follow_up": bool( + dispatch_report["terminal_publish_should_create_follow_up"] + ), + "terminal_publish_channel": str(dispatch_report["terminal_publish_channel"]), + "release_delivery_final_verdict_status": str( + dispatch_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + dispatch_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + dispatch_report["release_delivery_final_verdict_exit_code"] + ), + "final_should_close_release": bool(dispatch_report["final_should_close_release"]), + "final_should_open_follow_up": bool(dispatch_report["final_should_open_follow_up"]), + "final_should_page_owner": bool(dispatch_report["final_should_page_owner"]), + "final_announcement_target": str(dispatch_report["final_announcement_target"]), + "release_follow_up_dispatch_status": dispatch_status, + "release_follow_up_dispatch_decision": dispatch_decision, + "release_follow_up_dispatch_exit_code": dispatch_exit_code, + "follow_up_required": follow_up_required, + "escalation_required": escalation_required, + "dispatch_target": dispatch_target, + "should_queue_follow_up": should_queue_follow_up, + "release_follow_up_closure_status": closure_status, + "release_follow_up_closure_decision": closure_decision, + "release_follow_up_closure_exit_code": closure_exit_code, + "follow_up_queue_attempted": follow_up_queue_attempted, + "follow_up_task_queued": follow_up_task_queued, + "escalation_task_queued": escalation_task_queued, + "follow_up_command": follow_up_command, + "follow_up_command_parts": list(follow_up_command_parts), + "follow_up_command_returncode": command_returncode, + "follow_up_command_stdout_tail": command_stdout_tail, + "follow_up_command_stderr_tail": command_stderr_tail, + "follow_up_error_type": follow_up_error_type, + "follow_up_error_message": follow_up_error_message, + "follow_up_repo": follow_up_repo, + "follow_up_label": follow_up_label, + "follow_up_title_prefix": follow_up_title_prefix, + "follow_up_queue_url": follow_up_queue_url, + "release_run_id": dispatch_report["release_run_id"], + "release_run_url": str(dispatch_report["release_run_url"]), + "release_follow_up_closure_summary": closure_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Follow-Up Closure Report", + "", + ( + "- Release Follow-Up Closure Status: " + f"**{str(payload['release_follow_up_closure_status']).upper()}**" + ), + ( + "- Release Follow-Up Closure Decision: " + f"`{payload['release_follow_up_closure_decision']}`" + ), + ( + "- Release Follow-Up Closure Exit Code: " + f"`{payload['release_follow_up_closure_exit_code']}`" + ), + f"- Should Queue Follow Up: `{payload['should_queue_follow_up']}`", + f"- Follow Up Queue Attempted: `{payload['follow_up_queue_attempted']}`", + f"- Follow Up Task Queued: `{payload['follow_up_task_queued']}`", + f"- Escalation Task Queued: `{payload['escalation_task_queued']}`", + f"- Dispatch Target: `{payload['dispatch_target']}`", + f"- Follow Up Queue URL: `{payload['follow_up_queue_url']}`", + ( + "- Release Follow-Up Dispatch Status: " + f"`{payload['release_follow_up_dispatch_status']}`" + ), + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Follow-Up Command", + ] + + follow_up_command = str(payload["follow_up_command"]) + if follow_up_command: + lines.append(f"- `{follow_up_command}`") + else: + lines.append("- none") + + lines.extend(["", "## Summary", "", f"- {payload['release_follow_up_closure_summary']}", "", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_follow_up_closure_status": str(payload["release_follow_up_closure_status"]), + "workflow_release_follow_up_closure_decision": str(payload["release_follow_up_closure_decision"]), + "workflow_release_follow_up_closure_exit_code": str(payload["release_follow_up_closure_exit_code"]), + "workflow_release_follow_up_closure_should_queue": ( + "true" if payload["should_queue_follow_up"] else "false" + ), + "workflow_release_follow_up_closure_attempted": ( + "true" if payload["follow_up_queue_attempted"] else "false" + ), + "workflow_release_follow_up_closure_follow_up_queued": ( + "true" if payload["follow_up_task_queued"] else "false" + ), + "workflow_release_follow_up_closure_escalation_queued": ( + "true" if payload["escalation_task_queued"] else "false" + ), + "workflow_release_follow_up_closure_target": str(payload["dispatch_target"]), + "workflow_release_follow_up_closure_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_follow_up_closure_dispatch_status": str( + payload["release_follow_up_dispatch_status"] + ), + "workflow_release_follow_up_closure_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_follow_up_closure_run_url": str(payload["release_run_url"]), + "workflow_release_follow_up_closure_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_follow_up_closure_report_json": str(output_json), + "workflow_release_follow_up_closure_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Run Linux CI workflow release follow-up closure gate " + "from P2-44 release follow-up dispatch report" + ) + ) + parser.add_argument( + "--release-follow-up-dispatch-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json", + help="P2-44 release follow-up dispatch report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for follow-up command execution", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable used when auto-building follow-up command", + ) + parser.add_argument( + "--follow-up-command", + default="", + help="Optional explicit follow-up command string (overrides auto command)", + ) + parser.add_argument( + "--follow-up-repo", + default="", + help="Optional GitHub repo for follow-up creation (OWNER/REPO)", + ) + parser.add_argument( + "--follow-up-label", + default="release-follow-up", + help="Follow-up label forwarded to auto-built command", + ) + parser.add_argument( + "--follow-up-title-prefix", + default="[release-follow-up]", + help="Follow-up title prefix for auto-built command", + ) + parser.add_argument( + "--follow-up-timeout-seconds", + type=int, + default=600, + help="Follow-up command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json", + help="Output release follow-up closure JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md", + help="Output release follow-up closure markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release follow-up closure JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate contract and skip follow-up command execution", + ) + args = parser.parse_args() + + if args.follow_up_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-release-follow-up-closure-gate] " + "--follow-up-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + dispatch_report_path = Path(args.release_follow_up_dispatch_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + dispatch_report = load_release_follow_up_dispatch_report(dispatch_report_path) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-follow-up-closure-gate] {exc}", + file=sys.stderr, + ) + return 2 + + follow_up_command_parts: list[str] = [] + if dispatch_report["follow_up_required"]: + try: + follow_up_command_parts = build_follow_up_command_parts( + dispatch_report, + source_report_path=dispatch_report_path.resolve(), + gh_executable=args.gh_executable, + follow_up_command=args.follow_up_command, + follow_up_repo=args.follow_up_repo, + follow_up_label=args.follow_up_label, + follow_up_title_prefix=args.follow_up_title_prefix, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-follow-up-closure-gate] {exc}", + file=sys.stderr, + ) + return 2 + + command_result: dict[str, Any] | None = None + if dispatch_report["follow_up_required"] and not args.dry_run: + command_result = execute_follow_up_command( + follow_up_command_parts, + cwd=project_root, + timeout_seconds=args.follow_up_timeout_seconds, + ) + + payload = build_release_follow_up_closure_payload( + dispatch_report, + source_path=dispatch_report_path.resolve(), + project_root=project_root.resolve(), + follow_up_repo=args.follow_up_repo, + follow_up_label=args.follow_up_label, + follow_up_title_prefix=args.follow_up_title_prefix, + follow_up_command_parts=follow_up_command_parts, + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release follow-up closure json: {output_json_path}") + print(f"release follow-up closure markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release follow-up closure summary: " + f"release_follow_up_closure_status={payload['release_follow_up_closure_status']} " + f"should_queue_follow_up={payload['should_queue_follow_up']} " + f"release_follow_up_closure_exit_code={payload['release_follow_up_closure_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_follow_up_closure_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py b/scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py new file mode 100644 index 0000000..6602e43 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py @@ -0,0 +1,875 @@ +"""Phase 2 card P2-44 gate for Linux CI workflow release follow-up dispatch. + +This script consumes the P2-43 release delivery final verdict artifact and +converges the follow-up dispatch contract: +1) validate final verdict + evidence consistency, +2) normalize close/follow-up/escalate follow-up dispatch semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: set[str] = { + "close_release", + "open_follow_up", + "escalate_blocker", + "abort_close", +} +ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: set[str] = { + "release", + "hold", + "blocker", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: set[str] = { + "closed", + "follow_up_required", + "escalated", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: set[str] = { + "no_action", + "dispatch_follow_up", + "dispatch_escalation", + "abort_dispatch", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: set[str] = { + "none", + "follow_up_queue", + "incident_commander", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_delivery_final_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release delivery final verdict report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release delivery final verdict payload must be object") + + required_fields = ( + "source_release_delivery_terminal_publish_report", + "source_release_delivery_report", + "source_release_terminal_verdict_report", + "source_release_incident_report", + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_terminal_verdict_status", + "release_terminal_verdict_decision", + "release_terminal_verdict_exit_code", + "terminal_should_ship_release", + "terminal_requires_follow_up", + "terminal_incident_linked", + "release_delivery_status", + "release_delivery_decision", + "release_delivery_exit_code", + "delivery_should_ship_release", + "delivery_requires_human_action", + "delivery_should_announce_blocker", + "release_delivery_terminal_publish_status", + "release_delivery_terminal_publish_decision", + "release_delivery_terminal_publish_exit_code", + "terminal_publish_should_notify", + "terminal_publish_should_create_follow_up", + "terminal_publish_channel", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "final_should_close_release", + "final_should_open_follow_up", + "final_should_page_owner", + "final_announcement_target", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + final_status = _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ) + if final_status not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_STATUSES)}" + ) + + final_decision = _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ) + if final_decision not in ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_delivery_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_VERDICT_DECISIONS)}" + ) + + final_exit_code = _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ) + if final_exit_code < 0: + raise ValueError(f"{path}: field 'release_delivery_final_verdict_exit_code' must be >= 0") + + announcement_target = _coerce_str( + payload["final_announcement_target"], + field="final_announcement_target", + path=path, + ) + if announcement_target not in ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS: + raise ValueError( + f"{path}: field 'final_announcement_target' must be one of " + f"{sorted(ALLOWED_RELEASE_DELIVERY_FINAL_ANNOUNCEMENT_TARGETS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_delivery_terminal_publish_report": _coerce_str( + payload["source_release_delivery_terminal_publish_report"], + field="source_release_delivery_terminal_publish_report", + path=path, + ), + "source_release_delivery_report": _coerce_str( + payload["source_release_delivery_report"], + field="source_release_delivery_report", + path=path, + ), + "source_release_terminal_verdict_report": _coerce_str( + payload["source_release_terminal_verdict_report"], + field="source_release_terminal_verdict_report", + path=path, + ), + "source_release_incident_report": _coerce_str( + payload["source_release_incident_report"], + field="source_release_incident_report", + path=path, + ), + "source_release_verdict_report": _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ), + "source_release_archive_report": _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ), + "release_archive_status": _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ), + "release_archive_decision": _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ), + "release_archive_exit_code": _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ), + "should_publish_archive": _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ), + "release_verdict_status": _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ), + "release_verdict_decision": _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ), + "release_verdict_exit_code": _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ), + "should_ship_release": _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ), + "should_open_incident": _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ), + "should_dispatch_incident": _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ), + "incident_dispatch_status": _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ), + "incident_dispatch_exit_code": _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ), + "incident_dispatch_attempted": _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ), + "incident_url": _coerce_str( + payload["incident_url"], + field="incident_url", + path=path, + ), + "release_terminal_verdict_status": _coerce_str( + payload["release_terminal_verdict_status"], + field="release_terminal_verdict_status", + path=path, + ), + "release_terminal_verdict_decision": _coerce_str( + payload["release_terminal_verdict_decision"], + field="release_terminal_verdict_decision", + path=path, + ), + "release_terminal_verdict_exit_code": _coerce_int( + payload["release_terminal_verdict_exit_code"], + field="release_terminal_verdict_exit_code", + path=path, + ), + "terminal_should_ship_release": _coerce_bool( + payload["terminal_should_ship_release"], + field="terminal_should_ship_release", + path=path, + ), + "terminal_requires_follow_up": _coerce_bool( + payload["terminal_requires_follow_up"], + field="terminal_requires_follow_up", + path=path, + ), + "terminal_incident_linked": _coerce_bool( + payload["terminal_incident_linked"], + field="terminal_incident_linked", + path=path, + ), + "release_delivery_status": _coerce_str( + payload["release_delivery_status"], + field="release_delivery_status", + path=path, + ), + "release_delivery_decision": _coerce_str( + payload["release_delivery_decision"], + field="release_delivery_decision", + path=path, + ), + "release_delivery_exit_code": _coerce_int( + payload["release_delivery_exit_code"], + field="release_delivery_exit_code", + path=path, + ), + "delivery_should_ship_release": _coerce_bool( + payload["delivery_should_ship_release"], + field="delivery_should_ship_release", + path=path, + ), + "delivery_requires_human_action": _coerce_bool( + payload["delivery_requires_human_action"], + field="delivery_requires_human_action", + path=path, + ), + "delivery_should_announce_blocker": _coerce_bool( + payload["delivery_should_announce_blocker"], + field="delivery_should_announce_blocker", + path=path, + ), + "release_delivery_terminal_publish_status": _coerce_str( + payload["release_delivery_terminal_publish_status"], + field="release_delivery_terminal_publish_status", + path=path, + ), + "release_delivery_terminal_publish_decision": _coerce_str( + payload["release_delivery_terminal_publish_decision"], + field="release_delivery_terminal_publish_decision", + path=path, + ), + "release_delivery_terminal_publish_exit_code": _coerce_int( + payload["release_delivery_terminal_publish_exit_code"], + field="release_delivery_terminal_publish_exit_code", + path=path, + ), + "terminal_publish_should_notify": _coerce_bool( + payload["terminal_publish_should_notify"], + field="terminal_publish_should_notify", + path=path, + ), + "terminal_publish_should_create_follow_up": _coerce_bool( + payload["terminal_publish_should_create_follow_up"], + field="terminal_publish_should_create_follow_up", + path=path, + ), + "terminal_publish_channel": _coerce_str( + payload["terminal_publish_channel"], + field="terminal_publish_channel", + path=path, + ), + "release_delivery_final_verdict_status": final_status, + "release_delivery_final_verdict_decision": final_decision, + "release_delivery_final_verdict_exit_code": final_exit_code, + "final_should_close_release": _coerce_bool( + payload["final_should_close_release"], + field="final_should_close_release", + path=path, + ), + "final_should_open_follow_up": _coerce_bool( + payload["final_should_open_follow_up"], + field="final_should_open_follow_up", + path=path, + ), + "final_should_page_owner": _coerce_bool( + payload["final_should_page_owner"], + field="final_should_page_owner", + path=path, + ), + "final_announcement_target": announcement_target, + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_follow_up_dispatch_payload( + final_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_status = str(final_verdict_report["release_delivery_final_verdict_status"]) + final_decision = str(final_verdict_report["release_delivery_final_verdict_decision"]) + final_exit_code = int(final_verdict_report["release_delivery_final_verdict_exit_code"]) + final_should_close_release = bool(final_verdict_report["final_should_close_release"]) + final_should_open_follow_up = bool(final_verdict_report["final_should_open_follow_up"]) + final_should_page_owner = bool(final_verdict_report["final_should_page_owner"]) + final_announcement_target = str(final_verdict_report["final_announcement_target"]) + + terminal_publish_status = str(final_verdict_report["release_delivery_terminal_publish_status"]) + incident_dispatch_status = str(final_verdict_report["incident_dispatch_status"]) + + reason_codes = list(final_verdict_report["reason_codes"]) + structural_issues = list(final_verdict_report["structural_issues"]) + missing_artifacts = list(final_verdict_report["missing_artifacts"]) + evidence_manifest = list(final_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_follow_up_dispatch_evidence_missing") + + if final_status == "completed": + if final_decision != "close_release": + structural_issues.append("final_decision_mismatch_completed") + if final_exit_code != 0: + structural_issues.append("final_exit_code_mismatch_completed") + if not final_should_close_release: + structural_issues.append("final_should_close_release_mismatch_completed") + if final_should_open_follow_up: + structural_issues.append("final_should_open_follow_up_mismatch_completed") + if final_should_page_owner: + structural_issues.append("final_should_page_owner_mismatch_completed") + if final_announcement_target != "release": + structural_issues.append("final_announcement_target_mismatch_completed") + elif final_status == "requires_follow_up": + if final_decision != "open_follow_up": + structural_issues.append("final_decision_mismatch_requires_follow_up") + if final_exit_code == 0: + structural_issues.append("final_exit_code_mismatch_requires_follow_up") + if final_should_close_release: + structural_issues.append("final_should_close_release_mismatch_requires_follow_up") + if not final_should_open_follow_up: + structural_issues.append("final_should_open_follow_up_mismatch_requires_follow_up") + if final_should_page_owner: + structural_issues.append("final_should_page_owner_mismatch_requires_follow_up") + if final_announcement_target != "hold": + structural_issues.append("final_announcement_target_mismatch_requires_follow_up") + elif final_status == "blocked": + if final_decision != "escalate_blocker": + structural_issues.append("final_decision_mismatch_blocked") + if final_exit_code == 0: + structural_issues.append("final_exit_code_mismatch_blocked") + if final_should_close_release: + structural_issues.append("final_should_close_release_mismatch_blocked") + if not final_should_open_follow_up: + structural_issues.append("final_should_open_follow_up_mismatch_blocked") + if not final_should_page_owner: + structural_issues.append("final_should_page_owner_mismatch_blocked") + if final_announcement_target != "blocker": + structural_issues.append("final_announcement_target_mismatch_blocked") + if incident_dispatch_status == "not_required": + structural_issues.append("incident_dispatch_status_mismatch_blocked") + elif final_status == "contract_failed": + if final_decision != "abort_close": + structural_issues.append("final_decision_mismatch_contract_failed") + if final_exit_code == 0: + structural_issues.append("final_exit_code_mismatch_contract_failed") + if final_should_close_release: + structural_issues.append("final_should_close_release_mismatch_contract_failed") + if not final_should_open_follow_up: + structural_issues.append("final_should_open_follow_up_mismatch_contract_failed") + if not final_should_page_owner: + structural_issues.append("final_should_page_owner_mismatch_contract_failed") + if final_announcement_target != "blocker": + structural_issues.append("final_announcement_target_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + dispatch_status = "contract_failed" + dispatch_decision = "abort_dispatch" + dispatch_exit_code = 1 + follow_up_required = True + escalation_required = True + dispatch_target = "incident_commander" + reason_codes.extend(structural_issues) + elif final_status == "completed": + dispatch_status = "closed" + dispatch_decision = "no_action" + dispatch_exit_code = 0 + follow_up_required = False + escalation_required = False + dispatch_target = "none" + reason_codes = ["release_follow_up_dispatch_closed"] + elif final_status == "requires_follow_up": + dispatch_status = "follow_up_required" + dispatch_decision = "dispatch_follow_up" + dispatch_exit_code = max(1, final_exit_code) + follow_up_required = True + escalation_required = False + dispatch_target = "follow_up_queue" + reason_codes.append("release_follow_up_dispatch_required") + else: + dispatch_status = "escalated" + dispatch_decision = "dispatch_escalation" + dispatch_exit_code = max(1, final_exit_code) + follow_up_required = True + escalation_required = True + dispatch_target = "incident_commander" + reason_codes.append("release_follow_up_dispatch_escalated") + + if dispatch_status not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: + raise ValueError("internal: unsupported release_follow_up_dispatch_status computed") + if dispatch_decision not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: + raise ValueError("internal: unsupported release_follow_up_dispatch_decision computed") + if dispatch_target not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: + raise ValueError("internal: unsupported release_follow_up_dispatch_target computed") + + reason_codes = _unique(reason_codes) + release_follow_up_dispatch_summary = ( + f"release_delivery_final_verdict_status={final_status} " + f"release_follow_up_dispatch_status={dispatch_status} " + f"release_follow_up_dispatch_decision={dispatch_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_delivery_final_verdict_report": str(source_path), + "source_release_delivery_terminal_publish_report": str( + final_verdict_report["source_release_delivery_terminal_publish_report"] + ), + "source_release_delivery_report": str(final_verdict_report["source_release_delivery_report"]), + "source_release_terminal_verdict_report": str( + final_verdict_report["source_release_terminal_verdict_report"] + ), + "source_release_incident_report": str(final_verdict_report["source_release_incident_report"]), + "source_release_verdict_report": str(final_verdict_report["source_release_verdict_report"]), + "source_release_archive_report": str(final_verdict_report["source_release_archive_report"]), + "release_archive_status": str(final_verdict_report["release_archive_status"]), + "release_archive_decision": str(final_verdict_report["release_archive_decision"]), + "release_archive_exit_code": int(final_verdict_report["release_archive_exit_code"]), + "should_publish_archive": bool(final_verdict_report["should_publish_archive"]), + "release_verdict_status": str(final_verdict_report["release_verdict_status"]), + "release_verdict_decision": str(final_verdict_report["release_verdict_decision"]), + "release_verdict_exit_code": int(final_verdict_report["release_verdict_exit_code"]), + "should_ship_release": bool(final_verdict_report["should_ship_release"]), + "should_open_incident": bool(final_verdict_report["should_open_incident"]), + "should_dispatch_incident": bool(final_verdict_report["should_dispatch_incident"]), + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": int(final_verdict_report["incident_dispatch_exit_code"]), + "incident_dispatch_attempted": bool(final_verdict_report["incident_dispatch_attempted"]), + "incident_url": str(final_verdict_report["incident_url"]), + "release_terminal_verdict_status": str(final_verdict_report["release_terminal_verdict_status"]), + "release_terminal_verdict_decision": str( + final_verdict_report["release_terminal_verdict_decision"] + ), + "release_terminal_verdict_exit_code": int( + final_verdict_report["release_terminal_verdict_exit_code"] + ), + "terminal_should_ship_release": bool(final_verdict_report["terminal_should_ship_release"]), + "terminal_requires_follow_up": bool(final_verdict_report["terminal_requires_follow_up"]), + "terminal_incident_linked": bool(final_verdict_report["terminal_incident_linked"]), + "release_delivery_status": str(final_verdict_report["release_delivery_status"]), + "release_delivery_decision": str(final_verdict_report["release_delivery_decision"]), + "release_delivery_exit_code": int(final_verdict_report["release_delivery_exit_code"]), + "delivery_should_ship_release": bool(final_verdict_report["delivery_should_ship_release"]), + "delivery_requires_human_action": bool( + final_verdict_report["delivery_requires_human_action"] + ), + "delivery_should_announce_blocker": bool( + final_verdict_report["delivery_should_announce_blocker"] + ), + "release_delivery_terminal_publish_status": terminal_publish_status, + "release_delivery_terminal_publish_decision": str( + final_verdict_report["release_delivery_terminal_publish_decision"] + ), + "release_delivery_terminal_publish_exit_code": int( + final_verdict_report["release_delivery_terminal_publish_exit_code"] + ), + "terminal_publish_should_notify": bool(final_verdict_report["terminal_publish_should_notify"]), + "terminal_publish_should_create_follow_up": bool( + final_verdict_report["terminal_publish_should_create_follow_up"] + ), + "terminal_publish_channel": str(final_verdict_report["terminal_publish_channel"]), + "release_delivery_final_verdict_status": final_status, + "release_delivery_final_verdict_decision": final_decision, + "release_delivery_final_verdict_exit_code": final_exit_code, + "final_should_close_release": final_should_close_release, + "final_should_open_follow_up": final_should_open_follow_up, + "final_should_page_owner": final_should_page_owner, + "final_announcement_target": final_announcement_target, + "release_follow_up_dispatch_status": dispatch_status, + "release_follow_up_dispatch_decision": dispatch_decision, + "release_follow_up_dispatch_exit_code": dispatch_exit_code, + "follow_up_required": follow_up_required, + "escalation_required": escalation_required, + "dispatch_target": dispatch_target, + "release_run_id": final_verdict_report["release_run_id"], + "release_run_url": str(final_verdict_report["release_run_url"]), + "release_follow_up_dispatch_summary": release_follow_up_dispatch_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Follow-Up Dispatch Report", + "", + ( + "- Release Follow-Up Dispatch Status: " + f"**{str(payload['release_follow_up_dispatch_status']).upper()}**" + ), + ( + "- Release Follow-Up Dispatch Decision: " + f"`{payload['release_follow_up_dispatch_decision']}`" + ), + ( + "- Release Follow-Up Dispatch Exit Code: " + f"`{payload['release_follow_up_dispatch_exit_code']}`" + ), + f"- Follow Up Required: `{payload['follow_up_required']}`", + f"- Escalation Required: `{payload['escalation_required']}`", + f"- Dispatch Target: `{payload['dispatch_target']}`", + ( + "- Release Delivery Final Verdict Status: " + f"`{payload['release_delivery_final_verdict_status']}`" + ), + ( + "- Release Delivery Terminal Publish Status: " + f"`{payload['release_delivery_terminal_publish_status']}`" + ), + f"- Incident Dispatch Status: `{payload['incident_dispatch_status']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_follow_up_dispatch_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_follow_up_dispatch_status": str( + payload["release_follow_up_dispatch_status"] + ), + "workflow_release_follow_up_dispatch_decision": str( + payload["release_follow_up_dispatch_decision"] + ), + "workflow_release_follow_up_dispatch_exit_code": str( + payload["release_follow_up_dispatch_exit_code"] + ), + "workflow_release_follow_up_dispatch_follow_up_required": ( + "true" if payload["follow_up_required"] else "false" + ), + "workflow_release_follow_up_dispatch_escalation_required": ( + "true" if payload["escalation_required"] else "false" + ), + "workflow_release_follow_up_dispatch_target": str(payload["dispatch_target"]), + "workflow_release_follow_up_dispatch_final_status": str( + payload["release_delivery_final_verdict_status"] + ), + "workflow_release_follow_up_dispatch_incident_status": str( + payload["incident_dispatch_status"] + ), + "workflow_release_follow_up_dispatch_incident_url": str(payload["incident_url"]), + "workflow_release_follow_up_dispatch_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_follow_up_dispatch_run_url": str(payload["release_run_url"]), + "workflow_release_follow_up_dispatch_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_follow_up_dispatch_report_json": str(output_json), + "workflow_release_follow_up_dispatch_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release follow-up dispatch contract " + "from P2-43 release delivery final verdict report" + ) + ) + parser.add_argument( + "--release-delivery-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json", + help="P2-43 release delivery final verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json", + help="Output release follow-up dispatch JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md", + help="Output release follow-up dispatch markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release follow-up dispatch JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + final_verdict_report_path = Path(args.release_delivery_final_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + final_verdict_report = load_release_delivery_final_verdict_report( + final_verdict_report_path + ) + payload = build_release_follow_up_dispatch_payload( + final_verdict_report, + source_path=final_verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-follow-up-dispatch-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release follow-up dispatch json: {output_json_path}") + print(f"release follow-up dispatch markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release follow-up dispatch summary: " + f"release_follow_up_dispatch_status={payload['release_follow_up_dispatch_status']} " + f"release_follow_up_dispatch_decision={payload['release_follow_up_dispatch_decision']} " + f"release_follow_up_dispatch_exit_code={payload['release_follow_up_dispatch_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_follow_up_dispatch_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py new file mode 100644 index 0000000..f2cb7a5 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py @@ -0,0 +1,950 @@ +"""Phase 2 card P2-47 gate for Linux CI workflow release follow-up final verdict. + +This script consumes the P2-46 release follow-up terminal publish artifact and +converges the final follow-up verdict contract: +1) validate follow-up terminal publish + closure consistency, +2) normalize close/follow-up/escalate/abort semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: set[str] = { + "closed", + "follow_up_required", + "escalated", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: set[str] = { + "no_action", + "dispatch_follow_up", + "dispatch_escalation", + "abort_dispatch", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: set[str] = { + "none", + "follow_up_queue", + "incident_commander", +} +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: set[str] = { + "closed", + "queued_dry_run", + "queued", + "queue_failed", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: set[str] = { + "no_action", + "queue_follow_up", + "queue_escalation", + "abort_queue", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "pending_queue", + "queue_failed", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_closed", + "announce_queued", + "announce_pending_queue", + "announce_queue_failure", + "abort_publish", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: set[str] = { + "completed", + "requires_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: set[str] = { + "close_follow_up", + "keep_follow_up_open", + "escalate_queue_failure", + "abort_close", +} +ALLOWED_RELEASE_FOLLOW_UP_FINAL_ANNOUNCEMENT_TARGETS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_follow_up_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release follow-up terminal publish report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release follow-up terminal publish payload must be object") + + required_fields = ( + "source_release_follow_up_closure_report", + "source_release_follow_up_dispatch_report", + "project_root", + "release_follow_up_dispatch_status", + "release_follow_up_dispatch_decision", + "release_follow_up_dispatch_exit_code", + "follow_up_required", + "escalation_required", + "dispatch_target", + "should_queue_follow_up", + "release_follow_up_closure_status", + "release_follow_up_closure_decision", + "release_follow_up_closure_exit_code", + "follow_up_queue_attempted", + "follow_up_task_queued", + "escalation_task_queued", + "follow_up_command", + "follow_up_command_parts", + "follow_up_command_returncode", + "follow_up_command_stdout_tail", + "follow_up_command_stderr_tail", + "follow_up_error_type", + "follow_up_error_message", + "follow_up_repo", + "follow_up_label", + "follow_up_title_prefix", + "follow_up_queue_url", + "release_run_id", + "release_run_url", + "release_follow_up_terminal_publish_status", + "release_follow_up_terminal_publish_decision", + "release_follow_up_terminal_publish_exit_code", + "follow_up_terminal_publish_should_notify", + "follow_up_terminal_requires_manual_action", + "follow_up_terminal_publish_channel", + "release_follow_up_terminal_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + dispatch_status = _coerce_str( + payload["release_follow_up_dispatch_status"], + field="release_follow_up_dispatch_status", + path=path, + ) + if dispatch_status not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES)}" + ) + + dispatch_decision = _coerce_str( + payload["release_follow_up_dispatch_decision"], + field="release_follow_up_dispatch_decision", + path=path, + ) + if dispatch_decision not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS)}" + ) + + dispatch_exit_code = _coerce_int( + payload["release_follow_up_dispatch_exit_code"], + field="release_follow_up_dispatch_exit_code", + path=path, + ) + if dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'release_follow_up_dispatch_exit_code' must be >= 0") + + dispatch_target = _coerce_str(payload["dispatch_target"], field="dispatch_target", path=path) + if dispatch_target not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: + raise ValueError( + f"{path}: field 'dispatch_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS)}" + ) + + closure_status = _coerce_str( + payload["release_follow_up_closure_status"], + field="release_follow_up_closure_status", + path=path, + ) + if closure_status not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_closure_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES)}" + ) + + closure_decision = _coerce_str( + payload["release_follow_up_closure_decision"], + field="release_follow_up_closure_decision", + path=path, + ) + if closure_decision not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_closure_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS)}" + ) + + closure_exit_code = _coerce_int( + payload["release_follow_up_closure_exit_code"], + field="release_follow_up_closure_exit_code", + path=path, + ) + if closure_exit_code < 0: + raise ValueError(f"{path}: field 'release_follow_up_closure_exit_code' must be >= 0") + + terminal_publish_status = _coerce_str( + payload["release_follow_up_terminal_publish_status"], + field="release_follow_up_terminal_publish_status", + path=path, + ) + if terminal_publish_status not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_STATUSES)}" + ) + + terminal_publish_decision = _coerce_str( + payload["release_follow_up_terminal_publish_decision"], + field="release_follow_up_terminal_publish_decision", + path=path, + ) + if terminal_publish_decision not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_DECISIONS)}" + ) + + terminal_publish_exit_code = _coerce_int( + payload["release_follow_up_terminal_publish_exit_code"], + field="release_follow_up_terminal_publish_exit_code", + path=path, + ) + if terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'release_follow_up_terminal_publish_exit_code' must be >= 0" + ) + + terminal_publish_channel = _coerce_str( + payload["follow_up_terminal_publish_channel"], + field="follow_up_terminal_publish_channel", + path=path, + ) + if terminal_publish_channel not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'follow_up_terminal_publish_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_CHANNELS)}" + ) + + follow_up_command_returncode_raw = payload["follow_up_command_returncode"] + if follow_up_command_returncode_raw is None: + follow_up_command_returncode = None + else: + follow_up_command_returncode = _coerce_int( + follow_up_command_returncode_raw, + field="follow_up_command_returncode", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_follow_up_closure_report": _coerce_str( + payload["source_release_follow_up_closure_report"], + field="source_release_follow_up_closure_report", + path=path, + ), + "source_release_follow_up_dispatch_report": _coerce_str( + payload["source_release_follow_up_dispatch_report"], + field="source_release_follow_up_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "release_follow_up_dispatch_status": dispatch_status, + "release_follow_up_dispatch_decision": dispatch_decision, + "release_follow_up_dispatch_exit_code": dispatch_exit_code, + "follow_up_required": _coerce_bool( + payload["follow_up_required"], + field="follow_up_required", + path=path, + ), + "escalation_required": _coerce_bool( + payload["escalation_required"], + field="escalation_required", + path=path, + ), + "dispatch_target": dispatch_target, + "should_queue_follow_up": _coerce_bool( + payload["should_queue_follow_up"], + field="should_queue_follow_up", + path=path, + ), + "release_follow_up_closure_status": closure_status, + "release_follow_up_closure_decision": closure_decision, + "release_follow_up_closure_exit_code": closure_exit_code, + "follow_up_queue_attempted": _coerce_bool( + payload["follow_up_queue_attempted"], + field="follow_up_queue_attempted", + path=path, + ), + "follow_up_task_queued": _coerce_bool( + payload["follow_up_task_queued"], + field="follow_up_task_queued", + path=path, + ), + "escalation_task_queued": _coerce_bool( + payload["escalation_task_queued"], + field="escalation_task_queued", + path=path, + ), + "follow_up_command": _coerce_str( + payload["follow_up_command"], + field="follow_up_command", + path=path, + ), + "follow_up_command_parts": _coerce_str_list( + payload["follow_up_command_parts"], + field="follow_up_command_parts", + path=path, + ), + "follow_up_command_returncode": follow_up_command_returncode, + "follow_up_command_stdout_tail": _coerce_str( + payload["follow_up_command_stdout_tail"], + field="follow_up_command_stdout_tail", + path=path, + ), + "follow_up_command_stderr_tail": _coerce_str( + payload["follow_up_command_stderr_tail"], + field="follow_up_command_stderr_tail", + path=path, + ), + "follow_up_error_type": _coerce_str( + payload["follow_up_error_type"], + field="follow_up_error_type", + path=path, + ), + "follow_up_error_message": _coerce_str( + payload["follow_up_error_message"], + field="follow_up_error_message", + path=path, + ), + "follow_up_repo": _coerce_str(payload["follow_up_repo"], field="follow_up_repo", path=path), + "follow_up_label": _coerce_str(payload["follow_up_label"], field="follow_up_label", path=path), + "follow_up_title_prefix": _coerce_str( + payload["follow_up_title_prefix"], + field="follow_up_title_prefix", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "release_follow_up_terminal_publish_status": terminal_publish_status, + "release_follow_up_terminal_publish_decision": terminal_publish_decision, + "release_follow_up_terminal_publish_exit_code": terminal_publish_exit_code, + "follow_up_terminal_publish_should_notify": _coerce_bool( + payload["follow_up_terminal_publish_should_notify"], + field="follow_up_terminal_publish_should_notify", + path=path, + ), + "follow_up_terminal_requires_manual_action": _coerce_bool( + payload["follow_up_terminal_requires_manual_action"], + field="follow_up_terminal_requires_manual_action", + path=path, + ), + "follow_up_terminal_publish_channel": terminal_publish_channel, + "release_follow_up_terminal_publish_summary": _coerce_str( + payload["release_follow_up_terminal_publish_summary"], + field="release_follow_up_terminal_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_follow_up_final_verdict_payload( + terminal_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + terminal_status = str(terminal_publish_report["release_follow_up_terminal_publish_status"]) + terminal_decision = str(terminal_publish_report["release_follow_up_terminal_publish_decision"]) + terminal_exit_code = int(terminal_publish_report["release_follow_up_terminal_publish_exit_code"]) + terminal_should_notify = bool(terminal_publish_report["follow_up_terminal_publish_should_notify"]) + terminal_requires_manual_action = bool( + terminal_publish_report["follow_up_terminal_requires_manual_action"] + ) + terminal_channel = str(terminal_publish_report["follow_up_terminal_publish_channel"]) + + closure_status = str(terminal_publish_report["release_follow_up_closure_status"]) + closure_decision = str(terminal_publish_report["release_follow_up_closure_decision"]) + closure_exit_code = int(terminal_publish_report["release_follow_up_closure_exit_code"]) + should_queue_follow_up = bool(terminal_publish_report["should_queue_follow_up"]) + follow_up_required = bool(terminal_publish_report["follow_up_required"]) + escalation_required = bool(terminal_publish_report["escalation_required"]) + dispatch_target = str(terminal_publish_report["dispatch_target"]) + follow_up_queue_attempted = bool(terminal_publish_report["follow_up_queue_attempted"]) + follow_up_task_queued = bool(terminal_publish_report["follow_up_task_queued"]) + escalation_task_queued = bool(terminal_publish_report["escalation_task_queued"]) + + reason_codes = list(terminal_publish_report["reason_codes"]) + structural_issues = list(terminal_publish_report["structural_issues"]) + missing_artifacts = list(terminal_publish_report["missing_artifacts"]) + evidence_manifest = list(terminal_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_follow_up_final_verdict_evidence_missing") + + if terminal_status == "published": + if terminal_decision not in {"announce_closed", "announce_queued"}: + structural_issues.append("terminal_decision_mismatch_published") + if terminal_exit_code != 0: + structural_issues.append("terminal_exit_code_mismatch_published") + if not terminal_should_notify: + structural_issues.append("terminal_should_notify_mismatch_published") + if terminal_requires_manual_action: + structural_issues.append("terminal_manual_action_mismatch_published") + if terminal_channel not in {"release", "follow_up"}: + structural_issues.append("terminal_channel_mismatch_published") + + if closure_status not in {"closed", "queued"}: + structural_issues.append("closure_status_mismatch_published") + if terminal_decision == "announce_closed" and closure_status != "closed": + structural_issues.append("closure_status_mismatch_announce_closed") + if terminal_decision == "announce_queued" and closure_status != "queued": + structural_issues.append("closure_status_mismatch_announce_queued") + + if closure_status == "closed": + if closure_decision != "no_action": + structural_issues.append("closure_decision_mismatch_closed") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_closed") + if should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_closed") + if follow_up_required: + structural_issues.append("follow_up_required_mismatch_closed") + if follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_closed") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_closed") + if escalation_task_queued: + structural_issues.append("escalation_task_queued_mismatch_closed") + if dispatch_target != "none": + structural_issues.append("dispatch_target_mismatch_closed") + + if closure_status == "queued": + if closure_decision not in {"queue_follow_up", "queue_escalation"}: + structural_issues.append("closure_decision_mismatch_queued") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_queued") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_queued") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_queued") + if not follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_queued") + if not follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_queued") + if dispatch_target == "none": + structural_issues.append("dispatch_target_mismatch_queued") + if escalation_required and not escalation_task_queued: + structural_issues.append("escalation_task_queued_mismatch_queued") + + elif terminal_status == "pending_queue": + if terminal_decision != "announce_pending_queue": + structural_issues.append("terminal_decision_mismatch_pending_queue") + if terminal_exit_code == 0: + structural_issues.append("terminal_exit_code_mismatch_pending_queue") + if not terminal_should_notify: + structural_issues.append("terminal_should_notify_mismatch_pending_queue") + if not terminal_requires_manual_action: + structural_issues.append("terminal_manual_action_mismatch_pending_queue") + if terminal_channel != "follow_up": + structural_issues.append("terminal_channel_mismatch_pending_queue") + if closure_status != "queued_dry_run": + structural_issues.append("closure_status_mismatch_pending_queue") + if closure_decision not in {"queue_follow_up", "queue_escalation"}: + structural_issues.append("closure_decision_mismatch_pending_queue") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_pending_queue") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_pending_queue") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_pending_queue") + if follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_pending_queue") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_pending_queue") + if dispatch_target == "none": + structural_issues.append("dispatch_target_mismatch_pending_queue") + + elif terminal_status == "queue_failed": + if terminal_decision != "announce_queue_failure": + structural_issues.append("terminal_decision_mismatch_queue_failed") + if terminal_exit_code == 0: + structural_issues.append("terminal_exit_code_mismatch_queue_failed") + if not terminal_should_notify: + structural_issues.append("terminal_should_notify_mismatch_queue_failed") + if not terminal_requires_manual_action: + structural_issues.append("terminal_manual_action_mismatch_queue_failed") + if terminal_channel != "blocker": + structural_issues.append("terminal_channel_mismatch_queue_failed") + if closure_status != "queue_failed": + structural_issues.append("closure_status_mismatch_queue_failed") + if closure_decision != "abort_queue": + structural_issues.append("closure_decision_mismatch_queue_failed") + if closure_exit_code == 0: + structural_issues.append("closure_exit_code_mismatch_queue_failed") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_queue_failed") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_queue_failed") + if not follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_queue_failed") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_queue_failed") + + else: + if terminal_decision != "abort_publish": + structural_issues.append("terminal_decision_mismatch_contract_failed") + if terminal_exit_code == 0: + structural_issues.append("terminal_exit_code_mismatch_contract_failed") + if terminal_channel != "blocker": + structural_issues.append("terminal_channel_mismatch_contract_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + final_status = "contract_failed" + final_decision = "abort_close" + final_exit_code = 1 + final_should_close_follow_up = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.extend(structural_issues) + elif terminal_status == "published": + final_status = "completed" + final_decision = "close_follow_up" + final_exit_code = 0 + final_should_close_follow_up = True + final_should_open_follow_up = False + final_should_page_owner = False + if terminal_decision == "announce_queued": + final_announcement_target = "follow_up" + reason_codes = ["release_follow_up_final_completed_queued"] + else: + final_announcement_target = "release" + reason_codes = ["release_follow_up_final_completed_closed"] + elif terminal_status == "pending_queue": + final_status = "requires_follow_up" + final_decision = "keep_follow_up_open" + final_exit_code = max(1, terminal_exit_code, closure_exit_code) + final_should_close_follow_up = False + final_should_open_follow_up = True + final_should_page_owner = False + final_announcement_target = "follow_up" + reason_codes.append("release_follow_up_final_pending_queue") + elif terminal_status == "queue_failed": + final_status = "blocked" + final_decision = "escalate_queue_failure" + final_exit_code = max(1, terminal_exit_code, closure_exit_code) + final_should_close_follow_up = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.append("release_follow_up_final_queue_failed") + else: + final_status = "contract_failed" + final_decision = "abort_close" + final_exit_code = max(1, terminal_exit_code, closure_exit_code) + final_should_close_follow_up = False + final_should_open_follow_up = True + final_should_page_owner = True + final_announcement_target = "blocker" + reason_codes.append("release_follow_up_final_upstream_contract_failed") + + if final_status not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported release_follow_up_final_verdict_status computed") + if final_decision not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported release_follow_up_final_verdict_decision computed") + if final_announcement_target not in ALLOWED_RELEASE_FOLLOW_UP_FINAL_ANNOUNCEMENT_TARGETS: + raise ValueError("internal: unsupported release_follow_up_final_announcement_target computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_follow_up_terminal_publish_status={terminal_status} " + f"release_follow_up_final_verdict_status={final_status} " + f"release_follow_up_final_verdict_decision={final_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_follow_up_terminal_publish_report": str(source_path), + "source_release_follow_up_closure_report": str( + terminal_publish_report["source_release_follow_up_closure_report"] + ), + "source_release_follow_up_dispatch_report": str( + terminal_publish_report["source_release_follow_up_dispatch_report"] + ), + "project_root": str(terminal_publish_report["project_root"]), + "release_follow_up_dispatch_status": str( + terminal_publish_report["release_follow_up_dispatch_status"] + ), + "release_follow_up_dispatch_decision": str( + terminal_publish_report["release_follow_up_dispatch_decision"] + ), + "release_follow_up_dispatch_exit_code": int( + terminal_publish_report["release_follow_up_dispatch_exit_code"] + ), + "follow_up_required": follow_up_required, + "escalation_required": escalation_required, + "dispatch_target": dispatch_target, + "should_queue_follow_up": should_queue_follow_up, + "release_follow_up_closure_status": closure_status, + "release_follow_up_closure_decision": closure_decision, + "release_follow_up_closure_exit_code": closure_exit_code, + "follow_up_queue_attempted": follow_up_queue_attempted, + "follow_up_task_queued": follow_up_task_queued, + "escalation_task_queued": escalation_task_queued, + "follow_up_command": str(terminal_publish_report["follow_up_command"]), + "follow_up_command_parts": list(terminal_publish_report["follow_up_command_parts"]), + "follow_up_command_returncode": terminal_publish_report["follow_up_command_returncode"], + "follow_up_command_stdout_tail": str(terminal_publish_report["follow_up_command_stdout_tail"]), + "follow_up_command_stderr_tail": str(terminal_publish_report["follow_up_command_stderr_tail"]), + "follow_up_error_type": str(terminal_publish_report["follow_up_error_type"]), + "follow_up_error_message": str(terminal_publish_report["follow_up_error_message"]), + "follow_up_repo": str(terminal_publish_report["follow_up_repo"]), + "follow_up_label": str(terminal_publish_report["follow_up_label"]), + "follow_up_title_prefix": str(terminal_publish_report["follow_up_title_prefix"]), + "follow_up_queue_url": str(terminal_publish_report["follow_up_queue_url"]), + "release_run_id": terminal_publish_report["release_run_id"], + "release_run_url": str(terminal_publish_report["release_run_url"]), + "release_follow_up_terminal_publish_status": terminal_status, + "release_follow_up_terminal_publish_decision": terminal_decision, + "release_follow_up_terminal_publish_exit_code": terminal_exit_code, + "follow_up_terminal_publish_should_notify": terminal_should_notify, + "follow_up_terminal_requires_manual_action": terminal_requires_manual_action, + "follow_up_terminal_publish_channel": terminal_channel, + "release_follow_up_final_verdict_status": final_status, + "release_follow_up_final_verdict_decision": final_decision, + "release_follow_up_final_verdict_exit_code": final_exit_code, + "final_should_close_follow_up": final_should_close_follow_up, + "final_should_open_follow_up": final_should_open_follow_up, + "final_should_page_owner": final_should_page_owner, + "final_announcement_target": final_announcement_target, + "release_follow_up_final_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Follow-Up Final Verdict Report", + "", + ( + "- Release Follow-Up Final Verdict Status: " + f"**{str(payload['release_follow_up_final_verdict_status']).upper()}**" + ), + ( + "- Release Follow-Up Final Verdict Decision: " + f"`{payload['release_follow_up_final_verdict_decision']}`" + ), + ( + "- Release Follow-Up Final Verdict Exit Code: " + f"`{payload['release_follow_up_final_verdict_exit_code']}`" + ), + f"- Final Should Close Follow-Up: `{payload['final_should_close_follow_up']}`", + f"- Final Should Open Follow-Up: `{payload['final_should_open_follow_up']}`", + f"- Final Should Page Owner: `{payload['final_should_page_owner']}`", + f"- Final Announcement Target: `{payload['final_announcement_target']}`", + ( + "- Release Follow-Up Terminal Publish Status: " + f"`{payload['release_follow_up_terminal_publish_status']}`" + ), + ( + "- Follow-Up Terminal Publish Channel: " + f"`{payload['follow_up_terminal_publish_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_follow_up_final_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_follow_up_final_verdict_status": str( + payload["release_follow_up_final_verdict_status"] + ), + "workflow_release_follow_up_final_verdict_decision": str( + payload["release_follow_up_final_verdict_decision"] + ), + "workflow_release_follow_up_final_verdict_exit_code": str( + payload["release_follow_up_final_verdict_exit_code"] + ), + "workflow_release_follow_up_final_should_close_follow_up": ( + "true" if payload["final_should_close_follow_up"] else "false" + ), + "workflow_release_follow_up_final_should_open_follow_up": ( + "true" if payload["final_should_open_follow_up"] else "false" + ), + "workflow_release_follow_up_final_should_page_owner": ( + "true" if payload["final_should_page_owner"] else "false" + ), + "workflow_release_follow_up_final_announcement_target": str( + payload["final_announcement_target"] + ), + "workflow_release_follow_up_final_terminal_publish_status": str( + payload["release_follow_up_terminal_publish_status"] + ), + "workflow_release_follow_up_final_terminal_publish_channel": str( + payload["follow_up_terminal_publish_channel"] + ), + "workflow_release_follow_up_final_queue_url": str(payload["follow_up_queue_url"]), + "workflow_release_follow_up_final_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_follow_up_final_run_url": str(payload["release_run_url"]), + "workflow_release_follow_up_final_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_follow_up_final_report_json": str(output_json), + "workflow_release_follow_up_final_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release follow-up final verdict contract " + "from P2-46 release follow-up terminal publish report" + ) + ) + parser.add_argument( + "--release-follow-up-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json", + help="P2-46 release follow-up terminal publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json", + help="Output release follow-up final verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md", + help="Output release follow-up final verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release follow-up final verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_publish_report_path = Path(args.release_follow_up_terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_publish_report = load_release_follow_up_terminal_publish_report( + terminal_publish_report_path + ) + payload = build_release_follow_up_final_verdict_payload( + terminal_publish_report, + source_path=terminal_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-follow-up-final-verdict-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release follow-up final verdict json: {output_json_path}") + print(f"release follow-up final verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release follow-up final verdict summary: " + f"release_follow_up_final_verdict_status={payload['release_follow_up_final_verdict_status']} " + f"release_follow_up_final_verdict_decision={payload['release_follow_up_final_verdict_decision']} " + f"release_follow_up_final_verdict_exit_code={payload['release_follow_up_final_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_follow_up_final_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py new file mode 100644 index 0000000..6260a73 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py @@ -0,0 +1,779 @@ +"""Phase 2 card P2-46 gate for Linux CI workflow release follow-up terminal publish. + +This script consumes the P2-45 release follow-up closure artifact and +converges the terminal publish contract: +1) validate follow-up closure + evidence consistency, +2) normalize terminal publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: set[str] = { + "closed", + "follow_up_required", + "escalated", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: set[str] = { + "no_action", + "dispatch_follow_up", + "dispatch_escalation", + "abort_dispatch", +} +ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: set[str] = { + "none", + "follow_up_queue", + "incident_commander", +} +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: set[str] = { + "closed", + "queued_dry_run", + "queued", + "queue_failed", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: set[str] = { + "no_action", + "queue_follow_up", + "queue_escalation", + "abort_queue", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_STATUSES: set[str] = { + "published", + "pending_queue", + "queue_failed", + "contract_failed", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_closed", + "announce_queued", + "announce_pending_queue", + "announce_queue_failure", + "abort_publish", +} +ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_follow_up_closure_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release follow-up closure report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release follow-up closure payload must be object") + + required_fields = ( + "source_release_follow_up_dispatch_report", + "project_root", + "release_follow_up_dispatch_status", + "release_follow_up_dispatch_decision", + "release_follow_up_dispatch_exit_code", + "follow_up_required", + "escalation_required", + "dispatch_target", + "should_queue_follow_up", + "release_follow_up_closure_status", + "release_follow_up_closure_decision", + "release_follow_up_closure_exit_code", + "follow_up_queue_attempted", + "follow_up_task_queued", + "escalation_task_queued", + "follow_up_command", + "follow_up_command_parts", + "follow_up_command_returncode", + "follow_up_command_stdout_tail", + "follow_up_command_stderr_tail", + "follow_up_error_type", + "follow_up_error_message", + "follow_up_repo", + "follow_up_label", + "follow_up_title_prefix", + "follow_up_queue_url", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + dispatch_status = _coerce_str( + payload["release_follow_up_dispatch_status"], + field="release_follow_up_dispatch_status", + path=path, + ) + if dispatch_status not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_STATUSES)}" + ) + + dispatch_decision = _coerce_str( + payload["release_follow_up_dispatch_decision"], + field="release_follow_up_dispatch_decision", + path=path, + ) + if dispatch_decision not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_dispatch_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_DECISIONS)}" + ) + + dispatch_exit_code = _coerce_int( + payload["release_follow_up_dispatch_exit_code"], + field="release_follow_up_dispatch_exit_code", + path=path, + ) + if dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'release_follow_up_dispatch_exit_code' must be >= 0") + + dispatch_target = _coerce_str(payload["dispatch_target"], field="dispatch_target", path=path) + if dispatch_target not in ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS: + raise ValueError( + f"{path}: field 'dispatch_target' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_DISPATCH_TARGETS)}" + ) + + closure_status = _coerce_str( + payload["release_follow_up_closure_status"], + field="release_follow_up_closure_status", + path=path, + ) + if closure_status not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES: + raise ValueError( + f"{path}: field 'release_follow_up_closure_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_STATUSES)}" + ) + + closure_decision = _coerce_str( + payload["release_follow_up_closure_decision"], + field="release_follow_up_closure_decision", + path=path, + ) + if closure_decision not in ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS: + raise ValueError( + f"{path}: field 'release_follow_up_closure_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FOLLOW_UP_CLOSURE_DECISIONS)}" + ) + + closure_exit_code = _coerce_int( + payload["release_follow_up_closure_exit_code"], + field="release_follow_up_closure_exit_code", + path=path, + ) + if closure_exit_code < 0: + raise ValueError(f"{path}: field 'release_follow_up_closure_exit_code' must be >= 0") + + follow_up_command_returncode_raw = payload["follow_up_command_returncode"] + if follow_up_command_returncode_raw is None: + follow_up_command_returncode = None + else: + follow_up_command_returncode = _coerce_int( + follow_up_command_returncode_raw, + field="follow_up_command_returncode", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_follow_up_dispatch_report": _coerce_str( + payload["source_release_follow_up_dispatch_report"], + field="source_release_follow_up_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "release_follow_up_dispatch_status": dispatch_status, + "release_follow_up_dispatch_decision": dispatch_decision, + "release_follow_up_dispatch_exit_code": dispatch_exit_code, + "follow_up_required": _coerce_bool( + payload["follow_up_required"], + field="follow_up_required", + path=path, + ), + "escalation_required": _coerce_bool( + payload["escalation_required"], + field="escalation_required", + path=path, + ), + "dispatch_target": dispatch_target, + "should_queue_follow_up": _coerce_bool( + payload["should_queue_follow_up"], + field="should_queue_follow_up", + path=path, + ), + "release_follow_up_closure_status": closure_status, + "release_follow_up_closure_decision": closure_decision, + "release_follow_up_closure_exit_code": closure_exit_code, + "follow_up_queue_attempted": _coerce_bool( + payload["follow_up_queue_attempted"], + field="follow_up_queue_attempted", + path=path, + ), + "follow_up_task_queued": _coerce_bool( + payload["follow_up_task_queued"], + field="follow_up_task_queued", + path=path, + ), + "escalation_task_queued": _coerce_bool( + payload["escalation_task_queued"], + field="escalation_task_queued", + path=path, + ), + "follow_up_command": _coerce_str( + payload["follow_up_command"], + field="follow_up_command", + path=path, + ), + "follow_up_command_parts": _coerce_str_list( + payload["follow_up_command_parts"], + field="follow_up_command_parts", + path=path, + ), + "follow_up_command_returncode": follow_up_command_returncode, + "follow_up_command_stdout_tail": _coerce_str( + payload["follow_up_command_stdout_tail"], + field="follow_up_command_stdout_tail", + path=path, + ), + "follow_up_command_stderr_tail": _coerce_str( + payload["follow_up_command_stderr_tail"], + field="follow_up_command_stderr_tail", + path=path, + ), + "follow_up_error_type": _coerce_str( + payload["follow_up_error_type"], + field="follow_up_error_type", + path=path, + ), + "follow_up_error_message": _coerce_str( + payload["follow_up_error_message"], + field="follow_up_error_message", + path=path, + ), + "follow_up_repo": _coerce_str(payload["follow_up_repo"], field="follow_up_repo", path=path), + "follow_up_label": _coerce_str(payload["follow_up_label"], field="follow_up_label", path=path), + "follow_up_title_prefix": _coerce_str( + payload["follow_up_title_prefix"], + field="follow_up_title_prefix", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": evidence_manifest, + } + + +def build_release_follow_up_terminal_publish_payload( + closure_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + closure_status = str(closure_report["release_follow_up_closure_status"]) + closure_decision = str(closure_report["release_follow_up_closure_decision"]) + closure_exit_code = int(closure_report["release_follow_up_closure_exit_code"]) + should_queue_follow_up = bool(closure_report["should_queue_follow_up"]) + follow_up_queue_attempted = bool(closure_report["follow_up_queue_attempted"]) + follow_up_task_queued = bool(closure_report["follow_up_task_queued"]) + escalation_task_queued = bool(closure_report["escalation_task_queued"]) + follow_up_required = bool(closure_report["follow_up_required"]) + escalation_required = bool(closure_report["escalation_required"]) + dispatch_target = str(closure_report["dispatch_target"]) + + reason_codes = list(closure_report["reason_codes"]) + structural_issues = list(closure_report["structural_issues"]) + missing_artifacts = list(closure_report["missing_artifacts"]) + evidence_manifest = list(closure_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_follow_up_terminal_publish_evidence_missing") + + if closure_status == "closed": + if closure_decision != "no_action": + structural_issues.append("closure_decision_mismatch_closed") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_closed") + if should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_closed") + if follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_closed") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_closed") + if escalation_task_queued: + structural_issues.append("escalation_task_queued_mismatch_closed") + if dispatch_target != "none": + structural_issues.append("dispatch_target_mismatch_closed") + if follow_up_required: + structural_issues.append("follow_up_required_mismatch_closed") + elif closure_status == "queued_dry_run": + if closure_decision not in {"queue_follow_up", "queue_escalation"}: + structural_issues.append("closure_decision_mismatch_queued_dry_run") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_queued_dry_run") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_queued_dry_run") + if follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_queued_dry_run") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_queued_dry_run") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_queued_dry_run") + elif closure_status == "queued": + if closure_decision not in {"queue_follow_up", "queue_escalation"}: + structural_issues.append("closure_decision_mismatch_queued") + if closure_exit_code != 0: + structural_issues.append("closure_exit_code_mismatch_queued") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_queued") + if not follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_queued") + if not follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_queued") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_queued") + if escalation_required and not escalation_task_queued: + structural_issues.append("escalation_task_queued_mismatch_queued") + elif closure_status == "queue_failed": + if closure_decision != "abort_queue": + structural_issues.append("closure_decision_mismatch_queue_failed") + if closure_exit_code == 0: + structural_issues.append("closure_exit_code_mismatch_queue_failed") + if not should_queue_follow_up: + structural_issues.append("should_queue_follow_up_mismatch_queue_failed") + if not follow_up_queue_attempted: + structural_issues.append("follow_up_queue_attempted_mismatch_queue_failed") + if follow_up_task_queued: + structural_issues.append("follow_up_task_queued_mismatch_queue_failed") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_queue_failed") + else: + if closure_decision != "abort_queue": + structural_issues.append("closure_decision_mismatch_contract_failed") + if closure_exit_code == 0: + structural_issues.append("closure_exit_code_mismatch_contract_failed") + if not follow_up_required: + structural_issues.append("follow_up_required_mismatch_contract_failed") + + if closure_status in {"queued_dry_run", "queued", "queue_failed"} and dispatch_target == "none": + structural_issues.append("dispatch_target_mismatch_queue_path") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + terminal_status = "contract_failed" + terminal_decision = "abort_publish" + terminal_exit_code = 1 + terminal_should_notify = True + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.extend(structural_issues) + elif closure_status == "closed": + terminal_status = "published" + terminal_decision = "announce_closed" + terminal_exit_code = 0 + terminal_should_notify = True + terminal_requires_manual_action = False + terminal_channel = "release" + reason_codes = ["release_follow_up_terminal_closed"] + elif closure_status == "queued": + terminal_status = "published" + terminal_decision = "announce_queued" + terminal_exit_code = 0 + terminal_should_notify = True + terminal_requires_manual_action = False + terminal_channel = "follow_up" + reason_codes = ( + ["release_follow_up_terminal_escalation_queued"] + if escalation_required + else ["release_follow_up_terminal_queued"] + ) + elif closure_status == "queued_dry_run": + terminal_status = "pending_queue" + terminal_decision = "announce_pending_queue" + terminal_exit_code = max(1, closure_exit_code) + terminal_should_notify = True + terminal_requires_manual_action = True + terminal_channel = "follow_up" + reason_codes.append("release_follow_up_terminal_pending_queue") + elif closure_status == "queue_failed": + terminal_status = "queue_failed" + terminal_decision = "announce_queue_failure" + terminal_exit_code = max(1, closure_exit_code) + terminal_should_notify = True + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("release_follow_up_terminal_queue_failed") + else: + terminal_status = "contract_failed" + terminal_decision = "abort_publish" + terminal_exit_code = max(1, closure_exit_code) + terminal_should_notify = True + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("release_follow_up_terminal_upstream_contract_failed") + + if terminal_status not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_STATUSES: + raise ValueError("internal: unsupported release_follow_up_terminal_publish_status computed") + if terminal_decision not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported release_follow_up_terminal_publish_decision computed") + if terminal_channel not in ALLOWED_RELEASE_FOLLOW_UP_TERMINAL_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported release_follow_up_terminal_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_follow_up_closure_status={closure_status} " + f"release_follow_up_terminal_publish_status={terminal_status} " + f"release_follow_up_terminal_publish_decision={terminal_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_follow_up_closure_report": str(source_path), + "source_release_follow_up_dispatch_report": str( + closure_report["source_release_follow_up_dispatch_report"] + ), + "project_root": str(closure_report["project_root"]), + "release_follow_up_dispatch_status": str(closure_report["release_follow_up_dispatch_status"]), + "release_follow_up_dispatch_decision": str(closure_report["release_follow_up_dispatch_decision"]), + "release_follow_up_dispatch_exit_code": int(closure_report["release_follow_up_dispatch_exit_code"]), + "follow_up_required": follow_up_required, + "escalation_required": escalation_required, + "dispatch_target": dispatch_target, + "should_queue_follow_up": should_queue_follow_up, + "release_follow_up_closure_status": closure_status, + "release_follow_up_closure_decision": closure_decision, + "release_follow_up_closure_exit_code": closure_exit_code, + "follow_up_queue_attempted": follow_up_queue_attempted, + "follow_up_task_queued": follow_up_task_queued, + "escalation_task_queued": escalation_task_queued, + "follow_up_command": str(closure_report["follow_up_command"]), + "follow_up_command_parts": list(closure_report["follow_up_command_parts"]), + "follow_up_command_returncode": closure_report["follow_up_command_returncode"], + "follow_up_command_stdout_tail": str(closure_report["follow_up_command_stdout_tail"]), + "follow_up_command_stderr_tail": str(closure_report["follow_up_command_stderr_tail"]), + "follow_up_error_type": str(closure_report["follow_up_error_type"]), + "follow_up_error_message": str(closure_report["follow_up_error_message"]), + "follow_up_repo": str(closure_report["follow_up_repo"]), + "follow_up_label": str(closure_report["follow_up_label"]), + "follow_up_title_prefix": str(closure_report["follow_up_title_prefix"]), + "follow_up_queue_url": str(closure_report["follow_up_queue_url"]), + "release_run_id": closure_report["release_run_id"], + "release_run_url": str(closure_report["release_run_url"]), + "release_follow_up_terminal_publish_status": terminal_status, + "release_follow_up_terminal_publish_decision": terminal_decision, + "release_follow_up_terminal_publish_exit_code": terminal_exit_code, + "follow_up_terminal_publish_should_notify": terminal_should_notify, + "follow_up_terminal_requires_manual_action": terminal_requires_manual_action, + "follow_up_terminal_publish_channel": terminal_channel, + "release_follow_up_terminal_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Follow-Up Terminal Publish Report", + "", + ( + "- Release Follow-Up Terminal Publish Status: " + f"**{str(payload['release_follow_up_terminal_publish_status']).upper()}**" + ), + ( + "- Release Follow-Up Terminal Publish Decision: " + f"`{payload['release_follow_up_terminal_publish_decision']}`" + ), + ( + "- Release Follow-Up Terminal Publish Exit Code: " + f"`{payload['release_follow_up_terminal_publish_exit_code']}`" + ), + ( + "- Follow-Up Terminal Publish Should Notify: " + f"`{payload['follow_up_terminal_publish_should_notify']}`" + ), + ( + "- Follow-Up Terminal Requires Manual Action: " + f"`{payload['follow_up_terminal_requires_manual_action']}`" + ), + f"- Follow-Up Terminal Publish Channel: `{payload['follow_up_terminal_publish_channel']}`", + f"- Release Follow-Up Closure Status: `{payload['release_follow_up_closure_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_follow_up_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_follow_up_terminal_publish_status": str( + payload["release_follow_up_terminal_publish_status"] + ), + "workflow_release_follow_up_terminal_publish_decision": str( + payload["release_follow_up_terminal_publish_decision"] + ), + "workflow_release_follow_up_terminal_publish_exit_code": str( + payload["release_follow_up_terminal_publish_exit_code"] + ), + "workflow_release_follow_up_terminal_publish_should_notify": ( + "true" if payload["follow_up_terminal_publish_should_notify"] else "false" + ), + "workflow_release_follow_up_terminal_publish_manual_action": ( + "true" if payload["follow_up_terminal_requires_manual_action"] else "false" + ), + "workflow_release_follow_up_terminal_publish_channel": str( + payload["follow_up_terminal_publish_channel"] + ), + "workflow_release_follow_up_terminal_publish_closure_status": str( + payload["release_follow_up_closure_status"] + ), + "workflow_release_follow_up_terminal_publish_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_release_follow_up_terminal_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_follow_up_terminal_publish_run_url": str(payload["release_run_url"]), + "workflow_release_follow_up_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_follow_up_terminal_publish_report_json": str(output_json), + "workflow_release_follow_up_terminal_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release follow-up terminal publish contract " + "from P2-45 release follow-up closure report" + ) + ) + parser.add_argument( + "--release-follow-up-closure-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json", + help="P2-45 release follow-up closure report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json", + help="Output release follow-up terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md", + help="Output release follow-up terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release follow-up terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + closure_report_path = Path(args.release_follow_up_closure_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + closure_report = load_release_follow_up_closure_report(closure_report_path) + payload = build_release_follow_up_terminal_publish_payload( + closure_report, + source_path=closure_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-follow-up-terminal-publish-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release follow-up terminal publish json: {output_json_path}") + print(f"release follow-up terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release follow-up terminal publish summary: " + f"release_follow_up_terminal_publish_status={payload['release_follow_up_terminal_publish_status']} " + f"release_follow_up_terminal_publish_decision={payload['release_follow_up_terminal_publish_decision']} " + f"release_follow_up_terminal_publish_exit_code={payload['release_follow_up_terminal_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_follow_up_terminal_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_handoff_gate.py b/scripts/run_p2_linux_ci_workflow_release_handoff_gate.py new file mode 100644 index 0000000..d3b3680 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_handoff_gate.py @@ -0,0 +1,477 @@ +"""Phase 2 card P2-30 gate for Linux CI workflow release handoff. + +This script converts the P2-29 terminal publish artifact into a downstream +release handoff contract: +1) validate terminal publish contract consistency, +2) normalize promote/hold/blocked handoff decision, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_PUBLISH_STATUSES: set[str] = { + "passed", + "blocked", + "in_progress", + "failed", + "contract_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: terminal publish report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: terminal publish payload must be object") + + required_fields = ( + "publish_status", + "publish_exit_code", + "should_promote", + "completion_status", + "completion_exit_code", + "publish_summary", + "run_id", + "run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + publish_status = _coerce_str(payload["publish_status"], field="publish_status", path=path) + if publish_status not in ALLOWED_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'publish_status' must be one of " + f"{sorted(ALLOWED_PUBLISH_STATUSES)}" + ) + + publish_exit_code = _coerce_int( + payload["publish_exit_code"], + field="publish_exit_code", + path=path, + ) + if publish_exit_code < 0: + raise ValueError(f"{path}: field 'publish_exit_code' must be >= 0") + + should_promote = _coerce_bool(payload["should_promote"], field="should_promote", path=path) + completion_status = _coerce_str( + payload["completion_status"], + field="completion_status", + path=path, + ) + completion_exit_code = _coerce_int( + payload["completion_exit_code"], + field="completion_exit_code", + path=path, + ) + if completion_exit_code < 0: + raise ValueError(f"{path}: field 'completion_exit_code' must be >= 0") + + publish_summary = _coerce_str(payload["publish_summary"], field="publish_summary", path=path) + + run_id_raw = payload["run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="run_id", path=path) + if run_id < 1: + raise ValueError(f"{path}: field 'run_id' must be >= 1 when present") + + run_url = _coerce_str(payload["run_url"], field="run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "publish_status": publish_status, + "publish_exit_code": publish_exit_code, + "should_promote": should_promote, + "completion_status": completion_status, + "completion_exit_code": completion_exit_code, + "publish_summary": publish_summary, + "run_id": run_id, + "run_url": run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_release_handoff_payload( + terminal_publish_report: dict[str, Any], + *, + source_path: Path, + target_environment: str, + release_channel: str, +) -> dict[str, Any]: + publish_status = str(terminal_publish_report["publish_status"]) + publish_exit_code = int(terminal_publish_report["publish_exit_code"]) + should_promote = bool(terminal_publish_report["should_promote"]) + + structural_issues = list(terminal_publish_report["structural_issues"]) + reason_codes = list(terminal_publish_report["reason_codes"]) + failed_checks = list(terminal_publish_report["failed_checks"]) + missing_artifacts = list(terminal_publish_report["missing_artifacts"]) + + if publish_status == "passed": + if publish_exit_code != 0: + structural_issues.append("publish_exit_code_mismatch_passed") + if not should_promote: + structural_issues.append("should_promote_mismatch_passed") + else: + if should_promote: + structural_issues.append("should_promote_mismatch_non_passed") + + if publish_status == "blocked" and publish_exit_code != 0: + structural_issues.append("publish_exit_code_mismatch_blocked") + if publish_status == "in_progress" and publish_exit_code != 0: + structural_issues.append("publish_exit_code_mismatch_in_progress") + if publish_status in {"failed", "contract_failed"} and publish_exit_code == 0: + structural_issues.append("publish_exit_code_mismatch_failed") + + structural_issues = _unique(structural_issues) + + if structural_issues: + handoff_status = "contract_failed" + should_trigger_release = False + release_exit_code = 1 + handoff_action = "hold" + reason_codes.extend(structural_issues) + elif publish_status == "passed": + handoff_status = "ready_for_release" + should_trigger_release = True + release_exit_code = 0 + handoff_action = "promote" + reason_codes = ["release_ready"] + elif publish_status == "in_progress": + handoff_status = "awaiting_completion" + should_trigger_release = False + release_exit_code = 0 + handoff_action = "hold" + elif publish_status == "blocked": + handoff_status = "blocked" + should_trigger_release = False + release_exit_code = 0 + handoff_action = "hold" + else: + handoff_status = "failed" + should_trigger_release = False + release_exit_code = 1 + handoff_action = "reject" + + handoff_summary = ( + f"publish_status={publish_status} " + f"handoff_status={handoff_status} " + f"should_trigger_release={should_trigger_release} " + f"target_environment={target_environment} " + f"release_channel={release_channel}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_terminal_publish_report": str(source_path), + "source_dispatch_completion_report": str( + terminal_publish_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str(terminal_publish_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str( + terminal_publish_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(terminal_publish_report["source_dispatch_report"]), + "target_environment": target_environment, + "release_channel": release_channel, + "publish_status": publish_status, + "publish_exit_code": publish_exit_code, + "should_promote": should_promote, + "completion_status": str(terminal_publish_report["completion_status"]), + "completion_exit_code": int(terminal_publish_report["completion_exit_code"]), + "handoff_status": handoff_status, + "handoff_action": handoff_action, + "should_trigger_release": should_trigger_release, + "release_exit_code": int(release_exit_code), + "handoff_summary": handoff_summary, + "publish_summary": str(terminal_publish_report["publish_summary"]), + "run_id": terminal_publish_report["run_id"], + "run_url": str(terminal_publish_report["run_url"]), + "reason_codes": _unique(reason_codes), + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Handoff Report", + "", + f"- Handoff Status: **{str(payload['handoff_status']).upper()}**", + f"- Handoff Action: `{payload['handoff_action']}`", + f"- Should Trigger Release: `{payload['should_trigger_release']}`", + f"- Release Exit Code: `{payload['release_exit_code']}`", + f"- Target Environment: `{payload['target_environment']}`", + f"- Release Channel: `{payload['release_channel']}`", + f"- Publish Status: `{payload['publish_status']}`", + f"- Completion Status: `{payload['completion_status']}`", + f"- Run ID: `{payload['run_id']}`", + f"- Run URL: `{payload['run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Terminal Publish Report: `{payload['source_terminal_publish_report']}`", + f"- Source Dispatch Completion Report: `{payload['source_dispatch_completion_report']}`", + f"- Source Dispatch Trace Report: `{payload['source_dispatch_trace_report']}`", + f"- Source Dispatch Execution Report: `{payload['source_dispatch_execution_report']}`", + f"- Source Dispatch Report: `{payload['source_dispatch_report']}`", + "", + "## Handoff Summary", + "", + f"- {payload['handoff_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["run_id"] + return { + "workflow_release_handoff_status": str(payload["handoff_status"]), + "workflow_release_handoff_action": str(payload["handoff_action"]), + "workflow_release_handoff_should_trigger_release": "true" + if payload["should_trigger_release"] + else "false", + "workflow_release_handoff_release_exit_code": str(payload["release_exit_code"]), + "workflow_release_handoff_target_environment": str(payload["target_environment"]), + "workflow_release_handoff_release_channel": str(payload["release_channel"]), + "workflow_release_handoff_publish_status": str(payload["publish_status"]), + "workflow_release_handoff_completion_status": str(payload["completion_status"]), + "workflow_release_handoff_run_id": "" if run_id is None else str(run_id), + "workflow_release_handoff_run_url": str(payload["run_url"]), + "workflow_release_handoff_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_handoff_report_json": str(output_json), + "workflow_release_handoff_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Build Linux CI workflow release handoff report from P2-29 terminal publish artifact" + ) + ) + parser.add_argument( + "--terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json", + help="P2-29 terminal publish report JSON path", + ) + parser.add_argument( + "--target-environment", + default="production", + help="Release target environment label", + ) + parser.add_argument( + "--release-channel", + default="stable", + help="Release channel label", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_handoff.json", + help="Output release handoff JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_handoff.md", + help="Output release handoff markdown path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release handoff JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_publish_report_path = Path(args.terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_publish_report = load_terminal_publish_report(terminal_publish_report_path) + payload = build_release_handoff_payload( + terminal_publish_report, + source_path=terminal_publish_report_path.resolve(), + target_environment=args.target_environment, + release_channel=args.release_channel, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-handoff-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release handoff json: {output_json_path}") + print(f"release handoff markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release handoff summary: " + f"handoff_status={payload['handoff_status']} " + f"should_trigger_release={payload['should_trigger_release']} " + f"release_exit_code={payload['release_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_incident_gate.py b/scripts/run_p2_linux_ci_workflow_release_incident_gate.py new file mode 100644 index 0000000..3037aba --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_incident_gate.py @@ -0,0 +1,798 @@ +"""Phase 2 card P2-39 gate for Linux CI workflow release incident dispatch. + +This script consumes the P2-38 release verdict artifact and converges +the release-incident dispatch contract: +1) validate release verdict consistency + evidence contract, +2) build/execute incident dispatch command when incident is required, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_VERDICT_STATUSES: set[str] = { + "published", + "awaiting_archive", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_VERDICT_DECISIONS: set[str] = {"ship", "hold", "block"} +ALLOWED_INCIDENT_DISPATCH_STATUSES: set[str] = { + "not_required", + "ready_dry_run", + "dispatched", + "dispatch_failed", + "contract_failed", +} + +URL_PATTERN = re.compile(r"https?://[^\s]+") + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _extract_url(text: str) -> str: + for line in text.splitlines(): + match = URL_PATTERN.search(line) + if match: + return match.group(0) + return "" + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release verdict report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release verdict payload must be object") + + required_fields = ( + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_publish_archive", + "should_ship_release", + "should_open_incident", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_archive_status = _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ) + release_archive_decision = _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ) + release_archive_exit_code = _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ) + if release_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_archive_exit_code' must be >= 0") + + release_verdict_status = _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ) + if release_verdict_status not in ALLOWED_RELEASE_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_VERDICT_STATUSES)}" + ) + + release_verdict_decision = _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ) + if release_verdict_decision not in ALLOWED_RELEASE_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_VERDICT_DECISIONS)}" + ) + + release_verdict_exit_code = _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ) + if release_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_verdict_exit_code' must be >= 0") + + should_publish_archive = _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ) + should_ship_release = _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ) + should_open_incident = _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_archive_report": _coerce_str( + payload.get("source_release_archive_report", ""), + field="source_release_archive_report", + path=path, + ), + "release_archive_status": release_archive_status, + "release_archive_decision": release_archive_decision, + "release_archive_exit_code": release_archive_exit_code, + "release_verdict_status": release_verdict_status, + "release_verdict_decision": release_verdict_decision, + "release_verdict_exit_code": release_verdict_exit_code, + "should_publish_archive": should_publish_archive, + "should_ship_release": should_ship_release, + "should_open_incident": should_open_incident, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def build_incident_command_parts( + verdict_report: dict[str, Any], + *, + source_report_path: Path, + gh_executable: str, + incident_command: str, + incident_repo: str, + incident_label: str, + incident_title_prefix: str, +) -> list[str]: + if incident_command.strip(): + try: + custom_parts = shlex.split(incident_command) + except ValueError as exc: + raise ValueError(f"invalid --incident-command ({exc})") from exc + if not custom_parts: + raise ValueError("invalid --incident-command (empty command)") + return custom_parts + + verdict_status = str(verdict_report["release_verdict_status"]) + verdict_decision = str(verdict_report["release_verdict_decision"]) + release_run_url = str(verdict_report["release_run_url"]) + reason_codes = list(verdict_report["reason_codes"]) + reason_codes_text = ", ".join(reason_codes) if reason_codes else "none" + title = ( + f"{incident_title_prefix} release_verdict_status={verdict_status} " + f"release_verdict_decision={verdict_decision}" + ) + body = "\n".join( + [ + "Automated incident opened by P2-39 release incident gate.", + "", + f"- release_verdict_status: {verdict_status}", + f"- release_verdict_decision: {verdict_decision}", + f"- release_verdict_exit_code: {verdict_report['release_verdict_exit_code']}", + f"- release_run_id: {verdict_report['release_run_id']}", + f"- release_run_url: {release_run_url}", + f"- source_release_verdict_report: {source_report_path}", + f"- reason_codes: {reason_codes_text}", + ] + ) + + parts = [ + gh_executable, + "issue", + "create", + "--title", + title, + "--body", + body, + ] + if incident_label.strip(): + parts.extend(["--label", incident_label]) + if incident_repo.strip(): + parts.extend(["--repo", incident_repo]) + return parts + + +def execute_incident_command( + command_parts: list[str], *, cwd: Path, timeout_seconds: int +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"incident command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_release_incident_payload( + verdict_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + incident_repo: str, + incident_label: str, + incident_title_prefix: str, + incident_command_parts: list[str], + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + verdict_status = str(verdict_report["release_verdict_status"]) + verdict_decision = str(verdict_report["release_verdict_decision"]) + verdict_exit_code = int(verdict_report["release_verdict_exit_code"]) + should_ship_release = bool(verdict_report["should_ship_release"]) + should_open_incident = bool(verdict_report["should_open_incident"]) + + reason_codes = list(verdict_report["reason_codes"]) + structural_issues = list(verdict_report["structural_issues"]) + missing_artifacts = list(verdict_report["missing_artifacts"]) + evidence_manifest = list(verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_incident_evidence_missing") + + if verdict_status == "published": + if verdict_decision != "ship": + structural_issues.append("release_verdict_decision_mismatch_published") + if verdict_exit_code != 0: + structural_issues.append("release_verdict_exit_code_mismatch_published") + if not should_ship_release: + structural_issues.append("should_ship_release_mismatch_published") + if should_open_incident: + structural_issues.append("should_open_incident_mismatch_published") + elif verdict_status == "awaiting_archive": + if verdict_decision != "hold": + structural_issues.append("release_verdict_decision_mismatch_awaiting_archive") + if should_ship_release: + structural_issues.append("should_ship_release_mismatch_awaiting_archive") + if should_open_incident: + structural_issues.append("should_open_incident_mismatch_awaiting_archive") + elif verdict_status in {"blocked", "contract_failed"}: + if verdict_decision != "block": + structural_issues.append("release_verdict_decision_mismatch_blocked") + if should_ship_release: + structural_issues.append("should_ship_release_mismatch_blocked") + if not should_open_incident: + structural_issues.append("should_open_incident_mismatch_blocked") + if verdict_exit_code == 0: + structural_issues.append("release_verdict_exit_code_mismatch_blocked") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + reason_codes = _unique(reason_codes) + + incident_command = ( + _format_shell_command(incident_command_parts) if incident_command_parts else "" + ) + should_dispatch_incident = should_open_incident and not structural_issues and not missing_artifacts + + incident_dispatch_attempted = False + incident_dispatch_status = "not_required" + incident_dispatch_exit_code = 0 + incident_url = "" + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + incident_error_type = "" + incident_error_message = "" + + if structural_issues or missing_artifacts: + incident_dispatch_status = "contract_failed" + incident_dispatch_exit_code = 1 + reason_codes.extend(structural_issues) + elif not should_dispatch_incident: + incident_dispatch_status = "not_required" + incident_dispatch_exit_code = 0 + if verdict_status == "published": + reason_codes = ["incident_not_required_release_published"] + elif dry_run: + incident_dispatch_status = "ready_dry_run" + incident_dispatch_exit_code = 0 + else: + incident_dispatch_attempted = True + if command_result is None: + incident_dispatch_status = "dispatch_failed" + incident_dispatch_exit_code = 1 + reason_codes.append("incident_command_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + incident_error_type = str(command_result.get("error_type", "")) + incident_error_message = str(command_result.get("error_message", "")) + if command_returncode == 0: + incident_dispatch_status = "dispatched" + incident_dispatch_exit_code = 0 + incident_url = _extract_url(command_stdout_tail) + reason_codes = ["release_incident_dispatched"] + else: + incident_dispatch_status = "dispatch_failed" + incident_dispatch_exit_code = 1 + if incident_error_type == "command_not_found": + reason_codes.append("release_incident_cli_unavailable") + elif incident_error_type == "timeout": + reason_codes.append("release_incident_timeout") + else: + reason_codes.append("release_incident_command_failed") + + reason_codes = _unique(reason_codes) + + if incident_dispatch_status not in ALLOWED_INCIDENT_DISPATCH_STATUSES: + raise ValueError("internal: unsupported incident dispatch status computed") + + incident_dispatch_summary = ( + f"release_verdict_status={verdict_status} " + f"incident_dispatch_status={incident_dispatch_status} " + f"should_dispatch_incident={should_dispatch_incident}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_verdict_report": str(source_path), + "project_root": str(project_root), + "source_release_archive_report": str(verdict_report["source_release_archive_report"]), + "release_archive_status": str(verdict_report["release_archive_status"]), + "release_archive_decision": str(verdict_report["release_archive_decision"]), + "release_archive_exit_code": int(verdict_report["release_archive_exit_code"]), + "release_verdict_status": verdict_status, + "release_verdict_decision": verdict_decision, + "release_verdict_exit_code": verdict_exit_code, + "should_publish_archive": bool(verdict_report["should_publish_archive"]), + "should_ship_release": should_ship_release, + "should_open_incident": should_open_incident, + "should_dispatch_incident": should_dispatch_incident, + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": int(incident_dispatch_exit_code), + "incident_dispatch_attempted": incident_dispatch_attempted, + "incident_command": incident_command, + "incident_command_parts": list(incident_command_parts), + "incident_command_returncode": command_returncode, + "incident_command_stdout_tail": command_stdout_tail, + "incident_command_stderr_tail": command_stderr_tail, + "incident_error_type": incident_error_type, + "incident_error_message": incident_error_message, + "incident_repo": incident_repo, + "incident_label": incident_label, + "incident_title_prefix": incident_title_prefix, + "incident_url": incident_url, + "release_run_id": verdict_report["release_run_id"], + "release_run_url": str(verdict_report["release_run_url"]), + "incident_dispatch_summary": incident_dispatch_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Incident Dispatch Report", + "", + f"- Incident Dispatch Status: **{str(payload['incident_dispatch_status']).upper()}**", + f"- Incident Dispatch Exit Code: `{payload['incident_dispatch_exit_code']}`", + f"- Should Open Incident: `{payload['should_open_incident']}`", + f"- Should Dispatch Incident: `{payload['should_dispatch_incident']}`", + f"- Incident Dispatch Attempted: `{payload['incident_dispatch_attempted']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Verdict Status: `{payload['release_verdict_status']}`", + f"- Release Verdict Decision: `{payload['release_verdict_decision']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Incident Command", + ] + + incident_command = str(payload["incident_command"]) + if incident_command: + lines.append(f"- `{incident_command}`") + else: + lines.append("- none") + + lines.extend(["", "## Summary", "", f"- {payload['incident_dispatch_summary']}", "", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Incident Command Output (tail)"]) + stdout_tail = str(payload["incident_command_stdout_tail"]).strip() + stderr_tail = str(payload["incident_command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + command_returncode = payload["incident_command_returncode"] + return { + "workflow_release_incident_status": str(payload["incident_dispatch_status"]), + "workflow_release_incident_should_dispatch": ( + "true" if payload["should_dispatch_incident"] else "false" + ), + "workflow_release_incident_attempted": ( + "true" if payload["incident_dispatch_attempted"] else "false" + ), + "workflow_release_incident_exit_code": str(payload["incident_dispatch_exit_code"]), + "workflow_release_incident_command_returncode": ( + "" if command_returncode is None else str(command_returncode) + ), + "workflow_release_incident_url": str(payload["incident_url"]), + "workflow_release_incident_repo": str(payload["incident_repo"]), + "workflow_release_incident_label": str(payload["incident_label"]), + "workflow_release_incident_verdict_status": str(payload["release_verdict_status"]), + "workflow_release_incident_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_incident_run_url": str(payload["release_run_url"]), + "workflow_release_incident_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_incident_report_json": str(output_json), + "workflow_release_incident_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Run Linux CI workflow release incident gate from P2-38 release verdict report" + ) + ) + parser.add_argument( + "--release-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_verdict.json", + help="P2-38 release verdict report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for incident command execution", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable used when auto-building incident command", + ) + parser.add_argument( + "--incident-command", + default="", + help="Optional explicit incident command string (overrides auto command)", + ) + parser.add_argument( + "--incident-repo", + default="", + help="Optional GitHub repo for incident creation (OWNER/REPO)", + ) + parser.add_argument( + "--incident-label", + default="release-incident", + help="Incident label forwarded to auto-built command", + ) + parser.add_argument( + "--incident-title-prefix", + default="[release-incident]", + help="Incident title prefix for auto-built command", + ) + parser.add_argument( + "--incident-timeout-seconds", + type=int, + default=600, + help="Incident command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_incident.json", + help="Output release incident JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_incident.md", + help="Output release incident markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release incident JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate contract and skip incident command execution", + ) + args = parser.parse_args() + + if args.incident_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-release-incident-gate] " + "--incident-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + release_verdict_report_path = Path(args.release_verdict_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_release_verdict_report(release_verdict_report_path) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-incident-gate] {exc}", file=sys.stderr) + return 2 + + incident_command_parts: list[str] = [] + if verdict_report["should_open_incident"]: + try: + incident_command_parts = build_incident_command_parts( + verdict_report, + source_report_path=release_verdict_report_path.resolve(), + gh_executable=args.gh_executable, + incident_command=args.incident_command, + incident_repo=args.incident_repo, + incident_label=args.incident_label, + incident_title_prefix=args.incident_title_prefix, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-incident-gate] {exc}", file=sys.stderr) + return 2 + + command_result: dict[str, Any] | None = None + if verdict_report["should_open_incident"] and not args.dry_run: + command_result = execute_incident_command( + incident_command_parts, + cwd=project_root, + timeout_seconds=args.incident_timeout_seconds, + ) + + payload = build_release_incident_payload( + verdict_report, + source_path=release_verdict_report_path.resolve(), + project_root=project_root.resolve(), + incident_repo=args.incident_repo, + incident_label=args.incident_label, + incident_title_prefix=args.incident_title_prefix, + incident_command_parts=incident_command_parts, + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release incident json: {output_json_path}") + print(f"release incident markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release incident summary: " + f"incident_dispatch_status={payload['incident_dispatch_status']} " + f"should_dispatch_incident={payload['should_dispatch_incident']} " + f"incident_dispatch_exit_code={payload['incident_dispatch_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["incident_dispatch_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py b/scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py new file mode 100644 index 0000000..3bcfe63 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py @@ -0,0 +1,525 @@ +"""Phase 2 card P2-34 gate for Linux CI workflow release terminal publish. + +This script converts P2-33 release completion artifacts into a single +release terminal publish contract: +1) normalize release completion verdict to final release publish status, +2) provide finalize/hold decision fields for downstream release governance, +3) emit JSON/Markdown reports + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_COMPLETION_STATUSES: set[str] = { + "blocked", + "awaiting_completion", + "handoff_failed", + "release_trigger_failed", + "release_ready_dry_run", + "release_run_tracking_missing", + "release_run_tracking_ready", + "release_run_in_progress", + "release_run_completed_success", + "release_run_completed_failure", + "release_run_poll_failed", + "release_run_await_timeout", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_completion_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release completion report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release completion payload must be object") + + required_fields = ( + "release_trigger_status", + "release_tracking_status", + "release_completion_status", + "release_completion_exit_code", + "allow_in_progress", + "should_poll_release_run", + "release_run_id", + "release_run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_release_trace_report", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_trigger_status = _coerce_str( + payload["release_trigger_status"], + field="release_trigger_status", + path=path, + ) + release_tracking_status = _coerce_str( + payload["release_tracking_status"], + field="release_tracking_status", + path=path, + ) + release_completion_status = _coerce_str( + payload["release_completion_status"], + field="release_completion_status", + path=path, + ) + if release_completion_status not in ALLOWED_RELEASE_COMPLETION_STATUSES: + raise ValueError( + f"{path}: field 'release_completion_status' must be one of " + f"{sorted(ALLOWED_RELEASE_COMPLETION_STATUSES)}" + ) + + release_completion_exit_code = _coerce_int( + payload["release_completion_exit_code"], + field="release_completion_exit_code", + path=path, + ) + if release_completion_exit_code < 0: + raise ValueError(f"{path}: field 'release_completion_exit_code' must be >= 0") + + allow_in_progress = _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ) + should_poll_release_run = _coerce_bool( + payload["should_poll_release_run"], + field="should_poll_release_run", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_release_trace_report = _coerce_str( + payload["source_release_trace_report"], + field="source_release_trace_report", + path=path, + ) + source_release_trigger_report = _coerce_str( + payload["source_release_trigger_report"], + field="source_release_trigger_report", + path=path, + ) + source_release_handoff_report = _coerce_str( + payload["source_release_handoff_report"], + field="source_release_handoff_report", + path=path, + ) + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "release_trigger_status": release_trigger_status, + "release_tracking_status": release_tracking_status, + "release_completion_status": release_completion_status, + "release_completion_exit_code": release_completion_exit_code, + "allow_in_progress": allow_in_progress, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_release_trace_report": source_release_trace_report, + "source_release_trigger_report": source_release_trigger_report, + "source_release_handoff_report": source_release_handoff_report, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_release_terminal_publish_payload( + completion_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_completion_status = str(completion_report["release_completion_status"]) + release_completion_exit_code = int(completion_report["release_completion_exit_code"]) + allow_in_progress = bool(completion_report["allow_in_progress"]) + + structural_issues = list(completion_report["structural_issues"]) + reason_codes = list(completion_report["reason_codes"]) + + failure_statuses = { + "release_run_tracking_missing", + "release_run_completed_failure", + "release_run_poll_failed", + "release_trigger_failed", + "handoff_failed", + } + blocked_statuses = { + "blocked", + "release_ready_dry_run", + "awaiting_completion", + "release_run_tracking_ready", + } + in_progress_statuses = {"release_run_in_progress", "release_run_await_timeout"} + + if ( + release_completion_status == "release_run_completed_success" + and release_completion_exit_code != 0 + ): + structural_issues.append("release_completion_exit_code_mismatch_success") + if ( + release_completion_status in failure_statuses + and release_completion_exit_code == 0 + ): + structural_issues.append("release_completion_exit_code_mismatch_failure") + if release_completion_status in blocked_statuses and release_completion_exit_code != 0: + structural_issues.append("release_completion_exit_code_mismatch_blocked") + if ( + release_completion_status == "release_run_await_timeout" + and release_completion_exit_code == 0 + and not allow_in_progress + ): + structural_issues.append("release_completion_timeout_policy_mismatch") + + structural_issues = _unique(structural_issues) + + if structural_issues: + release_publish_status = "contract_failed" + release_publish_exit_code = 1 + should_finalize_release = False + reason_codes.extend(structural_issues) + elif release_completion_status == "release_run_completed_success": + release_publish_status = "passed" + release_publish_exit_code = 0 + should_finalize_release = True + reason_codes = ["release_completed_success"] + elif release_completion_status in blocked_statuses: + release_publish_status = "blocked" + release_publish_exit_code = release_completion_exit_code + should_finalize_release = False + elif release_completion_status in in_progress_statuses: + if release_completion_exit_code == 0: + release_publish_status = "in_progress" + release_publish_exit_code = 0 + else: + release_publish_status = "failed" + release_publish_exit_code = release_completion_exit_code + should_finalize_release = False + else: + release_publish_status = "failed" + release_publish_exit_code = ( + 1 if release_completion_exit_code == 0 else release_completion_exit_code + ) + should_finalize_release = False + + release_terminal_publish_summary = ( + f"release_completion_status={release_completion_status} " + f"release_publish_status={release_publish_status} " + f"should_finalize_release={should_finalize_release}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_completion_report": str(source_path), + "release_trigger_status": str(completion_report["release_trigger_status"]), + "release_tracking_status": str(completion_report["release_tracking_status"]), + "release_completion_status": release_completion_status, + "release_completion_exit_code": release_completion_exit_code, + "release_publish_status": release_publish_status, + "release_publish_exit_code": int(release_publish_exit_code), + "should_finalize_release": should_finalize_release, + "release_terminal_publish_summary": release_terminal_publish_summary, + "allow_in_progress": allow_in_progress, + "should_poll_release_run": bool(completion_report["should_poll_release_run"]), + "release_run_id": completion_report["release_run_id"], + "release_run_url": str(completion_report["release_run_url"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(completion_report["failed_checks"]), + "structural_issues": structural_issues, + "missing_artifacts": list(completion_report["missing_artifacts"]), + "source_release_trace_report": str(completion_report["source_release_trace_report"]), + "source_release_trigger_report": str(completion_report["source_release_trigger_report"]), + "source_release_handoff_report": str(completion_report["source_release_handoff_report"]), + "source_terminal_publish_report": str(completion_report["source_terminal_publish_report"]), + "source_dispatch_completion_report": str( + completion_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str(completion_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str( + completion_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(completion_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Terminal Publish Report", + "", + f"- Release Publish Status: **{str(payload['release_publish_status']).upper()}**", + f"- Release Publish Exit Code: `{payload['release_publish_exit_code']}`", + f"- Should Finalize Release: `{payload['should_finalize_release']}`", + f"- Release Completion Status: `{payload['release_completion_status']}`", + f"- Release Completion Exit Code: `{payload['release_completion_exit_code']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Completion Report: `{payload['source_release_completion_report']}`", + f"- Source Release Trace Report: `{payload['source_release_trace_report']}`", + f"- Source Release Trigger Report: `{payload['source_release_trigger_report']}`", + f"- Source Release Handoff Report: `{payload['source_release_handoff_report']}`", + "", + "## Summary", + "", + f"- {payload['release_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_terminal_publish_status": str(payload["release_publish_status"]), + "workflow_release_terminal_publish_exit_code": str(payload["release_publish_exit_code"]), + "workflow_release_terminal_publish_should_finalize_release": ( + "true" if payload["should_finalize_release"] else "false" + ), + "workflow_release_terminal_publish_completion_status": str( + payload["release_completion_status"] + ), + "workflow_release_terminal_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_terminal_publish_run_url": str(payload["release_run_url"]), + "workflow_release_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_terminal_publish_report_json": str(output_json), + "workflow_release_terminal_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release terminal verdict from P2-33 completion report" + ) + ) + parser.add_argument( + "--release-completion-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_completion.json", + help="P2-33 release completion report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json", + help="Output release terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md", + help="Output release terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_completion_report_path = Path(args.release_completion_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + completion_report = load_release_completion_report(release_completion_report_path) + payload = build_release_terminal_publish_payload( + completion_report, + source_path=release_completion_report_path.resolve(), + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-release-terminal-publish-gate] {exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release terminal publish json: {output_json_path}") + print(f"release terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release terminal publish summary: " + f"release_publish_status={payload['release_publish_status']} " + f"should_finalize_release={payload['should_finalize_release']} " + f"release_publish_exit_code={payload['release_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py new file mode 100644 index 0000000..ae557f1 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py @@ -0,0 +1,657 @@ +"""Phase 2 card P2-40 gate for Linux CI workflow release terminal verdict. + +This script consumes the P2-39 release incident artifact and converges +the terminal release verdict contract: +1) validate release verdict + incident dispatch consistency, +2) normalize ship/hold/escalate/block terminal semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_VERDICT_STATUSES: set[str] = { + "published", + "awaiting_archive", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_VERDICT_DECISIONS: set[str] = {"ship", "hold", "block"} +ALLOWED_INCIDENT_DISPATCH_STATUSES: set[str] = { + "not_required", + "ready_dry_run", + "dispatched", + "dispatch_failed", + "contract_failed", +} +ALLOWED_RELEASE_TERMINAL_VERDICT_STATUSES: set[str] = { + "released", + "awaiting_archive", + "blocked_incident_ready_dry_run", + "blocked_incident_dispatched", + "blocked_incident_failed", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_TERMINAL_VERDICT_DECISIONS: set[str] = {"ship", "hold", "escalate", "block"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_incident_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release incident report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release incident payload must be object") + + required_fields = ( + "source_release_verdict_report", + "source_release_archive_report", + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "release_verdict_status", + "release_verdict_decision", + "release_verdict_exit_code", + "should_publish_archive", + "should_ship_release", + "should_open_incident", + "should_dispatch_incident", + "incident_dispatch_status", + "incident_dispatch_exit_code", + "incident_dispatch_attempted", + "incident_url", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + source_release_verdict_report = _coerce_str( + payload["source_release_verdict_report"], + field="source_release_verdict_report", + path=path, + ) + source_release_archive_report = _coerce_str( + payload["source_release_archive_report"], + field="source_release_archive_report", + path=path, + ) + release_archive_status = _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ) + release_archive_decision = _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ) + release_archive_exit_code = _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ) + if release_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_archive_exit_code' must be >= 0") + + release_verdict_status = _coerce_str( + payload["release_verdict_status"], + field="release_verdict_status", + path=path, + ) + if release_verdict_status not in ALLOWED_RELEASE_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'release_verdict_status' must be one of " + f"{sorted(ALLOWED_RELEASE_VERDICT_STATUSES)}" + ) + + release_verdict_decision = _coerce_str( + payload["release_verdict_decision"], + field="release_verdict_decision", + path=path, + ) + if release_verdict_decision not in ALLOWED_RELEASE_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'release_verdict_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_VERDICT_DECISIONS)}" + ) + + release_verdict_exit_code = _coerce_int( + payload["release_verdict_exit_code"], + field="release_verdict_exit_code", + path=path, + ) + if release_verdict_exit_code < 0: + raise ValueError(f"{path}: field 'release_verdict_exit_code' must be >= 0") + + should_publish_archive = _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ) + should_ship_release = _coerce_bool( + payload["should_ship_release"], + field="should_ship_release", + path=path, + ) + should_open_incident = _coerce_bool( + payload["should_open_incident"], + field="should_open_incident", + path=path, + ) + should_dispatch_incident = _coerce_bool( + payload["should_dispatch_incident"], + field="should_dispatch_incident", + path=path, + ) + + incident_dispatch_status = _coerce_str( + payload["incident_dispatch_status"], + field="incident_dispatch_status", + path=path, + ) + if incident_dispatch_status not in ALLOWED_INCIDENT_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'incident_dispatch_status' must be one of " + f"{sorted(ALLOWED_INCIDENT_DISPATCH_STATUSES)}" + ) + + incident_dispatch_exit_code = _coerce_int( + payload["incident_dispatch_exit_code"], + field="incident_dispatch_exit_code", + path=path, + ) + if incident_dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'incident_dispatch_exit_code' must be >= 0") + + incident_dispatch_attempted = _coerce_bool( + payload["incident_dispatch_attempted"], + field="incident_dispatch_attempted", + path=path, + ) + incident_url = _coerce_str(payload["incident_url"], field="incident_url", path=path) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_verdict_report": source_release_verdict_report, + "source_release_archive_report": source_release_archive_report, + "release_archive_status": release_archive_status, + "release_archive_decision": release_archive_decision, + "release_archive_exit_code": release_archive_exit_code, + "release_verdict_status": release_verdict_status, + "release_verdict_decision": release_verdict_decision, + "release_verdict_exit_code": release_verdict_exit_code, + "should_publish_archive": should_publish_archive, + "should_ship_release": should_ship_release, + "should_open_incident": should_open_incident, + "should_dispatch_incident": should_dispatch_incident, + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": incident_dispatch_attempted, + "incident_url": incident_url, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def build_release_terminal_verdict_payload( + release_incident_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_verdict_status = str(release_incident_report["release_verdict_status"]) + release_verdict_decision = str(release_incident_report["release_verdict_decision"]) + release_verdict_exit_code = int(release_incident_report["release_verdict_exit_code"]) + should_publish_archive = bool(release_incident_report["should_publish_archive"]) + should_ship_release = bool(release_incident_report["should_ship_release"]) + should_open_incident = bool(release_incident_report["should_open_incident"]) + should_dispatch_incident = bool(release_incident_report["should_dispatch_incident"]) + incident_dispatch_status = str(release_incident_report["incident_dispatch_status"]) + incident_dispatch_exit_code = int(release_incident_report["incident_dispatch_exit_code"]) + incident_dispatch_attempted = bool(release_incident_report["incident_dispatch_attempted"]) + incident_url = str(release_incident_report["incident_url"]) + + reason_codes = list(release_incident_report["reason_codes"]) + structural_issues = list(release_incident_report["structural_issues"]) + missing_artifacts = list(release_incident_report["missing_artifacts"]) + evidence_manifest = list(release_incident_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_terminal_evidence_missing") + + if release_verdict_status == "published": + if release_verdict_decision != "ship": + structural_issues.append("release_verdict_decision_mismatch_published") + if release_verdict_exit_code != 0: + structural_issues.append("release_verdict_exit_code_mismatch_published") + if not should_ship_release: + structural_issues.append("should_ship_release_mismatch_published") + if should_open_incident: + structural_issues.append("should_open_incident_mismatch_published") + if should_dispatch_incident: + structural_issues.append("should_dispatch_incident_mismatch_published") + if incident_dispatch_status != "not_required": + structural_issues.append("incident_dispatch_status_mismatch_published") + if incident_dispatch_exit_code != 0: + structural_issues.append("incident_dispatch_exit_code_mismatch_published") + elif release_verdict_status == "awaiting_archive": + if release_verdict_decision != "hold": + structural_issues.append("release_verdict_decision_mismatch_awaiting_archive") + if should_ship_release: + structural_issues.append("should_ship_release_mismatch_awaiting_archive") + if should_open_incident: + structural_issues.append("should_open_incident_mismatch_awaiting_archive") + if should_dispatch_incident: + structural_issues.append("should_dispatch_incident_mismatch_awaiting_archive") + if incident_dispatch_status != "not_required": + structural_issues.append("incident_dispatch_status_mismatch_awaiting_archive") + else: + if release_verdict_decision != "block": + structural_issues.append("release_verdict_decision_mismatch_blocked") + if should_ship_release: + structural_issues.append("should_ship_release_mismatch_blocked") + if not should_open_incident: + structural_issues.append("should_open_incident_mismatch_blocked") + if should_dispatch_incident and incident_dispatch_status == "not_required": + structural_issues.append("incident_dispatch_status_mismatch_requires_dispatch") + if (not should_dispatch_incident) and incident_dispatch_status in { + "ready_dry_run", + "dispatched", + "dispatch_failed", + }: + structural_issues.append("incident_dispatch_status_mismatch_no_dispatch") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + release_terminal_verdict_status = "contract_failed" + release_terminal_verdict_decision = "block" + release_terminal_verdict_exit_code = 1 + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.extend(structural_issues) + elif release_verdict_status == "published": + release_terminal_verdict_status = "released" + release_terminal_verdict_decision = "ship" + release_terminal_verdict_exit_code = 0 + terminal_should_ship_release = True + terminal_requires_follow_up = False + reason_codes = ["release_terminal_released"] + elif release_verdict_status == "awaiting_archive": + release_terminal_verdict_status = "awaiting_archive" + release_terminal_verdict_decision = "hold" + release_terminal_verdict_exit_code = max(1, release_verdict_exit_code) + terminal_should_ship_release = False + terminal_requires_follow_up = True + elif incident_dispatch_status == "dispatched": + release_terminal_verdict_status = "blocked_incident_dispatched" + release_terminal_verdict_decision = "escalate" + release_terminal_verdict_exit_code = max(1, incident_dispatch_exit_code) + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.append("release_terminal_blocked_incident_dispatched") + elif incident_dispatch_status == "ready_dry_run": + release_terminal_verdict_status = "blocked_incident_ready_dry_run" + release_terminal_verdict_decision = "escalate" + release_terminal_verdict_exit_code = max( + 1, + release_verdict_exit_code, + incident_dispatch_exit_code, + ) + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.append("release_terminal_blocked_incident_ready_dry_run") + elif incident_dispatch_status == "dispatch_failed": + release_terminal_verdict_status = "blocked_incident_failed" + release_terminal_verdict_decision = "block" + release_terminal_verdict_exit_code = max(1, incident_dispatch_exit_code) + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.append("release_terminal_incident_dispatch_failed") + elif incident_dispatch_status == "contract_failed": + release_terminal_verdict_status = "contract_failed" + release_terminal_verdict_decision = "block" + release_terminal_verdict_exit_code = 1 + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.append("release_terminal_incident_contract_failed") + else: + release_terminal_verdict_status = "blocked" + release_terminal_verdict_decision = "block" + release_terminal_verdict_exit_code = max( + 1, + release_verdict_exit_code, + incident_dispatch_exit_code, + ) + terminal_should_ship_release = False + terminal_requires_follow_up = True + reason_codes.append("release_terminal_blocked") + + if release_terminal_verdict_status not in ALLOWED_RELEASE_TERMINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported release_terminal_verdict_status computed") + if release_terminal_verdict_decision not in ALLOWED_RELEASE_TERMINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported release_terminal_verdict_decision computed") + + reason_codes = _unique(reason_codes) + incident_linked = incident_dispatch_status in { + "ready_dry_run", + "dispatched", + "dispatch_failed", + } + release_terminal_verdict_summary = ( + f"release_verdict_status={release_verdict_status} " + f"incident_dispatch_status={incident_dispatch_status} " + f"release_terminal_verdict_status={release_terminal_verdict_status} " + f"release_terminal_verdict_decision={release_terminal_verdict_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_incident_report": str(source_path), + "source_release_verdict_report": str(release_incident_report["source_release_verdict_report"]), + "source_release_archive_report": str(release_incident_report["source_release_archive_report"]), + "release_archive_status": str(release_incident_report["release_archive_status"]), + "release_archive_decision": str(release_incident_report["release_archive_decision"]), + "release_archive_exit_code": int(release_incident_report["release_archive_exit_code"]), + "should_publish_archive": should_publish_archive, + "release_verdict_status": release_verdict_status, + "release_verdict_decision": release_verdict_decision, + "release_verdict_exit_code": release_verdict_exit_code, + "should_ship_release": should_ship_release, + "should_open_incident": should_open_incident, + "should_dispatch_incident": should_dispatch_incident, + "incident_dispatch_status": incident_dispatch_status, + "incident_dispatch_exit_code": incident_dispatch_exit_code, + "incident_dispatch_attempted": incident_dispatch_attempted, + "incident_url": incident_url, + "release_terminal_verdict_status": release_terminal_verdict_status, + "release_terminal_verdict_decision": release_terminal_verdict_decision, + "release_terminal_verdict_exit_code": int(release_terminal_verdict_exit_code), + "terminal_should_ship_release": terminal_should_ship_release, + "terminal_requires_follow_up": terminal_requires_follow_up, + "terminal_incident_linked": incident_linked, + "release_run_id": release_incident_report["release_run_id"], + "release_run_url": str(release_incident_report["release_run_url"]), + "release_terminal_verdict_summary": release_terminal_verdict_summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Terminal Verdict Report", + "", + f"- Terminal Verdict Status: **{str(payload['release_terminal_verdict_status']).upper()}**", + f"- Terminal Verdict Decision: `{payload['release_terminal_verdict_decision']}`", + f"- Terminal Verdict Exit Code: `{payload['release_terminal_verdict_exit_code']}`", + f"- Terminal Should Ship Release: `{payload['terminal_should_ship_release']}`", + f"- Terminal Requires Follow Up: `{payload['terminal_requires_follow_up']}`", + f"- Incident Dispatch Status: `{payload['incident_dispatch_status']}`", + f"- Incident URL: `{payload['incident_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_terminal_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_terminal_verdict_status": str(payload["release_terminal_verdict_status"]), + "workflow_release_terminal_verdict_decision": str( + payload["release_terminal_verdict_decision"] + ), + "workflow_release_terminal_verdict_exit_code": str( + payload["release_terminal_verdict_exit_code"] + ), + "workflow_release_terminal_verdict_should_ship_release": ( + "true" if payload["terminal_should_ship_release"] else "false" + ), + "workflow_release_terminal_verdict_requires_follow_up": ( + "true" if payload["terminal_requires_follow_up"] else "false" + ), + "workflow_release_terminal_verdict_incident_status": str(payload["incident_dispatch_status"]), + "workflow_release_terminal_verdict_incident_url": str(payload["incident_url"]), + "workflow_release_terminal_verdict_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_release_terminal_verdict_run_url": str(payload["release_run_url"]), + "workflow_release_terminal_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_terminal_verdict_report_json": str(output_json), + "workflow_release_terminal_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release terminal verdict contract " + "from P2-39 release incident report" + ) + ) + parser.add_argument( + "--release-incident-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_incident.json", + help="P2-39 release incident report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json", + help="Output release terminal verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md", + help="Output release terminal verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release terminal verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_incident_report_path = Path(args.release_incident_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_incident_report = load_release_incident_report(release_incident_report_path) + payload = build_release_terminal_verdict_payload( + release_incident_report, + source_path=release_incident_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-terminal-verdict-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release terminal verdict json: {output_json_path}") + print(f"release terminal verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release terminal verdict summary: " + f"release_terminal_verdict_status={payload['release_terminal_verdict_status']} " + f"release_terminal_verdict_decision={payload['release_terminal_verdict_decision']} " + f"release_terminal_verdict_exit_code={payload['release_terminal_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_terminal_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_trace_gate.py b/scripts/run_p2_linux_ci_workflow_release_trace_gate.py new file mode 100644 index 0000000..5b3f941 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_trace_gate.py @@ -0,0 +1,794 @@ +"""Phase 2 card P2-32 gate for Linux CI workflow release traceability. + +This script consumes the P2-31 release trigger artifact and converges a +release-run trace contract: +1) validate release trigger contract and trigger status, +2) extract release run reference (run_id/url) for triggered releases, +3) optionally poll release run status immediately, +4) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +RUN_URL_PATTERN = re.compile( + r"https://github\.com/(?P[A-Za-z0-9_.-]+)/(?P[A-Za-z0-9_.-]+)/actions/runs/(?P\d+)" +) + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_trigger_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release trigger report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release trigger payload must be object") + + required_fields = ( + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + "handoff_status", + "should_trigger_release", + "release_trigger_status", + "release_trigger_exit_code", + "release_trigger_attempted", + "release_command", + "release_command_parts", + "release_workflow_path", + "release_workflow_ref", + "target_environment", + "release_channel", + "run_id", + "run_url", + "command_returncode", + "command_stdout_tail", + "command_stderr_tail", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_trigger_status = _coerce_str( + payload["release_trigger_status"], + field="release_trigger_status", + path=path, + ) + allowed_release_trigger_statuses = { + "ready_dry_run", + "awaiting_completion", + "blocked", + "handoff_failed", + "triggered", + "trigger_failed", + } + if release_trigger_status not in allowed_release_trigger_statuses: + raise ValueError( + f"{path}: field 'release_trigger_status' must be one of " + f"{sorted(allowed_release_trigger_statuses)}" + ) + + should_trigger_release = _coerce_bool( + payload["should_trigger_release"], + field="should_trigger_release", + path=path, + ) + release_trigger_exit_code = _coerce_int( + payload["release_trigger_exit_code"], + field="release_trigger_exit_code", + path=path, + ) + if release_trigger_exit_code < 0: + raise ValueError(f"{path}: field 'release_trigger_exit_code' must be >= 0") + + release_trigger_attempted = _coerce_bool( + payload["release_trigger_attempted"], + field="release_trigger_attempted", + path=path, + ) + release_command = _coerce_str(payload["release_command"], field="release_command", path=path) + release_command_parts = _coerce_str_list( + payload["release_command_parts"], + field="release_command_parts", + path=path, + ) + release_workflow_path = _coerce_str( + payload["release_workflow_path"], + field="release_workflow_path", + path=path, + ) + release_workflow_ref = _coerce_str( + payload["release_workflow_ref"], + field="release_workflow_ref", + path=path, + ) + handoff_status = _coerce_str(payload["handoff_status"], field="handoff_status", path=path) + target_environment = _coerce_str( + payload["target_environment"], + field="target_environment", + path=path, + ) + release_channel = _coerce_str(payload["release_channel"], field="release_channel", path=path) + + run_id_raw = payload["run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="run_id", path=path) + if run_id < 1: + raise ValueError(f"{path}: field 'run_id' must be >= 1 when present") + run_url = _coerce_str(payload["run_url"], field="run_url", path=path) + + command_returncode_raw = payload["command_returncode"] + if command_returncode_raw is None: + command_returncode = None + else: + command_returncode = _coerce_int( + command_returncode_raw, + field="command_returncode", + path=path, + ) + command_stdout_tail = _coerce_str( + payload["command_stdout_tail"], + field="command_stdout_tail", + path=path, + ) + command_stderr_tail = _coerce_str( + payload["command_stderr_tail"], + field="command_stderr_tail", + path=path, + ) + + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + source_release_handoff_report = _coerce_str( + payload["source_release_handoff_report"], + field="source_release_handoff_report", + path=path, + ) + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + if release_trigger_status == "triggered": + if not should_trigger_release: + raise ValueError(f"{path}: triggered requires should_trigger_release=true") + if not release_trigger_attempted: + raise ValueError(f"{path}: triggered requires release_trigger_attempted=true") + if release_trigger_exit_code != 0: + raise ValueError(f"{path}: triggered requires release_trigger_exit_code=0") + if command_returncode != 0: + raise ValueError(f"{path}: triggered requires command_returncode=0") + elif release_trigger_status == "trigger_failed": + if not should_trigger_release: + raise ValueError(f"{path}: trigger_failed requires should_trigger_release=true") + if not release_trigger_attempted: + raise ValueError(f"{path}: trigger_failed requires release_trigger_attempted=true") + if release_trigger_exit_code == 0: + raise ValueError(f"{path}: trigger_failed requires release_trigger_exit_code>0") + elif release_trigger_status == "ready_dry_run": + if not should_trigger_release: + raise ValueError(f"{path}: ready_dry_run requires should_trigger_release=true") + if release_trigger_attempted: + raise ValueError(f"{path}: ready_dry_run requires release_trigger_attempted=false") + if release_trigger_exit_code != 0: + raise ValueError(f"{path}: ready_dry_run requires release_trigger_exit_code=0") + elif release_trigger_status in {"awaiting_completion", "blocked"}: + if should_trigger_release: + raise ValueError( + f"{path}: {release_trigger_status} requires should_trigger_release=false" + ) + if release_trigger_exit_code != 0: + raise ValueError( + f"{path}: {release_trigger_status} requires release_trigger_exit_code=0" + ) + else: + if should_trigger_release: + raise ValueError(f"{path}: handoff_failed requires should_trigger_release=false") + if release_trigger_exit_code == 0: + raise ValueError(f"{path}: handoff_failed requires release_trigger_exit_code>0") + + return { + "generated_at": payload.get("generated_at"), + "source_release_handoff_report": source_release_handoff_report, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + "handoff_status": handoff_status, + "should_trigger_release": should_trigger_release, + "release_trigger_status": release_trigger_status, + "release_trigger_exit_code": release_trigger_exit_code, + "release_trigger_attempted": release_trigger_attempted, + "release_command": release_command, + "release_command_parts": release_command_parts, + "release_workflow_path": release_workflow_path, + "release_workflow_ref": release_workflow_ref, + "target_environment": target_environment, + "release_channel": release_channel, + "run_id": run_id, + "run_url": run_url, + "command_returncode": command_returncode, + "command_stdout_tail": command_stdout_tail, + "command_stderr_tail": command_stderr_tail, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def extract_run_reference(*, stdout_tail: str, stderr_tail: str) -> dict[str, Any]: + merged = "\n".join(item for item in (stdout_tail, stderr_tail) if item) + match = RUN_URL_PATTERN.search(merged) + if not match: + return { + "run_url": "", + "run_id": None, + "repo_owner": "", + "repo_name": "", + } + run_url = match.group(0) + return { + "run_url": run_url, + "run_id": int(match.group("run_id")), + "repo_owner": match.group("owner"), + "repo_name": match.group("repo"), + } + + +def build_run_poll_command( + *, gh_executable: str, run_id: int, repo_owner: str, repo_name: str +) -> list[str]: + command = [ + gh_executable, + "run", + "view", + str(run_id), + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + ] + if repo_owner and repo_name: + command.extend(["--repo", f"{repo_owner}/{repo_name}"]) + return command + + +def execute_run_poll_command( + command_parts: list[str], *, cwd: Path, timeout_seconds: int +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"release run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def build_release_trace_payload( + release_trigger_report: dict[str, Any], + *, + source_path: Path, + gh_executable: str, + poll_now: bool, + dry_run: bool, + project_root: Path, + poll_timeout_seconds: int, +) -> dict[str, Any]: + release_trigger_status = str(release_trigger_report["release_trigger_status"]) + reason_codes = list(release_trigger_report["reason_codes"]) + + run_ref = extract_run_reference( + stdout_tail=str(release_trigger_report["command_stdout_tail"]), + stderr_tail=str(release_trigger_report["command_stderr_tail"]), + ) + release_run_id = run_ref["run_id"] + release_run_url = str(run_ref["run_url"]) + repo_owner = str(run_ref["repo_owner"]) + repo_name = str(run_ref["repo_name"]) + + should_poll_release_run = False + release_tracking_status = release_trigger_status + release_trace_exit_code = int(release_trigger_report["release_trigger_exit_code"]) + release_poll_command_parts: list[str] = [] + release_poll_command = "" + release_poll_attempted = False + release_poll_returncode: int | None = None + release_poll_status = "" + release_poll_conclusion = "" + release_poll_url = release_run_url + release_poll_error_type = "" + release_poll_error_message = "" + release_poll_stdout_tail = "" + release_poll_stderr_tail = "" + + if release_trigger_status == "triggered": + if release_run_id is None: + release_tracking_status = "release_run_tracking_missing" + release_trace_exit_code = 1 + reason_codes.append("release_run_reference_missing") + else: + should_poll_release_run = True + release_tracking_status = "release_run_tracking_ready" + release_poll_command_parts = build_run_poll_command( + gh_executable=gh_executable, + run_id=int(release_run_id), + repo_owner=repo_owner, + repo_name=repo_name, + ) + release_poll_command = _format_shell_command(release_poll_command_parts) + + if poll_now and not dry_run: + release_poll_attempted = True + poll_result = execute_run_poll_command( + release_poll_command_parts, + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + release_poll_returncode = int(poll_result["returncode"]) + release_poll_stdout_tail = str(poll_result["stdout_tail"]) + release_poll_stderr_tail = str(poll_result["stderr_tail"]) + release_poll_error_type = str(poll_result["error_type"]) + release_poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if release_poll_returncode != 0: + release_tracking_status = "release_run_poll_failed" + release_trace_exit_code = 1 + if release_poll_error_type == "command_not_found": + reason_codes.append("release_run_poll_cli_unavailable") + elif release_poll_error_type == "timeout": + reason_codes.append("release_run_poll_timeout") + else: + reason_codes.append("release_run_poll_command_failed") + elif poll_payload is None: + release_tracking_status = "release_run_poll_failed" + release_trace_exit_code = 1 + reason_codes.append("release_run_poll_payload_invalid") + else: + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + release_poll_status = poll_status_value + if isinstance(poll_conclusion_value, str): + release_poll_conclusion = poll_conclusion_value + if isinstance(poll_url_value, str) and poll_url_value: + release_poll_url = poll_url_value + + if release_poll_status == "completed": + if release_poll_conclusion == "success": + release_tracking_status = "release_run_completed_success" + release_trace_exit_code = 0 + reason_codes = ["release_run_completed_success"] + else: + release_tracking_status = "release_run_completed_failure" + release_trace_exit_code = 1 + reason_codes.append("release_run_completed_with_failure") + else: + release_tracking_status = "release_run_in_progress" + release_trace_exit_code = 0 + reason_codes.append("release_run_still_in_progress") + elif release_trigger_status == "ready_dry_run": + release_tracking_status = "release_ready_dry_run" + release_trace_exit_code = 0 + elif release_trigger_status == "awaiting_completion": + release_tracking_status = "awaiting_completion" + release_trace_exit_code = 0 + elif release_trigger_status == "blocked": + release_tracking_status = "blocked" + release_trace_exit_code = 0 + elif release_trigger_status == "handoff_failed": + release_tracking_status = "handoff_failed" + release_trace_exit_code = max(1, release_trace_exit_code) + elif release_trigger_status == "trigger_failed": + release_tracking_status = "release_trigger_failed" + release_trace_exit_code = max(1, release_trace_exit_code) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_trigger_report": str(source_path), + "project_root": str(project_root), + "source_release_handoff_report": str(release_trigger_report["source_release_handoff_report"]), + "source_terminal_publish_report": str(release_trigger_report["source_terminal_publish_report"]), + "source_dispatch_completion_report": str( + release_trigger_report["source_dispatch_completion_report"] + ), + "source_dispatch_trace_report": str(release_trigger_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str( + release_trigger_report["source_dispatch_execution_report"] + ), + "source_dispatch_report": str(release_trigger_report["source_dispatch_report"]), + "handoff_status": str(release_trigger_report["handoff_status"]), + "release_trigger_status": release_trigger_status, + "release_trigger_exit_code": int(release_trigger_report["release_trigger_exit_code"]), + "should_trigger_release": bool(release_trigger_report["should_trigger_release"]), + "release_trigger_attempted": bool(release_trigger_report["release_trigger_attempted"]), + "target_environment": str(release_trigger_report["target_environment"]), + "release_channel": str(release_trigger_report["release_channel"]), + "release_workflow_path": str(release_trigger_report["release_workflow_path"]), + "release_workflow_ref": str(release_trigger_report["release_workflow_ref"]), + "release_tracking_status": release_tracking_status, + "release_trace_exit_code": int(release_trace_exit_code), + "poll_now": poll_now, + "dry_run": dry_run, + "should_poll_release_run": should_poll_release_run, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "repo_owner": repo_owner, + "repo_name": repo_name, + "release_poll_command": release_poll_command, + "release_poll_command_parts": release_poll_command_parts, + "release_poll_attempted": release_poll_attempted, + "release_poll_returncode": release_poll_returncode, + "release_poll_status": release_poll_status, + "release_poll_conclusion": release_poll_conclusion, + "release_poll_url": release_poll_url, + "release_poll_error_type": release_poll_error_type, + "release_poll_error_message": release_poll_error_message, + "release_poll_stdout_tail": release_poll_stdout_tail, + "release_poll_stderr_tail": release_poll_stderr_tail, + "release_trigger_reason_codes": list(release_trigger_report["reason_codes"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(release_trigger_report["failed_checks"]), + "structural_issues": list(release_trigger_report["structural_issues"]), + "missing_artifacts": list(release_trigger_report["missing_artifacts"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Trace Report", + "", + f"- Release Tracking Status: **{str(payload['release_tracking_status']).upper()}**", + f"- Release Trace Exit Code: `{payload['release_trace_exit_code']}`", + f"- Should Poll Release Run: `{payload['should_poll_release_run']}`", + f"- Release Poll Attempted: `{payload['release_poll_attempted']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Trigger Report: `{payload['source_release_trigger_report']}`", + f"- Source Release Handoff Report: `{payload['source_release_handoff_report']}`", + f"- Source Terminal Publish Report: `{payload['source_terminal_publish_report']}`", + f"- Source Dispatch Completion Report: `{payload['source_dispatch_completion_report']}`", + "", + "## Release Poll Command", + ] + + if payload["release_poll_command"]: + lines.append(f"- `{payload['release_poll_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Release Poll Result"]) + lines.append(f"- Release Poll Return Code: `{payload['release_poll_returncode']}`") + lines.append(f"- Release Poll Status: `{payload['release_poll_status']}`") + lines.append(f"- Release Poll Conclusion: `{payload['release_poll_conclusion']}`") + lines.append(f"- Release Poll URL: `{payload['release_poll_url']}`") + lines.append(f"- Release Poll Error Type: `{payload['release_poll_error_type']}`") + if payload["release_poll_stdout_tail"]: + lines.append("- Release Poll stdout:") + lines.append("```text") + lines.append(str(payload["release_poll_stdout_tail"])) + lines.append("```") + if payload["release_poll_stderr_tail"]: + lines.append("- Release Poll stderr:") + lines.append("```text") + lines.append(str(payload["release_poll_stderr_tail"])) + lines.append("```") + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_trace_status": str(payload["release_tracking_status"]), + "workflow_release_trace_should_poll": ( + "true" if payload["should_poll_release_run"] else "false" + ), + "workflow_release_trace_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_trace_run_url": str(payload["release_run_url"]), + "workflow_release_trace_poll_status": str(payload["release_poll_status"]), + "workflow_release_trace_poll_conclusion": str(payload["release_poll_conclusion"]), + "workflow_release_trace_exit_code": str(payload["release_trace_exit_code"]), + "workflow_release_trace_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_trace_report_json": str(output_json), + "workflow_release_trace_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Build Linux CI workflow release trace report from P2-31 release trigger artifact" + ) + ) + parser.add_argument( + "--release-trigger-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trigger.json", + help="P2-31 release trigger report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for optional release run poll command execution", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable for release run poll command", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Release run poll timeout in seconds when --poll-now is enabled", + ) + parser.add_argument( + "--poll-now", + action="store_true", + help="Execute release run poll command when run reference is available", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trace.json", + help="Output release trace JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trace.md", + help="Output release trace markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release trace JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-release-trace-gate] --poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + release_trigger_report_path = Path(args.release_trigger_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + release_trigger_report = load_release_trigger_report(release_trigger_report_path) + payload = build_release_trace_payload( + release_trigger_report, + source_path=release_trigger_report_path.resolve(), + gh_executable=args.gh_executable, + poll_now=args.poll_now, + dry_run=args.dry_run, + project_root=project_root, + poll_timeout_seconds=args.poll_timeout_seconds, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-trace-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release trace json: {output_json_path}") + print(f"release trace markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release trace summary: " + f"release_tracking_status={payload['release_tracking_status']} " + f"should_poll={payload['should_poll_release_run']} " + f"release_trace_exit_code={payload['release_trace_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_trace_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_trigger_gate.py b/scripts/run_p2_linux_ci_workflow_release_trigger_gate.py new file mode 100644 index 0000000..96412cc --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_trigger_gate.py @@ -0,0 +1,692 @@ +"""Phase 2 card P2-31 gate for Linux CI workflow release trigger. + +This script consumes the P2-30 release handoff artifact and converges the +final release trigger behavior: +1) validate handoff contract and trigger readiness, +2) build/execute release dispatch command when ready, +3) emit JSON/Markdown reports + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_HANDOFF_STATUSES: set[str] = { + "ready_for_release", + "awaiting_completion", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_HANDOFF_ACTIONS: set[str] = {"promote", "hold", "reject"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_handoff_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release handoff report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release handoff payload must be object") + + required_fields = ( + "handoff_status", + "handoff_action", + "should_trigger_release", + "release_exit_code", + "target_environment", + "release_channel", + "publish_status", + "completion_status", + "run_id", + "run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + handoff_status = _coerce_str(payload["handoff_status"], field="handoff_status", path=path) + if handoff_status not in ALLOWED_HANDOFF_STATUSES: + raise ValueError( + f"{path}: field 'handoff_status' must be one of {sorted(ALLOWED_HANDOFF_STATUSES)}" + ) + + handoff_action = _coerce_str(payload["handoff_action"], field="handoff_action", path=path) + if handoff_action not in ALLOWED_HANDOFF_ACTIONS: + raise ValueError( + f"{path}: field 'handoff_action' must be one of {sorted(ALLOWED_HANDOFF_ACTIONS)}" + ) + + should_trigger_release = _coerce_bool( + payload["should_trigger_release"], + field="should_trigger_release", + path=path, + ) + release_exit_code = _coerce_int(payload["release_exit_code"], field="release_exit_code", path=path) + if release_exit_code < 0: + raise ValueError(f"{path}: field 'release_exit_code' must be >= 0") + + target_environment = _coerce_str( + payload["target_environment"], + field="target_environment", + path=path, + ) + release_channel = _coerce_str(payload["release_channel"], field="release_channel", path=path) + publish_status = _coerce_str(payload["publish_status"], field="publish_status", path=path) + completion_status = _coerce_str(payload["completion_status"], field="completion_status", path=path) + run_url = _coerce_str(payload["run_url"], field="run_url", path=path) + + run_id_raw = payload["run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="run_id", path=path) + if run_id < 1: + raise ValueError(f"{path}: field 'run_id' must be >= 1 when present") + + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + source_terminal_publish_report = _coerce_str( + payload["source_terminal_publish_report"], + field="source_terminal_publish_report", + path=path, + ) + source_dispatch_completion_report = _coerce_str( + payload["source_dispatch_completion_report"], + field="source_dispatch_completion_report", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + if handoff_status == "ready_for_release": + if not should_trigger_release: + raise ValueError( + f"{path}: ready_for_release requires should_trigger_release=true" + ) + if handoff_action != "promote": + raise ValueError(f"{path}: ready_for_release requires handoff_action='promote'") + if release_exit_code != 0: + raise ValueError(f"{path}: ready_for_release requires release_exit_code=0") + elif handoff_status in {"awaiting_completion", "blocked"}: + if should_trigger_release: + raise ValueError( + f"{path}: {handoff_status} requires should_trigger_release=false" + ) + if handoff_action != "hold": + raise ValueError(f"{path}: {handoff_status} requires handoff_action='hold'") + if release_exit_code != 0: + raise ValueError(f"{path}: {handoff_status} requires release_exit_code=0") + elif handoff_status == "failed": + if should_trigger_release: + raise ValueError(f"{path}: failed requires should_trigger_release=false") + if handoff_action != "reject": + raise ValueError(f"{path}: failed requires handoff_action='reject'") + if release_exit_code == 0: + raise ValueError(f"{path}: failed requires release_exit_code>0") + else: + if should_trigger_release: + raise ValueError(f"{path}: contract_failed requires should_trigger_release=false") + if handoff_action != "hold": + raise ValueError(f"{path}: contract_failed requires handoff_action='hold'") + if release_exit_code == 0: + raise ValueError(f"{path}: contract_failed requires release_exit_code>0") + + return { + "generated_at": payload.get("generated_at"), + "handoff_status": handoff_status, + "handoff_action": handoff_action, + "should_trigger_release": should_trigger_release, + "release_exit_code": release_exit_code, + "target_environment": target_environment, + "release_channel": release_channel, + "publish_status": publish_status, + "completion_status": completion_status, + "run_id": run_id, + "run_url": run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_terminal_publish_report": source_terminal_publish_report, + "source_dispatch_completion_report": source_dispatch_completion_report, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_release_command_parts( + handoff_report: dict[str, Any], + *, + source_report_path: Path, + gh_executable: str, + release_workflow_path: str, + release_workflow_ref: str, + release_command: str, +) -> list[str]: + if release_command.strip(): + try: + custom_parts = shlex.split(release_command) + except ValueError as exc: + raise ValueError(f"invalid --release-command ({exc})") from exc + if not custom_parts: + raise ValueError("invalid --release-command (empty command)") + return custom_parts + + return [ + gh_executable, + "workflow", + "run", + release_workflow_path, + "--ref", + release_workflow_ref, + "--raw-field", + f"source_release_handoff_report={source_report_path}", + "--raw-field", + f"target_environment={handoff_report['target_environment']}", + "--raw-field", + f"release_channel={handoff_report['release_channel']}", + ] + + +def execute_release_trigger_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"release trigger command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_release_trigger_payload( + handoff_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + release_command_parts: list[str], + release_workflow_path: str, + release_workflow_ref: str, + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + should_trigger_release = bool(handoff_report["should_trigger_release"]) + handoff_status = str(handoff_report["handoff_status"]) + release_exit_code = int(handoff_report["release_exit_code"]) + + release_command = ( + _format_shell_command(release_command_parts) if release_command_parts else "" + ) + reason_codes = list(handoff_report["reason_codes"]) + release_trigger_attempted = False + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + trigger_error_type = "" + trigger_error_message = "" + + if should_trigger_release: + if dry_run: + release_trigger_status = "ready_dry_run" + release_trigger_exit_code = 0 + else: + release_trigger_attempted = True + if command_result is None: + release_trigger_status = "trigger_failed" + release_trigger_exit_code = 1 + reason_codes.append("release_trigger_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + trigger_error_type = str(command_result.get("error_type", "")) + trigger_error_message = str(command_result.get("error_message", "")) + if command_returncode == 0: + release_trigger_status = "triggered" + release_trigger_exit_code = 0 + reason_codes = ["release_trigger_command_succeeded"] + else: + release_trigger_status = "trigger_failed" + release_trigger_exit_code = 1 + if trigger_error_type == "command_not_found": + reason_codes.append("release_trigger_cli_unavailable") + elif trigger_error_type == "timeout": + reason_codes.append("release_trigger_timeout") + else: + reason_codes.append("release_trigger_command_failed") + else: + if handoff_status == "awaiting_completion": + release_trigger_status = "awaiting_completion" + release_trigger_exit_code = 0 + elif handoff_status == "blocked": + release_trigger_status = "blocked" + release_trigger_exit_code = 0 + else: + release_trigger_status = "handoff_failed" + release_trigger_exit_code = max(1, release_exit_code) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_handoff_report": str(source_path), + "project_root": str(project_root), + "source_terminal_publish_report": str(handoff_report["source_terminal_publish_report"]), + "source_dispatch_completion_report": str(handoff_report["source_dispatch_completion_report"]), + "source_dispatch_trace_report": str(handoff_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str(handoff_report["source_dispatch_execution_report"]), + "source_dispatch_report": str(handoff_report["source_dispatch_report"]), + "handoff_status": handoff_status, + "handoff_action": str(handoff_report["handoff_action"]), + "should_trigger_release": should_trigger_release, + "release_exit_code": release_exit_code, + "release_trigger_status": release_trigger_status, + "release_trigger_exit_code": int(release_trigger_exit_code), + "release_trigger_attempted": release_trigger_attempted, + "release_command": release_command, + "release_command_parts": list(release_command_parts), + "release_workflow_path": release_workflow_path, + "release_workflow_ref": release_workflow_ref, + "target_environment": str(handoff_report["target_environment"]), + "release_channel": str(handoff_report["release_channel"]), + "publish_status": str(handoff_report["publish_status"]), + "completion_status": str(handoff_report["completion_status"]), + "run_id": handoff_report["run_id"], + "run_url": str(handoff_report["run_url"]), + "dry_run": dry_run, + "command_returncode": command_returncode, + "command_stdout_tail": command_stdout_tail, + "command_stderr_tail": command_stderr_tail, + "trigger_error_type": trigger_error_type, + "trigger_error_message": trigger_error_message, + "reason_codes": _unique(reason_codes), + "failed_checks": list(handoff_report["failed_checks"]), + "structural_issues": list(handoff_report["structural_issues"]), + "missing_artifacts": list(handoff_report["missing_artifacts"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Trigger Report", + "", + f"- Trigger Status: **{str(payload['release_trigger_status']).upper()}**", + f"- Should Trigger Release: `{payload['should_trigger_release']}`", + f"- Trigger Attempted: `{payload['release_trigger_attempted']}`", + f"- Trigger Exit Code: `{payload['release_trigger_exit_code']}`", + f"- Target Environment: `{payload['target_environment']}`", + f"- Release Channel: `{payload['release_channel']}`", + f"- Handoff Status: `{payload['handoff_status']}`", + f"- Run ID: `{payload['run_id']}`", + f"- Run URL: `{payload['run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Release Handoff Report: `{payload['source_release_handoff_report']}`", + f"- Source Terminal Publish Report: `{payload['source_terminal_publish_report']}`", + f"- Source Dispatch Completion Report: `{payload['source_dispatch_completion_report']}`", + f"- Source Dispatch Trace Report: `{payload['source_dispatch_trace_report']}`", + f"- Source Dispatch Execution Report: `{payload['source_dispatch_execution_report']}`", + f"- Source Dispatch Report: `{payload['source_dispatch_report']}`", + "", + "## Release Command", + ] + + if payload["release_command"]: + lines.append(f"- `{payload['release_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Command Output (tail)"]) + stdout_tail = str(payload["command_stdout_tail"]).strip() + stderr_tail = str(payload["command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], + *, + output_json: Path, + output_markdown: Path, +) -> dict[str, str]: + command_returncode = payload["command_returncode"] + run_id = payload["run_id"] + return { + "workflow_release_trigger_status": str(payload["release_trigger_status"]), + "workflow_release_trigger_should_trigger_release": "true" + if payload["should_trigger_release"] + else "false", + "workflow_release_trigger_attempted": "true" + if payload["release_trigger_attempted"] + else "false", + "workflow_release_trigger_exit_code": str(payload["release_trigger_exit_code"]), + "workflow_release_trigger_command_returncode": ( + "" if command_returncode is None else str(command_returncode) + ), + "workflow_release_trigger_target_environment": str(payload["target_environment"]), + "workflow_release_trigger_release_channel": str(payload["release_channel"]), + "workflow_release_trigger_handoff_status": str(payload["handoff_status"]), + "workflow_release_trigger_run_id": "" if run_id is None else str(run_id), + "workflow_release_trigger_run_url": str(payload["run_url"]), + "workflow_release_trigger_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_trigger_report_json": str(output_json), + "workflow_release_trigger_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run Linux CI workflow release trigger gate from P2-30 handoff artifact" + ) + parser.add_argument( + "--release-handoff-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_handoff.json", + help="P2-30 release handoff report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for release trigger command execution", + ) + parser.add_argument( + "--release-workflow-path", + default=".github/workflows/release.yml", + help="Release workflow path used when auto-building release command", + ) + parser.add_argument( + "--release-workflow-ref", + default="main", + help="Release workflow ref used when auto-building release command", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable used when auto-building release command", + ) + parser.add_argument( + "--release-command", + default="", + help="Optional explicit release trigger command string (overrides auto command)", + ) + parser.add_argument( + "--release-timeout-seconds", + type=int, + default=900, + help="Release trigger command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trigger.json", + help="Output release trigger JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_trigger.md", + help="Output release trigger markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release trigger JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate contract and skip release trigger execution", + ) + args = parser.parse_args() + + if args.release_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-release-trigger-gate] --release-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + release_handoff_report_path = Path(args.release_handoff_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + handoff_report = load_release_handoff_report(release_handoff_report_path) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-trigger-gate] {exc}", file=sys.stderr) + return 2 + + release_command_parts: list[str] = [] + if handoff_report["should_trigger_release"]: + try: + release_command_parts = build_release_command_parts( + handoff_report, + source_report_path=release_handoff_report_path.resolve(), + gh_executable=args.gh_executable, + release_workflow_path=args.release_workflow_path, + release_workflow_ref=args.release_workflow_ref, + release_command=args.release_command, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-trigger-gate] {exc}", file=sys.stderr) + return 2 + + command_result: dict[str, Any] | None = None + if handoff_report["should_trigger_release"] and not args.dry_run: + command_result = execute_release_trigger_command( + release_command_parts, + cwd=project_root, + timeout_seconds=args.release_timeout_seconds, + ) + + payload = build_release_trigger_payload( + handoff_report, + source_path=release_handoff_report_path.resolve(), + project_root=project_root.resolve(), + release_command_parts=release_command_parts, + release_workflow_path=args.release_workflow_path, + release_workflow_ref=args.release_workflow_ref, + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release trigger json: {output_json_path}") + print(f"release trigger markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release trigger summary: " + f"release_trigger_status={payload['release_trigger_status']} " + f"release_trigger_attempted={payload['release_trigger_attempted']} " + f"release_trigger_exit_code={payload['release_trigger_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_trigger_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_release_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_release_verdict_gate.py new file mode 100644 index 0000000..ec4ff3e --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_release_verdict_gate.py @@ -0,0 +1,460 @@ +"""Phase 2 card P2-38 gate for Linux CI workflow release verdict closure. + +This script consumes the P2-37 release archive artifact and converges +the terminal release verdict contract: +1) validate release archive contract consistency, +2) normalize terminal release verdict for CI exit semantics, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_ARCHIVE_STATUSES: set[str] = {"ready", "pending", "failed", "contract_failed"} +ALLOWED_RELEASE_ARCHIVE_DECISIONS: set[str] = {"publish", "hold", "block"} +ALLOWED_RELEASE_VERDICT_STATUSES: set[str] = { + "published", + "awaiting_archive", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_VERDICT_DECISIONS: set[str] = {"ship", "hold", "block"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_release_archive_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release archive report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: release archive payload must be object") + + required_fields = ( + "release_archive_status", + "release_archive_decision", + "release_archive_exit_code", + "should_publish_archive", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + release_archive_status = _coerce_str( + payload["release_archive_status"], + field="release_archive_status", + path=path, + ) + if release_archive_status not in ALLOWED_RELEASE_ARCHIVE_STATUSES: + raise ValueError( + f"{path}: field 'release_archive_status' must be one of " + f"{sorted(ALLOWED_RELEASE_ARCHIVE_STATUSES)}" + ) + + release_archive_decision = _coerce_str( + payload["release_archive_decision"], + field="release_archive_decision", + path=path, + ) + if release_archive_decision not in ALLOWED_RELEASE_ARCHIVE_DECISIONS: + raise ValueError( + f"{path}: field 'release_archive_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_ARCHIVE_DECISIONS)}" + ) + + release_archive_exit_code = _coerce_int( + payload["release_archive_exit_code"], + field="release_archive_exit_code", + path=path, + ) + if release_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_archive_exit_code' must be >= 0") + + should_publish_archive = _coerce_bool( + payload["should_publish_archive"], + field="should_publish_archive", + path=path, + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int( + release_run_id_raw, + field="release_run_id", + path=path, + ) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + release_run_url = _coerce_str(payload["release_run_url"], field="release_run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + evidence_manifest_raw = payload["evidence_manifest"] + if not isinstance(evidence_manifest_raw, list): + raise ValueError(f"{path}: field 'evidence_manifest' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(evidence_manifest_raw): + if not isinstance(entry, dict): + raise ValueError(f"{path}: evidence_manifest[{idx}] must be object") + if "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: evidence_manifest[{idx}] missing path/exists") + evidence_path = _coerce_str(entry["path"], field=f"evidence_manifest[{idx}].path", path=path) + evidence_exists = _coerce_bool( + entry["exists"], + field=f"evidence_manifest[{idx}].exists", + path=path, + ) + evidence_manifest.append({"path": evidence_path, "exists": evidence_exists}) + + return { + "generated_at": payload.get("generated_at"), + "source_release_closure_report": _coerce_str( + payload.get("source_release_closure_report", ""), + field="source_release_closure_report", + path=path, + ), + "release_archive_status": release_archive_status, + "release_archive_decision": release_archive_decision, + "release_archive_exit_code": release_archive_exit_code, + "should_publish_archive": should_publish_archive, + "release_run_id": release_run_id, + "release_run_url": release_run_url, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def build_release_verdict_payload( + release_archive_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + release_archive_status = str(release_archive_report["release_archive_status"]) + release_archive_decision = str(release_archive_report["release_archive_decision"]) + release_archive_exit_code = int(release_archive_report["release_archive_exit_code"]) + should_publish_archive = bool(release_archive_report["should_publish_archive"]) + + reason_codes = list(release_archive_report["reason_codes"]) + structural_issues = list(release_archive_report["structural_issues"]) + missing_artifacts = list(release_archive_report["missing_artifacts"]) + evidence_manifest = list(release_archive_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("release_verdict_evidence_missing") + + if release_archive_status == "ready": + if release_archive_decision != "publish": + structural_issues.append("release_archive_decision_mismatch_ready") + if release_archive_exit_code != 0: + structural_issues.append("release_archive_exit_code_mismatch_ready") + if not should_publish_archive: + structural_issues.append("should_publish_archive_mismatch_ready") + elif release_archive_status == "pending": + if release_archive_decision != "hold": + structural_issues.append("release_archive_decision_mismatch_pending") + if should_publish_archive: + structural_issues.append("should_publish_archive_mismatch_pending") + elif release_archive_status in {"failed", "contract_failed"}: + if release_archive_decision != "block": + structural_issues.append("release_archive_decision_mismatch_failed") + if should_publish_archive: + structural_issues.append("should_publish_archive_mismatch_failed") + if release_archive_exit_code == 0: + structural_issues.append("release_archive_exit_code_mismatch_failed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + release_verdict_status = "contract_failed" + release_verdict_decision = "block" + release_verdict_exit_code = 1 + should_ship_release = False + should_open_incident = True + reason_codes.extend(structural_issues) + elif release_archive_status == "ready": + release_verdict_status = "published" + release_verdict_decision = "ship" + release_verdict_exit_code = 0 + should_ship_release = True + should_open_incident = False + reason_codes = ["release_verdict_published"] + elif release_archive_status == "pending": + release_verdict_status = "awaiting_archive" + release_verdict_decision = "hold" + release_verdict_exit_code = max(1, release_archive_exit_code) + should_ship_release = False + should_open_incident = False + else: + release_verdict_status = "blocked" + release_verdict_decision = "block" + release_verdict_exit_code = max(1, release_archive_exit_code) + should_ship_release = False + should_open_incident = True + + if release_verdict_status not in ALLOWED_RELEASE_VERDICT_STATUSES: + raise ValueError("internal: unsupported release_verdict_status computed") + if release_verdict_decision not in ALLOWED_RELEASE_VERDICT_DECISIONS: + raise ValueError("internal: unsupported release_verdict_decision computed") + + release_verdict_summary = ( + f"release_archive_status={release_archive_status} " + f"release_verdict_status={release_verdict_status} " + f"release_verdict_decision={release_verdict_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_archive_report": str(source_path), + "release_archive_status": release_archive_status, + "release_archive_decision": release_archive_decision, + "release_archive_exit_code": release_archive_exit_code, + "release_verdict_status": release_verdict_status, + "release_verdict_decision": release_verdict_decision, + "release_verdict_exit_code": int(release_verdict_exit_code), + "should_publish_archive": should_publish_archive, + "should_ship_release": should_ship_release, + "should_open_incident": should_open_incident, + "release_run_id": release_archive_report["release_run_id"], + "release_run_url": str(release_archive_report["release_run_url"]), + "release_verdict_summary": release_verdict_summary, + "reason_codes": _unique(reason_codes), + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Release Verdict Report", + "", + f"- Release Verdict Status: **{str(payload['release_verdict_status']).upper()}**", + f"- Release Verdict Decision: `{payload['release_verdict_decision']}`", + f"- Should Ship Release: `{payload['should_ship_release']}`", + f"- Should Open Incident: `{payload['should_open_incident']}`", + f"- Release Verdict Exit Code: `{payload['release_verdict_exit_code']}`", + f"- Release Archive Status: `{payload['release_archive_status']}`", + f"- Release Archive Decision: `{payload['release_archive_decision']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['release_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_release_verdict_status": str(payload["release_verdict_status"]), + "workflow_release_verdict_decision": str(payload["release_verdict_decision"]), + "workflow_release_verdict_should_ship_release": ( + "true" if payload["should_ship_release"] else "false" + ), + "workflow_release_verdict_should_open_incident": ( + "true" if payload["should_open_incident"] else "false" + ), + "workflow_release_verdict_exit_code": str(payload["release_verdict_exit_code"]), + "workflow_release_verdict_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_release_verdict_run_url": str(payload["release_run_url"]), + "workflow_release_verdict_missing_artifacts": json.dumps( + payload["missing_artifacts"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_release_verdict_report_json": str(output_json), + "workflow_release_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow release verdict contract from P2-37 release archive report" + ) + ) + parser.add_argument( + "--release-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_archive.json", + help="P2-37 release archive report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_release_verdict.json", + help="Output release verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_release_verdict.md", + help="Output release verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print release verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_archive_report_path = Path(args.release_archive_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_archive_report = load_release_archive_report(release_archive_report_path) + payload = build_release_verdict_payload( + release_archive_report, + source_path=release_archive_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-release-verdict-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"release verdict json: {output_json_path}") + print(f"release verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "release verdict summary: " + f"release_verdict_status={payload['release_verdict_status']} " + f"release_verdict_decision={payload['release_verdict_decision']} " + f"release_verdict_exit_code={payload['release_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["release_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_sync_gate.py b/scripts/run_p2_linux_ci_workflow_sync_gate.py new file mode 100644 index 0000000..03fc2d4 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_sync_gate.py @@ -0,0 +1,265 @@ +"""Phase 2 card P2-19 gate for Linux CI workflow drift sync. + +This script verifies that workflow/metadata artifacts rendered from the +P2-17 workflow plan stay synchronized with the expected generated outputs. +It can optionally rewrite drifted artifacts in place. +""" + +from __future__ import annotations + +import argparse +import copy +import difflib +import importlib.util +import json +import sys +from pathlib import Path +from typing import Any + + +def _load_yaml_gate_module(): + script_path = Path(__file__).resolve().with_name("run_p2_linux_ci_workflow_yaml_gate.py") + spec = importlib.util.spec_from_file_location("p2_linux_ci_workflow_yaml_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _normalize_text(text: str) -> str: + return text.replace("\r\n", "\n") + + +def _build_unified_diff(*, expected: str, actual: str, expected_name: str, actual_name: str) -> str: + expected_lines = expected.splitlines(keepends=True) + actual_lines = actual.splitlines(keepends=True) + diff_lines = list( + difflib.unified_diff( + expected_lines, + actual_lines, + fromfile=expected_name, + tofile=actual_name, + ) + ) + return "".join(diff_lines) + + +def _load_json_object(path: Path, *, label: str) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid {label} JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: {label} payload must be object") + return payload + + +def _normalize_metadata(payload: dict[str, Any], *, strict_generated_at: bool) -> dict[str, Any]: + normalized = copy.deepcopy(payload) + if not strict_generated_at: + normalized.pop("generated_at", None) + return normalized + + +def _format_json(payload: dict[str, Any]) -> str: + return json.dumps(payload, indent=2, ensure_ascii=False, sort_keys=True) + "\n" + + +def build_expected_artifacts( + *, + plan_path: Path, + workflow_path: Path, + workflow_name: str, + python_version: str, + artifact_prefix: str, +) -> tuple[str, dict[str, Any]]: + yaml_gate = _load_yaml_gate_module() + plan = yaml_gate.load_ci_workflow_plan(plan_path) + workflow_yaml = yaml_gate.build_ci_workflow_yaml( + plan, + workflow_name=workflow_name, + python_version=python_version, + artifact_prefix=artifact_prefix, + ) + metadata = yaml_gate.build_render_metadata( + plan, + source_plan_path=plan_path.resolve(), + workflow_output_path=workflow_path, + artifact_prefix=artifact_prefix, + ) + metadata["workflow_name"] = workflow_name + return workflow_yaml, metadata + + +def check_render_drift( + *, + expected_workflow_yaml: str, + expected_metadata: dict[str, Any], + workflow_path: Path, + metadata_path: Path, + strict_generated_at: bool, +) -> dict[str, Any]: + workflow_missing = not workflow_path.is_file() + metadata_missing = not metadata_path.is_file() + + workflow_diff = "" + metadata_diff = "" + + if not workflow_missing: + actual_workflow_yaml = _normalize_text(workflow_path.read_text(encoding="utf-8")) + expected_workflow_yaml = _normalize_text(expected_workflow_yaml) + if actual_workflow_yaml != expected_workflow_yaml: + workflow_diff = _build_unified_diff( + expected=expected_workflow_yaml, + actual=actual_workflow_yaml, + expected_name="expected-workflow.yml", + actual_name=str(workflow_path), + ) + + if not metadata_missing: + actual_metadata = _load_json_object(metadata_path, label="workflow metadata") + actual_metadata_normalized = _normalize_metadata( + actual_metadata, + strict_generated_at=strict_generated_at, + ) + expected_metadata_normalized = _normalize_metadata( + expected_metadata, + strict_generated_at=strict_generated_at, + ) + if actual_metadata_normalized != expected_metadata_normalized: + metadata_diff = _build_unified_diff( + expected=_format_json(expected_metadata_normalized), + actual=_format_json(actual_metadata_normalized), + expected_name="expected-metadata.json", + actual_name=str(metadata_path), + ) + + return { + "workflow_missing": workflow_missing, + "metadata_missing": metadata_missing, + "workflow_drift": workflow_missing or bool(workflow_diff), + "metadata_drift": metadata_missing or bool(metadata_diff), + "workflow_diff": workflow_diff, + "metadata_diff": metadata_diff, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate/sync rendered Linux CI workflow artifacts from workflow plan" + ) + parser.add_argument( + "--ci-workflow-plan", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 CI workflow plan JSON path", + ) + parser.add_argument( + "--workflow-path", + default=".github/workflows/linux_unified_gate.yml", + help="Rendered workflow YAML path", + ) + parser.add_argument( + "--metadata-path", + default=".claude/reports/linux_unified_gate/ci_workflow_render.json", + help="Rendered metadata JSON path", + ) + parser.add_argument( + "--workflow-name", + default="Linux Unified Gate", + help="Workflow display name forwarded to P2-18 renderer", + ) + parser.add_argument( + "--python-version", + default="3.11", + help="Python version forwarded to P2-18 renderer", + ) + parser.add_argument( + "--artifact-prefix", + default="linux-unified-summary", + help="Artifact prefix forwarded to P2-18 renderer", + ) + parser.add_argument( + "--strict-generated-at", + action="store_true", + help="Treat generated_at drift in metadata as validation failure", + ) + parser.add_argument( + "--write", + action="store_true", + help="Rewrite workflow and metadata artifacts to expected generated outputs", + ) + parser.add_argument( + "--print-diff", + action="store_true", + help="Print unified diffs when drift is detected", + ) + args = parser.parse_args() + + plan_path = Path(args.ci_workflow_plan) + workflow_path = Path(args.workflow_path) + metadata_path = Path(args.metadata_path) + + try: + expected_workflow_yaml, expected_metadata = build_expected_artifacts( + plan_path=plan_path, + workflow_path=workflow_path, + workflow_name=args.workflow_name, + python_version=args.python_version, + artifact_prefix=args.artifact_prefix, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-sync-gate] {exc}", file=sys.stderr) + return 2 + + if args.write: + workflow_path.parent.mkdir(parents=True, exist_ok=True) + workflow_path.write_text(expected_workflow_yaml, encoding="utf-8") + metadata_path.parent.mkdir(parents=True, exist_ok=True) + metadata_path.write_text( + json.dumps(expected_metadata, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + print(f"workflow synced: {workflow_path}") + print(f"metadata synced: {metadata_path}") + + try: + drift = check_render_drift( + expected_workflow_yaml=expected_workflow_yaml, + expected_metadata=expected_metadata, + workflow_path=workflow_path, + metadata_path=metadata_path, + strict_generated_at=args.strict_generated_at, + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-sync-gate] {exc}", file=sys.stderr) + return 2 + + drift_count = int(drift["workflow_drift"]) + int(drift["metadata_drift"]) + print( + "sync summary: " + f"workflow_drift={drift['workflow_drift']} " + f"metadata_drift={drift['metadata_drift']} " + f"drift_count={drift_count}" + ) + + if drift["workflow_missing"]: + print(f"missing workflow: {workflow_path}") + if drift["metadata_missing"]: + print(f"missing metadata: {metadata_path}") + + if args.print_diff: + if drift["workflow_diff"]: + print("workflow diff:") + print(drift["workflow_diff"], end="") + if drift["metadata_diff"]: + print("metadata diff:") + print(drift["metadata_diff"], end="") + + if drift["workflow_drift"] or drift["metadata_drift"]: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py b/scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py new file mode 100644 index 0000000..2ba01af --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py @@ -0,0 +1,435 @@ +"""Phase 2 card P2-29 gate for Linux CI workflow terminal publish. + +This script converts P2-28 dispatch completion artifacts into a single +terminal publish contract: +1) normalize completion verdict to final CI publish status, +2) provide promote/hold decision fields for downstream stages, +3) emit JSON/Markdown reports + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_COMPLETION_STATUSES: set[str] = { + "blocked", + "ready_dry_run", + "run_tracking_missing", + "run_completed_success", + "run_completed_failure", + "run_poll_failed", + "run_await_timeout", + "run_in_progress", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_dispatch_completion_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: dispatch completion report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: dispatch completion payload must be object") + + required_fields = ( + "completion_status", + "completion_exit_code", + "execution_status", + "tracking_status", + "allow_in_progress", + "should_poll_workflow_run", + "run_id", + "run_url", + "reason_codes", + "failed_checks", + "structural_issues", + "missing_artifacts", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + completion_status = _coerce_str( + payload["completion_status"], + field="completion_status", + path=path, + ) + if completion_status not in ALLOWED_COMPLETION_STATUSES: + raise ValueError( + f"{path}: field 'completion_status' must be one of " + f"{sorted(ALLOWED_COMPLETION_STATUSES)}" + ) + + completion_exit_code = _coerce_int( + payload["completion_exit_code"], + field="completion_exit_code", + path=path, + ) + if completion_exit_code < 0: + raise ValueError(f"{path}: field 'completion_exit_code' must be >= 0") + + execution_status = _coerce_str( + payload["execution_status"], + field="execution_status", + path=path, + ) + tracking_status = _coerce_str( + payload["tracking_status"], + field="tracking_status", + path=path, + ) + allow_in_progress = _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ) + should_poll_workflow_run = _coerce_bool( + payload["should_poll_workflow_run"], + field="should_poll_workflow_run", + path=path, + ) + + run_id_raw = payload["run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="run_id", path=path) + if run_id < 1: + raise ValueError(f"{path}: field 'run_id' must be >= 1 when present") + + run_url = _coerce_str(payload["run_url"], field="run_url", path=path) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + failed_checks = _coerce_str_list(payload["failed_checks"], field="failed_checks", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + source_dispatch_trace_report = _coerce_str( + payload["source_dispatch_trace_report"], + field="source_dispatch_trace_report", + path=path, + ) + source_dispatch_execution_report = _coerce_str( + payload["source_dispatch_execution_report"], + field="source_dispatch_execution_report", + path=path, + ) + source_dispatch_report = _coerce_str( + payload["source_dispatch_report"], + field="source_dispatch_report", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "completion_status": completion_status, + "completion_exit_code": completion_exit_code, + "execution_status": execution_status, + "tracking_status": tracking_status, + "allow_in_progress": allow_in_progress, + "should_poll_workflow_run": should_poll_workflow_run, + "run_id": run_id, + "run_url": run_url, + "reason_codes": reason_codes, + "failed_checks": failed_checks, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "source_dispatch_trace_report": source_dispatch_trace_report, + "source_dispatch_execution_report": source_dispatch_execution_report, + "source_dispatch_report": source_dispatch_report, + } + + +def build_terminal_publish_payload( + completion_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + completion_status = str(completion_report["completion_status"]) + completion_exit_code = int(completion_report["completion_exit_code"]) + structural_issues = list(completion_report["structural_issues"]) + reason_codes = list(completion_report["reason_codes"]) + + if completion_status == "run_completed_success" and completion_exit_code != 0: + structural_issues.append("completion_exit_code_mismatch_success") + if completion_status in { + "run_tracking_missing", + "run_completed_failure", + "run_poll_failed", + } and completion_exit_code == 0: + structural_issues.append("completion_exit_code_mismatch_failure") + if completion_status == "run_await_timeout" and completion_exit_code == 0: + if not bool(completion_report["allow_in_progress"]): + structural_issues.append("completion_timeout_policy_mismatch") + + structural_issues = _unique(structural_issues) + + if structural_issues: + publish_status = "contract_failed" + publish_exit_code = 1 + should_promote = False + reason_codes.extend(structural_issues) + elif completion_status == "run_completed_success": + publish_status = "passed" + publish_exit_code = 0 + should_promote = True + reason_codes = ["workflow_completed_success"] + elif completion_status in {"blocked", "ready_dry_run"}: + publish_status = "blocked" + publish_exit_code = completion_exit_code + should_promote = False + elif completion_status in {"run_await_timeout", "run_in_progress"}: + if completion_exit_code == 0: + publish_status = "in_progress" + publish_exit_code = 0 + else: + publish_status = "failed" + publish_exit_code = 1 + should_promote = False + else: + publish_status = "failed" + publish_exit_code = 1 if completion_exit_code == 0 else completion_exit_code + should_promote = False + + publish_summary = ( + f"completion_status={completion_status} " + f"publish_status={publish_status} " + f"should_promote={should_promote}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_dispatch_completion_report": str(source_path), + "completion_status": completion_status, + "completion_exit_code": completion_exit_code, + "publish_status": publish_status, + "publish_exit_code": int(publish_exit_code), + "should_promote": should_promote, + "publish_summary": publish_summary, + "execution_status": str(completion_report["execution_status"]), + "tracking_status": str(completion_report["tracking_status"]), + "allow_in_progress": bool(completion_report["allow_in_progress"]), + "should_poll_workflow_run": bool(completion_report["should_poll_workflow_run"]), + "run_id": completion_report["run_id"], + "run_url": str(completion_report["run_url"]), + "reason_codes": _unique(reason_codes), + "failed_checks": list(completion_report["failed_checks"]), + "structural_issues": structural_issues, + "missing_artifacts": list(completion_report["missing_artifacts"]), + "source_dispatch_trace_report": str(completion_report["source_dispatch_trace_report"]), + "source_dispatch_execution_report": str(completion_report["source_dispatch_execution_report"]), + "source_dispatch_report": str(completion_report["source_dispatch_report"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Terminal Publish Report", + "", + f"- Publish Status: **{str(payload['publish_status']).upper()}**", + f"- Publish Exit Code: `{payload['publish_exit_code']}`", + f"- Should Promote: `{payload['should_promote']}`", + f"- Completion Status: `{payload['completion_status']}`", + f"- Completion Exit Code: `{payload['completion_exit_code']}`", + f"- Run ID: `{payload['run_id']}`", + f"- Run URL: `{payload['run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Dispatch Completion Report: `{payload['source_dispatch_completion_report']}`", + f"- Source Dispatch Trace Report: `{payload['source_dispatch_trace_report']}`", + f"- Source Dispatch Execution Report: `{payload['source_dispatch_execution_report']}`", + f"- Source Dispatch Report: `{payload['source_dispatch_report']}`", + "", + "## Publish Summary", + "", + f"- {payload['publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["run_id"] + return { + "workflow_terminal_publish_status": str(payload["publish_status"]), + "workflow_terminal_publish_exit_code": str(payload["publish_exit_code"]), + "workflow_terminal_publish_should_promote": "true" if payload["should_promote"] else "false", + "workflow_terminal_publish_completion_status": str(payload["completion_status"]), + "workflow_terminal_publish_run_id": "" if run_id is None else str(run_id), + "workflow_terminal_publish_run_url": str(payload["run_url"]), + "workflow_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_terminal_publish_report_json": str(output_json), + "workflow_terminal_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Publish Linux CI workflow terminal verdict from P2-28 completion report" + ) + parser.add_argument( + "--dispatch-completion-report", + default=".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json", + help="P2-28 dispatch completion report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json", + help="Output terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md", + help="Output terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + dispatch_completion_report_path = Path(args.dispatch_completion_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + completion_report = load_dispatch_completion_report(dispatch_completion_report_path) + payload = build_terminal_publish_payload( + completion_report, + source_path=dispatch_completion_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-terminal-publish-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"terminal publish json: {output_json_path}") + print(f"terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "terminal publish summary: " + f"publish_status={payload['publish_status']} " + f"should_promote={payload['should_promote']} " + f"publish_exit_code={payload['publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py b/scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py new file mode 100644 index 0000000..904cd36 --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py @@ -0,0 +1,1062 @@ +"""Phase 2 card P2-59 gate for Linux CI workflow terminal verdict closure. + +This script converges one terminal Linux-validation verdict by combining: +1) P2-56 release final publish archive report, +2) P2-57 gate manifest drift report. + +It emits one normalized verdict contract for Linux-stage execution readiness. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_release_shipped", + "archive_release_shipped_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: set[str] = {"passed", "failed"} + +ALLOWED_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation", + "ready_with_follow_up_for_linux_validation", + "blocked", + "contract_failed", +} +ALLOWED_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation", + "proceed_linux_validation_with_follow_up", + "halt_linux_validation_blocker", + "abort_linux_validation", +} +ALLOWED_TERMINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_release_final_publish_archive_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: release final publish archive report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: release final publish archive payload must be object") + + required_fields = ( + "source_release_final_verdict_publish_report", + "source_release_final_verdict_report", + "source_release_final_archive_report", + "source_release_final_closure_publish_report", + "source_release_final_closure_report", + "source_release_final_handoff_report", + "source_release_final_terminal_publish_report", + "source_release_final_outcome_report", + "source_release_delivery_final_verdict_report", + "source_release_follow_up_final_verdict_report", + "release_delivery_final_verdict_status", + "release_delivery_final_verdict_decision", + "release_delivery_final_verdict_exit_code", + "release_follow_up_final_verdict_status", + "release_follow_up_final_verdict_decision", + "release_follow_up_final_verdict_exit_code", + "release_final_outcome_status", + "release_final_outcome_decision", + "release_final_outcome_exit_code", + "release_final_terminal_publish_status", + "release_final_terminal_publish_decision", + "release_final_terminal_publish_exit_code", + "release_final_handoff_status", + "release_final_handoff_decision", + "release_final_handoff_exit_code", + "release_final_closure_status", + "release_final_closure_decision", + "release_final_closure_exit_code", + "release_final_closure_publish_status", + "release_final_closure_publish_decision", + "release_final_closure_publish_exit_code", + "release_final_archive_status", + "release_final_archive_decision", + "release_final_archive_exit_code", + "release_final_verdict_status", + "release_final_verdict_decision", + "release_final_verdict_exit_code", + "release_final_verdict_publish_status", + "release_final_verdict_publish_decision", + "release_final_verdict_publish_exit_code", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "final_publish_archive_should_archive", + "final_publish_archive_requires_manual_action", + "final_publish_archive_channel", + "release_final_publish_archive_status", + "release_final_publish_archive_decision", + "release_final_publish_archive_exit_code", + "release_final_publish_archive_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + publish_archive_status = _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ) + if publish_archive_status not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_STATUSES: + raise ValueError( + f"{path}: field 'release_final_publish_archive_status' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_STATUSES)}" + ) + + publish_archive_decision = _coerce_str( + payload["release_final_publish_archive_decision"], + field="release_final_publish_archive_decision", + path=path, + ) + if publish_archive_decision not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_DECISIONS: + raise ValueError( + f"{path}: field 'release_final_publish_archive_decision' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_DECISIONS)}" + ) + + publish_archive_channel = _coerce_str( + payload["final_publish_archive_channel"], + field="final_publish_archive_channel", + path=path, + ) + if publish_archive_channel not in ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_CHANNELS: + raise ValueError( + f"{path}: field 'final_publish_archive_channel' must be one of " + f"{sorted(ALLOWED_RELEASE_FINAL_PUBLISH_ARCHIVE_CHANNELS)}" + ) + + publish_archive_exit_code = _coerce_int( + payload["release_final_publish_archive_exit_code"], + field="release_final_publish_archive_exit_code", + path=path, + ) + if publish_archive_exit_code < 0: + raise ValueError(f"{path}: field 'release_final_publish_archive_exit_code' must be >= 0") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_verdict_publish_report": _coerce_str( + payload["source_release_final_verdict_publish_report"], + field="source_release_final_verdict_publish_report", + path=path, + ), + "source_release_final_verdict_report": _coerce_str( + payload["source_release_final_verdict_report"], + field="source_release_final_verdict_report", + path=path, + ), + "source_release_final_archive_report": _coerce_str( + payload["source_release_final_archive_report"], + field="source_release_final_archive_report", + path=path, + ), + "source_release_final_closure_publish_report": _coerce_str( + payload["source_release_final_closure_publish_report"], + field="source_release_final_closure_publish_report", + path=path, + ), + "source_release_final_closure_report": _coerce_str( + payload["source_release_final_closure_report"], + field="source_release_final_closure_report", + path=path, + ), + "source_release_final_handoff_report": _coerce_str( + payload["source_release_final_handoff_report"], + field="source_release_final_handoff_report", + path=path, + ), + "source_release_final_terminal_publish_report": _coerce_str( + payload["source_release_final_terminal_publish_report"], + field="source_release_final_terminal_publish_report", + path=path, + ), + "source_release_final_outcome_report": _coerce_str( + payload["source_release_final_outcome_report"], + field="source_release_final_outcome_report", + path=path, + ), + "source_release_delivery_final_verdict_report": _coerce_str( + payload["source_release_delivery_final_verdict_report"], + field="source_release_delivery_final_verdict_report", + path=path, + ), + "source_release_follow_up_final_verdict_report": _coerce_str( + payload["source_release_follow_up_final_verdict_report"], + field="source_release_follow_up_final_verdict_report", + path=path, + ), + "release_delivery_final_verdict_status": _coerce_str( + payload["release_delivery_final_verdict_status"], + field="release_delivery_final_verdict_status", + path=path, + ), + "release_delivery_final_verdict_decision": _coerce_str( + payload["release_delivery_final_verdict_decision"], + field="release_delivery_final_verdict_decision", + path=path, + ), + "release_delivery_final_verdict_exit_code": _coerce_int( + payload["release_delivery_final_verdict_exit_code"], + field="release_delivery_final_verdict_exit_code", + path=path, + ), + "release_follow_up_final_verdict_status": _coerce_str( + payload["release_follow_up_final_verdict_status"], + field="release_follow_up_final_verdict_status", + path=path, + ), + "release_follow_up_final_verdict_decision": _coerce_str( + payload["release_follow_up_final_verdict_decision"], + field="release_follow_up_final_verdict_decision", + path=path, + ), + "release_follow_up_final_verdict_exit_code": _coerce_int( + payload["release_follow_up_final_verdict_exit_code"], + field="release_follow_up_final_verdict_exit_code", + path=path, + ), + "release_final_outcome_status": _coerce_str( + payload["release_final_outcome_status"], + field="release_final_outcome_status", + path=path, + ), + "release_final_outcome_decision": _coerce_str( + payload["release_final_outcome_decision"], + field="release_final_outcome_decision", + path=path, + ), + "release_final_outcome_exit_code": _coerce_int( + payload["release_final_outcome_exit_code"], + field="release_final_outcome_exit_code", + path=path, + ), + "release_final_terminal_publish_status": _coerce_str( + payload["release_final_terminal_publish_status"], + field="release_final_terminal_publish_status", + path=path, + ), + "release_final_terminal_publish_decision": _coerce_str( + payload["release_final_terminal_publish_decision"], + field="release_final_terminal_publish_decision", + path=path, + ), + "release_final_terminal_publish_exit_code": _coerce_int( + payload["release_final_terminal_publish_exit_code"], + field="release_final_terminal_publish_exit_code", + path=path, + ), + "release_final_handoff_status": _coerce_str( + payload["release_final_handoff_status"], + field="release_final_handoff_status", + path=path, + ), + "release_final_handoff_decision": _coerce_str( + payload["release_final_handoff_decision"], + field="release_final_handoff_decision", + path=path, + ), + "release_final_handoff_exit_code": _coerce_int( + payload["release_final_handoff_exit_code"], + field="release_final_handoff_exit_code", + path=path, + ), + "release_final_closure_status": _coerce_str( + payload["release_final_closure_status"], + field="release_final_closure_status", + path=path, + ), + "release_final_closure_decision": _coerce_str( + payload["release_final_closure_decision"], + field="release_final_closure_decision", + path=path, + ), + "release_final_closure_exit_code": _coerce_int( + payload["release_final_closure_exit_code"], + field="release_final_closure_exit_code", + path=path, + ), + "release_final_closure_publish_status": _coerce_str( + payload["release_final_closure_publish_status"], + field="release_final_closure_publish_status", + path=path, + ), + "release_final_closure_publish_decision": _coerce_str( + payload["release_final_closure_publish_decision"], + field="release_final_closure_publish_decision", + path=path, + ), + "release_final_closure_publish_exit_code": _coerce_int( + payload["release_final_closure_publish_exit_code"], + field="release_final_closure_publish_exit_code", + path=path, + ), + "release_final_archive_status": _coerce_str( + payload["release_final_archive_status"], + field="release_final_archive_status", + path=path, + ), + "release_final_archive_decision": _coerce_str( + payload["release_final_archive_decision"], + field="release_final_archive_decision", + path=path, + ), + "release_final_archive_exit_code": _coerce_int( + payload["release_final_archive_exit_code"], + field="release_final_archive_exit_code", + path=path, + ), + "release_final_verdict_status": _coerce_str( + payload["release_final_verdict_status"], + field="release_final_verdict_status", + path=path, + ), + "release_final_verdict_decision": _coerce_str( + payload["release_final_verdict_decision"], + field="release_final_verdict_decision", + path=path, + ), + "release_final_verdict_exit_code": _coerce_int( + payload["release_final_verdict_exit_code"], + field="release_final_verdict_exit_code", + path=path, + ), + "release_final_verdict_publish_status": _coerce_str( + payload["release_final_verdict_publish_status"], + field="release_final_verdict_publish_status", + path=path, + ), + "release_final_verdict_publish_decision": _coerce_str( + payload["release_final_verdict_publish_decision"], + field="release_final_verdict_publish_decision", + path=path, + ), + "release_final_verdict_publish_exit_code": _coerce_int( + payload["release_final_verdict_publish_exit_code"], + field="release_final_verdict_publish_exit_code", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "final_publish_archive_should_archive": _coerce_bool( + payload["final_publish_archive_should_archive"], + field="final_publish_archive_should_archive", + path=path, + ), + "final_publish_archive_requires_manual_action": _coerce_bool( + payload["final_publish_archive_requires_manual_action"], + field="final_publish_archive_requires_manual_action", + path=path, + ), + "final_publish_archive_channel": publish_archive_channel, + "release_final_publish_archive_status": publish_archive_status, + "release_final_publish_archive_decision": publish_archive_decision, + "release_final_publish_archive_exit_code": publish_archive_exit_code, + "release_final_publish_archive_summary": _coerce_str( + payload["release_final_publish_archive_summary"], + field="release_final_publish_archive_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def load_gate_manifest_drift_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: gate manifest drift report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: gate manifest drift payload must be object") + + required_fields = ( + "status", + "missing_runtime_tests", + "missing_manifest_entries", + "orphan_manifest_entries", + "structural_issues", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + status = _coerce_str(payload["status"], field="status", path=path) + if status not in ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: + raise ValueError( + f"{path}: field 'status' must be one of " + f"{sorted(ALLOWED_GATE_MANIFEST_DRIFT_STATUSES)}" + ) + + return { + "status": status, + "missing_runtime_tests": _coerce_str_list( + payload["missing_runtime_tests"], + field="missing_runtime_tests", + path=path, + ), + "missing_manifest_entries": _coerce_str_list( + payload["missing_manifest_entries"], + field="missing_manifest_entries", + path=path, + ), + "orphan_manifest_entries": _coerce_str_list( + payload["orphan_manifest_entries"], + field="orphan_manifest_entries", + path=path, + ), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + } + + +def build_terminal_verdict_payload( + release_final_publish_archive_report: dict[str, Any], + gate_manifest_drift_report: dict[str, Any], + *, + release_source_path: Path, + drift_source_path: Path, +) -> dict[str, Any]: + publish_archive_status = str( + release_final_publish_archive_report["release_final_publish_archive_status"] + ) + publish_archive_decision = str( + release_final_publish_archive_report["release_final_publish_archive_decision"] + ) + publish_archive_exit_code = int( + release_final_publish_archive_report["release_final_publish_archive_exit_code"] + ) + publish_archive_should_archive = bool( + release_final_publish_archive_report["final_publish_archive_should_archive"] + ) + publish_archive_requires_manual_action = bool( + release_final_publish_archive_report["final_publish_archive_requires_manual_action"] + ) + publish_archive_channel = str( + release_final_publish_archive_report["final_publish_archive_channel"] + ) + + drift_status = str(gate_manifest_drift_report["status"]) + drift_missing_runtime_tests = list(gate_manifest_drift_report["missing_runtime_tests"]) + drift_missing_manifest_entries = list(gate_manifest_drift_report["missing_manifest_entries"]) + drift_orphan_manifest_entries = list(gate_manifest_drift_report["orphan_manifest_entries"]) + drift_structural_issues = list(gate_manifest_drift_report["structural_issues"]) + + reason_codes = list(release_final_publish_archive_report["reason_codes"]) + structural_issues = list(release_final_publish_archive_report["structural_issues"]) + missing_artifacts = list(release_final_publish_archive_report["missing_artifacts"]) + evidence_manifest = list(release_final_publish_archive_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("terminal_verdict_evidence_missing") + + expected_publish_archive = { + "archived": ("archive_release_shipped", True, False, "release", 0), + "archived_with_follow_up": ( + "archive_release_shipped_with_follow_up", + True, + False, + "follow_up", + None, + ), + "blocked": ("archive_release_blocker", False, True, "blocker", None), + "contract_failed": ("abort_archive", False, True, "blocker", None), + } + ( + expected_decision, + expected_should_archive, + expected_requires_manual_action, + expected_channel, + expected_exit_code, + ) = expected_publish_archive[publish_archive_status] + + if publish_archive_decision != expected_decision: + structural_issues.append("final_publish_archive_decision_mismatch") + if publish_archive_should_archive != expected_should_archive: + structural_issues.append("final_publish_archive_should_archive_mismatch") + if publish_archive_requires_manual_action != expected_requires_manual_action: + structural_issues.append("final_publish_archive_requires_manual_action_mismatch") + if publish_archive_channel != expected_channel: + structural_issues.append("final_publish_archive_channel_mismatch") + if expected_exit_code == 0 and publish_archive_exit_code != 0: + structural_issues.append("final_publish_archive_exit_code_mismatch_archived") + if expected_exit_code is None and publish_archive_exit_code == 0: + structural_issues.append("final_publish_archive_exit_code_mismatch_non_archived") + + if drift_status == "failed": + structural_issues.append("gate_manifest_drift_failed") + reason_codes.append("terminal_verdict_gate_manifest_drift_failed") + if drift_missing_runtime_tests: + structural_issues.append("gate_manifest_drift_missing_runtime_tests") + if drift_missing_manifest_entries: + structural_issues.append("gate_manifest_drift_missing_manifest_entries") + if drift_orphan_manifest_entries: + structural_issues.append("gate_manifest_drift_orphan_manifest_entries") + if drift_structural_issues: + structural_issues.extend(drift_structural_issues) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + drift_missing_total = ( + len(drift_missing_runtime_tests) + + len(drift_missing_manifest_entries) + + len(drift_orphan_manifest_entries) + ) + + if structural_issues or missing_artifacts or drift_missing_total > 0: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation" + terminal_exit_code = max(1, publish_archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.extend(structural_issues) + elif publish_archive_status == "archived": + terminal_status = "ready_for_linux_validation" + terminal_decision = "proceed_linux_validation" + terminal_exit_code = 0 + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_channel = "release" + reason_codes = ["terminal_verdict_ready_for_linux_validation"] + elif publish_archive_status == "archived_with_follow_up": + terminal_status = "ready_with_follow_up_for_linux_validation" + terminal_decision = "proceed_linux_validation_with_follow_up" + terminal_exit_code = max(1, publish_archive_exit_code) + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_channel = "follow_up" + reason_codes.append("terminal_verdict_ready_with_follow_up_for_linux_validation") + elif publish_archive_status == "blocked": + terminal_status = "blocked" + terminal_decision = "halt_linux_validation_blocker" + terminal_exit_code = max(1, publish_archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("terminal_verdict_blocked") + else: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation" + terminal_exit_code = max(1, publish_archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("terminal_verdict_upstream_contract_failed") + + if terminal_status not in ALLOWED_TERMINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported terminal_verdict_status computed") + if terminal_decision not in ALLOWED_TERMINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported terminal_verdict_decision computed") + if terminal_channel not in ALLOWED_TERMINAL_VERDICT_CHANNELS: + raise ValueError("internal: unsupported terminal_verdict_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"release_final_publish_archive_status={publish_archive_status} " + f"gate_manifest_drift_status={drift_status} " + f"terminal_verdict_status={terminal_status} " + f"terminal_verdict_decision={terminal_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_release_final_publish_archive_report": str(release_source_path), + "source_gate_manifest_drift_report": str(drift_source_path), + "source_release_final_verdict_publish_report": str( + release_final_publish_archive_report["source_release_final_verdict_publish_report"] + ), + "source_release_final_verdict_report": str( + release_final_publish_archive_report["source_release_final_verdict_report"] + ), + "source_release_final_archive_report": str( + release_final_publish_archive_report["source_release_final_archive_report"] + ), + "source_release_final_closure_publish_report": str( + release_final_publish_archive_report["source_release_final_closure_publish_report"] + ), + "source_release_final_closure_report": str( + release_final_publish_archive_report["source_release_final_closure_report"] + ), + "source_release_final_handoff_report": str( + release_final_publish_archive_report["source_release_final_handoff_report"] + ), + "source_release_final_terminal_publish_report": str( + release_final_publish_archive_report["source_release_final_terminal_publish_report"] + ), + "source_release_final_outcome_report": str( + release_final_publish_archive_report["source_release_final_outcome_report"] + ), + "source_release_delivery_final_verdict_report": str( + release_final_publish_archive_report["source_release_delivery_final_verdict_report"] + ), + "source_release_follow_up_final_verdict_report": str( + release_final_publish_archive_report["source_release_follow_up_final_verdict_report"] + ), + "release_delivery_final_verdict_status": str( + release_final_publish_archive_report["release_delivery_final_verdict_status"] + ), + "release_delivery_final_verdict_decision": str( + release_final_publish_archive_report["release_delivery_final_verdict_decision"] + ), + "release_delivery_final_verdict_exit_code": int( + release_final_publish_archive_report["release_delivery_final_verdict_exit_code"] + ), + "release_follow_up_final_verdict_status": str( + release_final_publish_archive_report["release_follow_up_final_verdict_status"] + ), + "release_follow_up_final_verdict_decision": str( + release_final_publish_archive_report["release_follow_up_final_verdict_decision"] + ), + "release_follow_up_final_verdict_exit_code": int( + release_final_publish_archive_report["release_follow_up_final_verdict_exit_code"] + ), + "release_final_outcome_status": str( + release_final_publish_archive_report["release_final_outcome_status"] + ), + "release_final_outcome_decision": str( + release_final_publish_archive_report["release_final_outcome_decision"] + ), + "release_final_outcome_exit_code": int( + release_final_publish_archive_report["release_final_outcome_exit_code"] + ), + "release_final_terminal_publish_status": str( + release_final_publish_archive_report["release_final_terminal_publish_status"] + ), + "release_final_terminal_publish_decision": str( + release_final_publish_archive_report["release_final_terminal_publish_decision"] + ), + "release_final_terminal_publish_exit_code": int( + release_final_publish_archive_report["release_final_terminal_publish_exit_code"] + ), + "release_final_handoff_status": str( + release_final_publish_archive_report["release_final_handoff_status"] + ), + "release_final_handoff_decision": str( + release_final_publish_archive_report["release_final_handoff_decision"] + ), + "release_final_handoff_exit_code": int( + release_final_publish_archive_report["release_final_handoff_exit_code"] + ), + "release_final_closure_status": str( + release_final_publish_archive_report["release_final_closure_status"] + ), + "release_final_closure_decision": str( + release_final_publish_archive_report["release_final_closure_decision"] + ), + "release_final_closure_exit_code": int( + release_final_publish_archive_report["release_final_closure_exit_code"] + ), + "release_final_closure_publish_status": str( + release_final_publish_archive_report["release_final_closure_publish_status"] + ), + "release_final_closure_publish_decision": str( + release_final_publish_archive_report["release_final_closure_publish_decision"] + ), + "release_final_closure_publish_exit_code": int( + release_final_publish_archive_report["release_final_closure_publish_exit_code"] + ), + "release_final_archive_status": str( + release_final_publish_archive_report["release_final_archive_status"] + ), + "release_final_archive_decision": str( + release_final_publish_archive_report["release_final_archive_decision"] + ), + "release_final_archive_exit_code": int( + release_final_publish_archive_report["release_final_archive_exit_code"] + ), + "release_final_verdict_status": str( + release_final_publish_archive_report["release_final_verdict_status"] + ), + "release_final_verdict_decision": str( + release_final_publish_archive_report["release_final_verdict_decision"] + ), + "release_final_verdict_exit_code": int( + release_final_publish_archive_report["release_final_verdict_exit_code"] + ), + "release_final_verdict_publish_status": str( + release_final_publish_archive_report["release_final_verdict_publish_status"] + ), + "release_final_verdict_publish_decision": str( + release_final_publish_archive_report["release_final_verdict_publish_decision"] + ), + "release_final_verdict_publish_exit_code": int( + release_final_publish_archive_report["release_final_verdict_publish_exit_code"] + ), + "release_final_publish_archive_status": publish_archive_status, + "release_final_publish_archive_decision": publish_archive_decision, + "release_final_publish_archive_exit_code": publish_archive_exit_code, + "release_run_id": release_final_publish_archive_report["release_run_id"], + "release_run_url": str(release_final_publish_archive_report["release_run_url"]), + "follow_up_queue_url": str(release_final_publish_archive_report["follow_up_queue_url"]), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": drift_missing_runtime_tests, + "gate_manifest_drift_missing_manifest_entries": drift_missing_manifest_entries, + "gate_manifest_drift_orphan_manifest_entries": drift_orphan_manifest_entries, + "terminal_verdict_should_proceed": terminal_should_proceed, + "terminal_verdict_requires_manual_action": terminal_requires_manual_action, + "terminal_verdict_channel": terminal_channel, + "terminal_verdict_status": terminal_status, + "terminal_verdict_decision": terminal_decision, + "terminal_verdict_exit_code": terminal_exit_code, + "terminal_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Terminal Verdict Report", + "", + f"- Terminal Verdict Status: **{str(payload['terminal_verdict_status']).upper()}**", + f"- Terminal Verdict Decision: `{payload['terminal_verdict_decision']}`", + f"- Terminal Verdict Exit Code: `{payload['terminal_verdict_exit_code']}`", + f"- Terminal Verdict Should Proceed: `{payload['terminal_verdict_should_proceed']}`", + ( + "- Terminal Verdict Requires Manual Action: " + f"`{payload['terminal_verdict_requires_manual_action']}`" + ), + f"- Terminal Verdict Channel: `{payload['terminal_verdict_channel']}`", + ( + "- Release Final Publish Archive Status: " + f"`{payload['release_final_publish_archive_status']}`" + ), + f"- Gate Manifest Drift Status: `{payload['gate_manifest_drift_status']}`", + ( + "- Drift Missing Runtime Tests: " + f"`{len(payload['gate_manifest_drift_missing_runtime_tests'])}`" + ), + ( + "- Drift Missing Manifest Entries: " + f"`{len(payload['gate_manifest_drift_missing_manifest_entries'])}`" + ), + ( + "- Drift Orphan Manifest Entries: " + f"`{len(payload['gate_manifest_drift_orphan_manifest_entries'])}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['terminal_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Gate Manifest Drift Details"]) + drift_runtime = payload["gate_manifest_drift_missing_runtime_tests"] + if drift_runtime: + lines.extend(f"- missing runtime test: `{item}`" for item in drift_runtime) + else: + lines.append("- missing runtime test: none") + + drift_manifest = payload["gate_manifest_drift_missing_manifest_entries"] + if drift_manifest: + lines.extend(f"- missing manifest entry: `{item}`" for item in drift_manifest) + else: + lines.append("- missing manifest entry: none") + + drift_orphan = payload["gate_manifest_drift_orphan_manifest_entries"] + if drift_orphan: + lines.extend(f"- orphan manifest entry: `{item}`" for item in drift_orphan) + else: + lines.append("- orphan manifest entry: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_terminal_verdict_status": str(payload["terminal_verdict_status"]), + "workflow_terminal_verdict_decision": str(payload["terminal_verdict_decision"]), + "workflow_terminal_verdict_exit_code": str(payload["terminal_verdict_exit_code"]), + "workflow_terminal_verdict_should_proceed": ( + "true" if payload["terminal_verdict_should_proceed"] else "false" + ), + "workflow_terminal_verdict_requires_manual_action": ( + "true" if payload["terminal_verdict_requires_manual_action"] else "false" + ), + "workflow_terminal_verdict_channel": str(payload["terminal_verdict_channel"]), + "workflow_terminal_verdict_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_terminal_verdict_gate_manifest_drift_missing_runtime_tests": str( + len(payload["gate_manifest_drift_missing_runtime_tests"]) + ), + "workflow_terminal_verdict_gate_manifest_drift_missing_manifest_entries": str( + len(payload["gate_manifest_drift_missing_manifest_entries"]) + ), + "workflow_terminal_verdict_gate_manifest_drift_orphan_manifest_entries": str( + len(payload["gate_manifest_drift_orphan_manifest_entries"]) + ), + "workflow_terminal_verdict_release_final_publish_archive_status": str( + payload["release_final_publish_archive_status"] + ), + "workflow_terminal_verdict_follow_up_queue_url": str(payload["follow_up_queue_url"]), + "workflow_terminal_verdict_run_id": "" if release_run_id is None else str(release_run_id), + "workflow_terminal_verdict_run_url": str(payload["release_run_url"]), + "workflow_terminal_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_terminal_verdict_report_json": str(output_json), + "workflow_terminal_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow terminal verdict contract " + "from P2-56 release final publish archive and P2-57 gate manifest drift" + ) + ) + parser.add_argument( + "--release-final-publish-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json", + help="P2-56 release final publish archive report JSON path", + ) + parser.add_argument( + "--gate-manifest-drift-report", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json", + help="P2-57 gate manifest drift report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json", + help="Output terminal verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md", + help="Output terminal verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print terminal verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + release_report_path = Path(args.release_final_publish_archive_report) + drift_report_path = Path(args.gate_manifest_drift_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + release_report = load_release_final_publish_archive_report(release_report_path) + drift_report = load_gate_manifest_drift_report(drift_report_path) + payload = build_terminal_verdict_payload( + release_report, + drift_report, + release_source_path=release_report_path.resolve(), + drift_source_path=drift_report_path.resolve(), + ) + except ValueError as exc: + print(f"[p2-linux-ci-workflow-terminal-verdict-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"terminal verdict json: {output_json_path}") + print(f"terminal verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "terminal verdict summary: " + f"terminal_verdict_status={payload['terminal_verdict_status']} " + f"terminal_verdict_decision={payload['terminal_verdict_decision']} " + f"terminal_verdict_exit_code={payload['terminal_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["terminal_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_ci_workflow_yaml_gate.py b/scripts/run_p2_linux_ci_workflow_yaml_gate.py new file mode 100644 index 0000000..5de338f --- /dev/null +++ b/scripts/run_p2_linux_ci_workflow_yaml_gate.py @@ -0,0 +1,454 @@ +"""Phase 2 card P2-18 gate for Linux CI workflow YAML rendering. + +This script converts the P2-17 workflow-plan artifact into a GitHub Actions +workflow YAML so CI can consume fan-out/fan-in commands without manual edits. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path}: field '{field}' must be non-empty string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ValueError(f"{path}: field '{field}' must be non-empty string list") + return list(value) + + +def _yaml_scalar(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + return json.dumps(str(value), ensure_ascii=False) + + +def load_ci_workflow_plan(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: workflow plan file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: workflow plan payload must be object") + + for field in ("shard_total", "manifest_total_tests", "selected_shards", "fan_out_matrix", "fan_in"): + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + shard_total = _coerce_int(payload["shard_total"], field="shard_total", path=path) + manifest_total_tests = _coerce_int( + payload["manifest_total_tests"], + field="manifest_total_tests", + path=path, + ) + selected_shards = _coerce_int(payload["selected_shards"], field="selected_shards", path=path) + + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + if manifest_total_tests < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if selected_shards < 0: + raise ValueError(f"{path}: selected_shards must be >= 0") + + fan_out_matrix = payload["fan_out_matrix"] + if not isinstance(fan_out_matrix, dict): + raise ValueError(f"{path}: field 'fan_out_matrix' must be object") + include = fan_out_matrix.get("include") + if not isinstance(include, list): + raise ValueError(f"{path}: field 'fan_out_matrix.include' must be list") + + normalized_include: list[dict[str, Any]] = [] + seen_indexes: set[int] = set() + include_summary_paths: list[str] = [] + include_total_tests = 0 + for index, shard in enumerate(include, start=1): + if not isinstance(shard, dict): + raise ValueError(f"{path}: fan_out_matrix.include[{index - 1}] must be object") + + for required in ( + "job_id", + "shard_index", + "shard_total", + "total_tests", + "report_dir", + "summary_path", + "command", + ): + if required not in shard: + raise ValueError( + f"{path}: fan_out_matrix.include[{index - 1}] missing field '{required}'" + ) + + job_id = _coerce_str( + shard["job_id"], + field=f"fan_out_matrix.include[{index - 1}].job_id", + path=path, + ) + shard_index = _coerce_int( + shard["shard_index"], + field=f"fan_out_matrix.include[{index - 1}].shard_index", + path=path, + ) + nested_total = _coerce_int( + shard["shard_total"], + field=f"fan_out_matrix.include[{index - 1}].shard_total", + path=path, + ) + total_tests = _coerce_int( + shard["total_tests"], + field=f"fan_out_matrix.include[{index - 1}].total_tests", + path=path, + ) + report_dir = _coerce_str( + shard["report_dir"], + field=f"fan_out_matrix.include[{index - 1}].report_dir", + path=path, + ) + summary_path = _coerce_str( + shard["summary_path"], + field=f"fan_out_matrix.include[{index - 1}].summary_path", + path=path, + ) + command = _coerce_str( + shard["command"], + field=f"fan_out_matrix.include[{index - 1}].command", + path=path, + ) + + if shard_index < 1 or shard_index > shard_total: + raise ValueError(f"{path}: fan_out_matrix.include[{index - 1}].shard_index out of range") + if nested_total != shard_total: + raise ValueError( + f"{path}: fan_out_matrix.include[{index - 1}].shard_total ({nested_total}) " + f"!= top-level shard_total ({shard_total})" + ) + if total_tests < 0: + raise ValueError(f"{path}: fan_out_matrix.include[{index - 1}].total_tests must be >= 0") + if shard_index in seen_indexes: + raise ValueError(f"{path}: duplicate shard_index {shard_index} in fan_out_matrix.include") + + seen_indexes.add(shard_index) + include_total_tests += total_tests + include_summary_paths.append(summary_path) + normalized_include.append( + { + "job_id": job_id, + "shard_index": shard_index, + "shard_total": nested_total, + "total_tests": total_tests, + "report_dir": report_dir, + "summary_path": summary_path, + "command": command, + } + ) + + fan_in = payload["fan_in"] + if not isinstance(fan_in, dict): + raise ValueError(f"{path}: field 'fan_in' must be object") + for required in ( + "summary_paths", + "merged_summary_path", + "aggregation_command", + "publish_command", + "final_report_json", + "final_report_markdown", + ): + if required not in fan_in: + raise ValueError(f"{path}: fan_in missing field '{required}'") + + summary_paths = _coerce_str_list(fan_in["summary_paths"], field="fan_in.summary_paths", path=path) + merged_summary_path = _coerce_str( + fan_in["merged_summary_path"], + field="fan_in.merged_summary_path", + path=path, + ) + aggregation_command = _coerce_str( + fan_in["aggregation_command"], + field="fan_in.aggregation_command", + path=path, + ) + publish_command = _coerce_str( + fan_in["publish_command"], + field="fan_in.publish_command", + path=path, + ) + final_report_json = _coerce_str( + fan_in["final_report_json"], + field="fan_in.final_report_json", + path=path, + ) + final_report_markdown = _coerce_str( + fan_in["final_report_markdown"], + field="fan_in.final_report_markdown", + path=path, + ) + + if selected_shards != len(normalized_include): + raise ValueError( + f"{path}: selected_shards ({selected_shards}) " + f"!= fan_out_matrix.include count ({len(normalized_include)})" + ) + if summary_paths != include_summary_paths: + raise ValueError( + f"{path}: fan_in.summary_paths must match fan_out_matrix.include[*].summary_path order" + ) + if include_total_tests > manifest_total_tests: + raise ValueError( + f"{path}: fan_out_matrix.include total_tests ({include_total_tests}) " + f"exceeds manifest_total_tests ({manifest_total_tests})" + ) + + return { + "generated_at": payload.get("generated_at"), + "source_ci_matrix": payload.get("source_ci_matrix"), + "shard_total": shard_total, + "manifest_total_tests": manifest_total_tests, + "selected_shards": selected_shards, + "skipped_empty_shards": bool(payload.get("skipped_empty_shards", False)), + "fan_out_matrix": {"include": normalized_include}, + "fan_in": { + "summary_paths": summary_paths, + "merged_summary_path": merged_summary_path, + "aggregation_command": aggregation_command, + "publish_command": publish_command, + "final_report_json": final_report_json, + "final_report_markdown": final_report_markdown, + }, + } + + +def build_ci_workflow_yaml( + plan: dict[str, Any], + *, + workflow_name: str, + python_version: str, + artifact_prefix: str, +) -> str: + include = plan["fan_out_matrix"]["include"] + fan_in = plan["fan_in"] + + lines: list[str] = [ + "# Generated by scripts/run_p2_linux_ci_workflow_yaml_gate.py (P2-18).", + f"name: {_yaml_scalar(workflow_name)}", + "on:", + " workflow_dispatch:", + " pull_request:", + " branches:", + " - main", + "jobs:", + " linux_fan_out:", + ' name: "Linux Fan-out / shard ${{ matrix.shard_index }}"', + " runs-on: ubuntu-latest", + " strategy:", + " fail-fast: false", + " matrix:", + " include:", + ] + + for shard in include: + lines.extend( + [ + f" - job_id: {_yaml_scalar(shard['job_id'])}", + f" shard_index: {_yaml_scalar(shard['shard_index'])}", + f" shard_total: {_yaml_scalar(shard['shard_total'])}", + f" total_tests: {_yaml_scalar(shard['total_tests'])}", + f" report_dir: {_yaml_scalar(shard['report_dir'])}", + f" summary_path: {_yaml_scalar(shard['summary_path'])}", + f" command: {_yaml_scalar(shard['command'])}", + ] + ) + + lines.extend( + [ + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " - name: Setup Python", + " uses: actions/setup-python@v5", + " with:", + f" python-version: {_yaml_scalar(python_version)}", + " - name: Install Dependencies", + " run: python -m pip install -r requirements.txt", + " - name: Run Linux Unified Gate Shard", + " run: ${{ matrix.command }}", + " - name: Upload Shard Summary", + " uses: actions/upload-artifact@v4", + " with:", + f' name: "{artifact_prefix}-${{{{ matrix.shard_index }}}}"', + " path: ${{ matrix.summary_path }}", + " if-no-files-found: error", + " linux_fan_in:", + " name: Linux Fan-in", + " runs-on: ubuntu-latest", + " needs: linux_fan_out", + " if: ${{ always() }}", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " - name: Setup Python", + " uses: actions/setup-python@v5", + " with:", + f" python-version: {_yaml_scalar(python_version)}", + " - name: Install Dependencies", + " run: python -m pip install -r requirements.txt", + " - name: Download Shard Summaries", + " uses: actions/download-artifact@v4", + " with:", + f" pattern: {_yaml_scalar(f'{artifact_prefix}-*')}", + " path: .", + " merge-multiple: true", + " - name: Aggregate Shard Summaries", + f" run: {_yaml_scalar(fan_in['aggregation_command'])}", + " - name: Publish Final Linux Report", + f" run: {_yaml_scalar(fan_in['publish_command'])}", + " - name: Upload Final Linux Report", + " uses: actions/upload-artifact@v4", + " with:", + ' name: "linux-unified-final-report"', + " path: |", + f" {fan_in['merged_summary_path']}", + f" {fan_in['final_report_json']}", + f" {fan_in['final_report_markdown']}", + " if-no-files-found: error", + ] + ) + return "\n".join(lines) + "\n" + + +def build_render_metadata( + plan: dict[str, Any], + *, + source_plan_path: Path, + workflow_output_path: Path, + artifact_prefix: str, +) -> dict[str, Any]: + fan_in = plan["fan_in"] + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_ci_workflow_plan": str(source_plan_path), + "workflow_output_path": str(workflow_output_path), + "workflow_name": "Linux Unified Gate", + "shard_total": int(plan["shard_total"]), + "manifest_total_tests": int(plan["manifest_total_tests"]), + "selected_shards": int(plan["selected_shards"]), + "artifact_prefix": artifact_prefix, + "fan_in": { + "summary_paths": list(fan_in["summary_paths"]), + "merged_summary_path": str(fan_in["merged_summary_path"]), + "aggregation_command": str(fan_in["aggregation_command"]), + "publish_command": str(fan_in["publish_command"]), + "final_report_json": str(fan_in["final_report_json"]), + "final_report_markdown": str(fan_in["final_report_markdown"]), + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Render GitHub Actions workflow from P2 Linux CI workflow plan" + ) + parser.add_argument( + "--ci-workflow-plan", + default=".claude/reports/linux_unified_gate/ci_workflow_plan.json", + help="P2-17 CI workflow plan JSON path", + ) + parser.add_argument( + "--output-workflow", + default=".github/workflows/linux_unified_gate.yml", + help="Rendered GitHub Actions workflow YAML path", + ) + parser.add_argument( + "--output-metadata", + default=".claude/reports/linux_unified_gate/ci_workflow_render.json", + help="Rendered workflow metadata JSON path", + ) + parser.add_argument( + "--workflow-name", + default="Linux Unified Gate", + help="Workflow display name", + ) + parser.add_argument( + "--python-version", + default="3.11", + help="Python version in rendered workflow setup step", + ) + parser.add_argument( + "--artifact-prefix", + default="linux-unified-summary", + help="Artifact prefix for fan-out shard summary upload/download", + ) + parser.add_argument( + "--print-yaml", + action="store_true", + help="Print rendered workflow YAML", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write files; only validate and print summary", + ) + args = parser.parse_args() + + source_path = Path(args.ci_workflow_plan) + workflow_output_path = Path(args.output_workflow) + metadata_output_path = Path(args.output_metadata) + + try: + plan = load_ci_workflow_plan(source_path) + workflow_yaml = build_ci_workflow_yaml( + plan, + workflow_name=args.workflow_name, + python_version=args.python_version, + artifact_prefix=args.artifact_prefix, + ) + metadata = build_render_metadata( + plan, + source_plan_path=source_path.resolve(), + workflow_output_path=workflow_output_path, + artifact_prefix=args.artifact_prefix, + ) + metadata["workflow_name"] = args.workflow_name + except ValueError as exc: + print(f"[p2-linux-ci-workflow-yaml-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + workflow_output_path.parent.mkdir(parents=True, exist_ok=True) + workflow_output_path.write_text(workflow_yaml, encoding="utf-8") + metadata_output_path.parent.mkdir(parents=True, exist_ok=True) + metadata_output_path.write_text(json.dumps(metadata, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"workflow yaml: {workflow_output_path}") + print(f"workflow metadata: {metadata_output_path}") + + print( + "workflow render summary: " + f"{metadata['selected_shards']} shard job(s), " + f"manifest_total_tests={metadata['manifest_total_tests']}" + ) + if args.print_yaml or args.dry_run: + print(workflow_yaml, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_gate_manifest_drift_gate.py b/scripts/run_p2_linux_gate_manifest_drift_gate.py new file mode 100644 index 0000000..c247c6c --- /dev/null +++ b/scripts/run_p2_linux_gate_manifest_drift_gate.py @@ -0,0 +1,290 @@ +"""Phase 2 card P2-57 gate for Linux unified gate manifest drift closure. + +This script validates gate runtime contract closure across three layers: +1) scripts/run_*_gate.py scripts, +2) tests/test_*_gate_runtime.py runtime contracts, +3) scripts/run_linux_unified_gate.py Linux unified manifest entries. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import re +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +_GATE_SCRIPT_PATTERN = re.compile(r"^run_(.+_gate)\.py$") + + +def _strip_prefix(value: str, prefix: str) -> str: + if value.startswith(prefix): + return value[len(prefix) :] + return value + + +def _strip_suffix(value: str, suffix: str) -> str: + if suffix and value.endswith(suffix): + return value[: -len(suffix)] + return value + + +def _load_linux_unified_gate_module(project_root: Path) -> Any: + script_path = project_root / "scripts" / "run_linux_unified_gate.py" + spec = importlib.util.spec_from_file_location("linux_unified_gate_script", script_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"failed to load {script_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def collect_gate_script_basenames(*, scripts_dir: Path) -> list[str]: + basenames: list[str] = [] + for path in sorted(scripts_dir.glob("run_*_gate.py")): + if path.name == "run_linux_unified_gate.py": + continue + if _GATE_SCRIPT_PATTERN.match(path.name) is None: + continue + basenames.append(path.name) + return basenames + + +def expected_runtime_test_for_gate(gate_script_basename: str) -> str: + stem = _strip_suffix(gate_script_basename, ".py") + return f"test_{_strip_prefix(stem, 'run_')}_runtime.py" + + +def build_manifest_drift_report(*, project_root: Path) -> dict[str, Any]: + scripts_dir = project_root / "scripts" + tests_dir = project_root / "tests" + + gate_scripts = collect_gate_script_basenames(scripts_dir=scripts_dir) + gate_tests: list[str] = [expected_runtime_test_for_gate(name) for name in gate_scripts] + + missing_runtime_tests: list[str] = [] + existing_runtime_tests: list[str] = [] + for runtime_test in gate_tests: + if (tests_dir / runtime_test).is_file(): + existing_runtime_tests.append(runtime_test) + else: + missing_runtime_tests.append(runtime_test) + + linux_unified_gate = _load_linux_unified_gate_module(project_root) + manifest_entries = set(getattr(linux_unified_gate, "LINUX_UNIFIED_TEST_FILES", ())) + + missing_manifest_entries: list[str] = [] + existing_manifest_entries: list[str] = [] + for runtime_test in gate_tests: + manifest_path = f"tests/{runtime_test}" + if manifest_path in manifest_entries: + existing_manifest_entries.append(manifest_path) + else: + missing_manifest_entries.append(manifest_path) + + orphan_manifest_entries: list[str] = [] + for manifest_path in sorted(manifest_entries): + if not manifest_path.startswith("tests/test_"): + continue + if not manifest_path.endswith("_gate_runtime.py"): + continue + basename = Path(manifest_path).name + expected_script = ( + "run_" + + _strip_suffix(_strip_prefix(basename, "test_"), "_runtime.py") + + ".py" + ) + if (scripts_dir / expected_script).is_file(): + continue + orphan_manifest_entries.append(manifest_path) + + structural_issues: list[str] = [] + if missing_runtime_tests: + structural_issues.append("missing_gate_runtime_tests") + if missing_manifest_entries: + structural_issues.append("missing_manifest_entries") + if orphan_manifest_entries: + structural_issues.append("orphan_manifest_gate_tests") + + status = "passed" if not structural_issues else "failed" + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "project_root": str(project_root.resolve()), + "status": status, + "gate_scripts_scanned": gate_scripts, + "gate_runtime_tests_expected": gate_tests, + "existing_runtime_tests": existing_runtime_tests, + "missing_runtime_tests": missing_runtime_tests, + "existing_manifest_entries": existing_manifest_entries, + "missing_manifest_entries": missing_manifest_entries, + "orphan_manifest_entries": orphan_manifest_entries, + "structural_issues": structural_issues, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# P2-57 Linux Gate Manifest Drift Report", + "", + f"- Status: **{str(payload['status']).upper()}**", + f"- Generated At (UTC): `{payload['generated_at']}`", + f"- Project Root: `{payload['project_root']}`", + f"- Gate Scripts Scanned: {len(payload['gate_scripts_scanned'])}", + "", + "## Missing Runtime Tests", + ] + + missing_runtime_tests = payload["missing_runtime_tests"] + if missing_runtime_tests: + lines.extend(f"- `{item}`" for item in missing_runtime_tests) + else: + lines.append("- none") + + lines.extend(["", "## Missing Manifest Entries"]) + missing_manifest_entries = payload["missing_manifest_entries"] + if missing_manifest_entries: + lines.extend(f"- `{item}`" for item in missing_manifest_entries) + else: + lines.append("- none") + + lines.extend(["", "## Orphan Manifest Gate Tests"]) + orphan_manifest_entries = payload["orphan_manifest_entries"] + if orphan_manifest_entries: + lines.extend(f"- `{item}`" for item in orphan_manifest_entries) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + *, + payload: dict[str, Any], + output_json: Path, + output_markdown: Path, +) -> dict[str, str]: + return { + "linux_gate_manifest_drift_status": str(payload["status"]), + "linux_gate_manifest_drift_missing_runtime_tests": str( + len(payload["missing_runtime_tests"]) + ), + "linux_gate_manifest_drift_missing_manifest_entries": str( + len(payload["missing_manifest_entries"]) + ), + "linux_gate_manifest_drift_orphan_manifest_entries": str( + len(payload["orphan_manifest_entries"]) + ), + "linux_gate_manifest_drift_report_json": str(output_json), + "linux_gate_manifest_drift_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Linux gate runtime contract drift across scripts/tests/manifest" + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root containing scripts/ and tests/ (default: current directory)", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json", + help="Output JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.md", + help="Output Markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print report JSON to stdout", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing report files", + ) + args = parser.parse_args() + + project_root = Path(args.project_root) + scripts_dir = project_root / "scripts" + tests_dir = project_root / "tests" + if not scripts_dir.is_dir() or not tests_dir.is_dir(): + print( + "[p2-linux-gate-manifest-drift-gate] project root must contain scripts/ and tests/", + file=sys.stderr, + ) + return 2 + + try: + payload = build_manifest_drift_report(project_root=project_root) + except Exception as exc: # defensive for import/runtime failures + print(f"[p2-linux-gate-manifest-drift-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_markdown = Path(args.output_markdown) + + if not args.dry_run: + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"report json: {output_json}") + print(f"report markdown: {output_markdown}") + + if args.github_output: + values = build_github_output_values( + payload=payload, + output_json=output_json, + output_markdown=output_markdown, + ) + write_github_output(Path(args.github_output), values) + print(f"github output: {args.github_output}") + + print( + "manifest drift summary: " + f"status={payload['status']} " + f"missing_runtime_tests={len(payload['missing_runtime_tests'])} " + f"missing_manifest_entries={len(payload['missing_manifest_entries'])} " + f"orphan_manifest_entries={len(payload['orphan_manifest_entries'])}" + ) + + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return 0 if payload["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_report_publish_gate.py b/scripts/run_p2_linux_report_publish_gate.py new file mode 100644 index 0000000..7e581f9 --- /dev/null +++ b/scripts/run_p2_linux_report_publish_gate.py @@ -0,0 +1,220 @@ +"""Phase 2 card P2-13 gate for Linux final report publishing. + +This script reads merged shard summary from P2-12, validates the contract, +emits a compact JSON payload plus Markdown report, and returns pass/fail exit +status for Linux CI recovery stage. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +def _coerce_int(value: Any, *, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"field '{field}' must be int") + return value + + +def load_merged_summary(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: merged summary file not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: merged summary payload must be object") + + required_fields = ( + "manifest_total_tests", + "shard_total", + "shards_present", + "shards_missing", + "totals", + "structural_issues", + "overall_status", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + manifest_total_tests = _coerce_int(payload["manifest_total_tests"], field="manifest_total_tests") + shard_total = _coerce_int(payload["shard_total"], field="shard_total") + totals = payload["totals"] + if not isinstance(totals, dict): + raise ValueError(f"{path}: field 'totals' must be object") + total_tests = _coerce_int(totals.get("total_tests"), field="totals.total_tests") + passed = _coerce_int(totals.get("passed"), field="totals.passed") + failed = _coerce_int(totals.get("failed"), field="totals.failed") + + shards_present = payload["shards_present"] + shards_missing = payload["shards_missing"] + structural_issues = payload["structural_issues"] + overall_status = payload["overall_status"] + + if not isinstance(shards_present, list) or not all(isinstance(item, int) for item in shards_present): + raise ValueError(f"{path}: field 'shards_present' must be int list") + if not isinstance(shards_missing, list) or not all(isinstance(item, int) for item in shards_missing): + raise ValueError(f"{path}: field 'shards_missing' must be int list") + if not isinstance(structural_issues, list) or not all( + isinstance(item, str) for item in structural_issues + ): + raise ValueError(f"{path}: field 'structural_issues' must be string list") + if overall_status not in {"passed", "failed"}: + raise ValueError(f"{path}: field 'overall_status' must be 'passed' or 'failed'") + + if manifest_total_tests < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + if total_tests < 0 or passed < 0 or failed < 0: + raise ValueError(f"{path}: totals must be >= 0") + if passed + failed > total_tests: + raise ValueError(f"{path}: passed + failed exceeds total_tests") + + return payload + + +def build_publish_payload(merged_summary: dict[str, Any], *, source_path: Path) -> dict[str, Any]: + totals = merged_summary["totals"] + total_tests = int(totals["total_tests"]) + passed = int(totals["passed"]) + failed = int(totals["failed"]) + structural_issues = list(merged_summary["structural_issues"]) + shards_missing = list(merged_summary["shards_missing"]) + + computed_status = ( + "passed" + if failed == 0 and not structural_issues and not shards_missing + else "failed" + ) + reported_status = merged_summary["overall_status"] + status_mismatch = computed_status != reported_status + final_status = "failed" if status_mismatch else reported_status + + notes: list[str] = [] + if status_mismatch: + notes.append( + "overall_status mismatch between computed and merged summary" + f" (computed={computed_status}, reported={reported_status})" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_merged_summary": str(source_path), + "overall_status": final_status, + "manifest_total_tests": int(merged_summary["manifest_total_tests"]), + "shard_total": int(merged_summary["shard_total"]), + "shards_present": list(merged_summary["shards_present"]), + "shards_missing": shards_missing, + "totals": { + "total_tests": total_tests, + "passed": passed, + "failed": failed, + }, + "structural_issues": structural_issues, + "notes": notes, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + status = payload["overall_status"] + totals = payload["totals"] + lines = [ + "# Linux Unified Gate Final Report", + "", + f"- Status: **{status.upper()}**", + f"- Generated At (UTC): `{payload['generated_at']}`", + f"- Source Summary: `{payload['source_merged_summary']}`", + "", + "## Totals", + "", + "| Metric | Value |", + "|---|---:|", + f"| Manifest Total Tests | {payload['manifest_total_tests']} |", + f"| Selected Total Tests | {totals['total_tests']} |", + f"| Passed | {totals['passed']} |", + f"| Failed | {totals['failed']} |", + f"| Shard Total | {payload['shard_total']} |", + "", + "## Shards", + "", + f"- Present: {payload['shards_present']}", + f"- Missing: {payload['shards_missing']}", + "", + "## Structural Issues", + ] + + issues = payload["structural_issues"] + if issues: + lines.extend(f"- {item}" for item in issues) + else: + lines.append("- none") + + lines.append("") + lines.append("## Notes") + notes = payload["notes"] + if notes: + lines.extend(f"- {item}" for item in notes) + else: + lines.append("- none") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Publish final Linux unified gate report from merged shard summary" + ) + parser.add_argument( + "--merged-summary", + default=".claude/reports/linux_unified_gate/merged_summary.json", + help="Merged summary JSON produced by P2-12 aggregation gate", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/final_report.json", + help="Output JSON file path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/final_report.md", + help="Output Markdown report path", + ) + args = parser.parse_args() + + merged_summary_path = Path(args.merged_summary) + try: + merged_summary = load_merged_summary(merged_summary_path) + payload = build_publish_payload(merged_summary, source_path=merged_summary_path.resolve()) + except ValueError as exc: + print(f"[p2-linux-report-publish-gate] {exc}", file=sys.stderr) + return 2 + + output_json = Path(args.output_json) + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + + output_markdown = Path(args.output_markdown) + output_markdown.parent.mkdir(parents=True, exist_ok=True) + output_markdown.write_text(render_markdown_report(payload), encoding="utf-8") + + print(f"final json: {output_json}") + print(f"final markdown: {output_markdown}") + print( + "totals: " + f"{payload['totals']['passed']} passed, {payload['totals']['failed']} failed, " + f"{payload['totals']['total_tests']} total" + ) + print(f"overall_status: {payload['overall_status']}") + return 0 if payload["overall_status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_shard_aggregation_gate.py b/scripts/run_p2_linux_shard_aggregation_gate.py new file mode 100644 index 0000000..ee3ac4e --- /dev/null +++ b/scripts/run_p2_linux_shard_aggregation_gate.py @@ -0,0 +1,253 @@ +"""Phase 2 card P2-12 gate for Linux shard summary aggregation. + +This script merges summary artifacts generated by +`run_p2_linux_unified_execution_gate.py` and produces a single merged summary. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterable, Sequence + + +def discover_summary_files( + *, + summary_paths: Sequence[str], + summary_globs: Sequence[str], + artifacts_dir: str | None, +) -> list[Path]: + discovered: dict[str, Path] = {} + + def _add(path: Path) -> None: + resolved = path.resolve() + discovered[str(resolved)] = resolved + + for item in summary_paths: + _add(Path(item)) + + for pattern in summary_globs: + for candidate in sorted(Path.cwd().glob(pattern)): + _add(candidate) + + if artifacts_dir: + base = Path(artifacts_dir) + for candidate in sorted(base.rglob("summary.json")): + _add(candidate) + + if not discovered: + fallback = Path(".claude/reports/linux_unified_gate/summary.json") + if fallback.is_file(): + _add(fallback) + + return [discovered[key] for key in sorted(discovered.keys())] + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def load_shard_summary(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: summary file not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: summary payload must be object") + + required_fields = ( + "manifest_total_tests", + "total_tests", + "passed", + "failed", + "shard_total", + "shard_index", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + manifest_total = _coerce_int(payload["manifest_total_tests"], field="manifest_total_tests", path=path) + total_tests = _coerce_int(payload["total_tests"], field="total_tests", path=path) + passed = _coerce_int(payload["passed"], field="passed", path=path) + failed = _coerce_int(payload["failed"], field="failed", path=path) + shard_total = _coerce_int(payload["shard_total"], field="shard_total", path=path) + shard_index = _coerce_int(payload["shard_index"], field="shard_index", path=path) + + if manifest_total < 0: + raise ValueError(f"{path}: manifest_total_tests must be >= 0") + if total_tests < 0: + raise ValueError(f"{path}: total_tests must be >= 0") + if passed < 0 or failed < 0: + raise ValueError(f"{path}: passed/failed must be >= 0") + if passed + failed > total_tests: + raise ValueError(f"{path}: passed + failed exceeds total_tests") + if shard_total < 1: + raise ValueError(f"{path}: shard_total must be >= 1") + if shard_index < 1 or shard_index > shard_total: + raise ValueError(f"{path}: shard_index out of range") + + return { + "path": str(path), + "manifest_total_tests": manifest_total, + "total_tests": total_tests, + "passed": passed, + "failed": failed, + "shard_total": shard_total, + "shard_index": shard_index, + "report_dir": str(payload.get("report_dir", "")), + } + + +def aggregate_shard_summaries(summaries: Iterable[dict[str, Any]]) -> dict[str, Any]: + items = list(summaries) + if not items: + raise ValueError("no shard summaries provided") + + structural_issues: list[str] = [] + manifest_totals = {item["manifest_total_tests"] for item in items} + shard_totals = {item["shard_total"] for item in items} + manifest_total_tests = min(manifest_totals) + shard_total = max(shard_totals) + + if len(manifest_totals) > 1: + structural_issues.append( + f"manifest_total_tests mismatch across shards: {sorted(manifest_totals)}" + ) + if len(shard_totals) > 1: + structural_issues.append(f"shard_total mismatch across shards: {sorted(shard_totals)}") + + per_shard: dict[int, dict[str, Any]] = {} + duplicate_shards: set[int] = set() + for item in sorted(items, key=lambda value: (value["shard_index"], value["path"])): + shard_index = item["shard_index"] + if shard_index in per_shard: + duplicate_shards.add(shard_index) + continue + per_shard[shard_index] = item + + if duplicate_shards: + structural_issues.append(f"duplicate shard indexes: {sorted(duplicate_shards)}") + + expected_shards = set(range(1, shard_total + 1)) + present_shards = set(per_shard.keys()) + missing_shards = sorted(expected_shards - present_shards) + if missing_shards: + structural_issues.append(f"missing shard indexes: {missing_shards}") + + total_tests = sum(item["total_tests"] for item in per_shard.values()) + passed = sum(item["passed"] for item in per_shard.values()) + failed = sum(item["failed"] for item in per_shard.values()) + + if total_tests != manifest_total_tests: + structural_issues.append( + f"aggregated total_tests ({total_tests}) != manifest_total_tests ({manifest_total_tests})" + ) + + overall_status = "passed" if failed == 0 and not structural_issues else "failed" + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "manifest_total_tests": manifest_total_tests, + "shard_total": shard_total, + "shards_present": sorted(present_shards), + "shards_missing": missing_shards, + "totals": { + "total_tests": total_tests, + "passed": passed, + "failed": failed, + }, + "summary_files": [item["path"] for item in sorted(items, key=lambda value: value["path"])], + "per_shard": [ + { + "path": item["path"], + "shard_index": item["shard_index"], + "shard_total": item["shard_total"], + "total_tests": item["total_tests"], + "passed": item["passed"], + "failed": item["failed"], + "report_dir": item["report_dir"], + } + for item in sorted(per_shard.values(), key=lambda value: value["shard_index"]) + ], + "structural_issues": structural_issues, + "overall_status": overall_status, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Aggregate Linux shard summaries from P2 unified execution gate" + ) + parser.add_argument( + "--summary", + action="append", + default=[], + help="Path to shard summary.json (repeatable)", + ) + parser.add_argument( + "--summary-glob", + action="append", + default=[], + help="Glob pattern for summary files (repeatable)", + ) + parser.add_argument( + "--artifacts-dir", + default=None, + help="Directory to recursively scan for summary.json files", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/merged_summary.json", + help="Merged summary output file", + ) + args = parser.parse_args() + + summary_files = discover_summary_files( + summary_paths=args.summary, + summary_globs=args.summary_glob, + artifacts_dir=args.artifacts_dir, + ) + if not summary_files: + print("[p2-linux-shard-aggregation-gate] no summary files found", file=sys.stderr) + return 2 + + loaded: list[dict[str, Any]] = [] + try: + for summary_file in summary_files: + loaded.append(load_shard_summary(summary_file)) + merged = aggregate_shard_summaries(loaded) + except ValueError as exc: + print(f"[p2-linux-shard-aggregation-gate] {exc}", file=sys.stderr) + return 2 + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8") + + print(f"merged summary: {output_path}") + print(f"overall_status: {merged['overall_status']}") + print( + "totals: " + f"{merged['totals']['passed']} passed, {merged['totals']['failed']} failed, " + f"{merged['totals']['total_tests']} total" + ) + if merged["structural_issues"]: + print("structural issues:") + for issue in merged["structural_issues"]: + print(f"- {issue}") + + return 0 if merged["overall_status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_shard_plan_gate.py b/scripts/run_p2_linux_shard_plan_gate.py new file mode 100644 index 0000000..1ad68b0 --- /dev/null +++ b/scripts/run_p2_linux_shard_plan_gate.py @@ -0,0 +1,226 @@ +"""Phase 2 card P2-15 gate for Linux shard execution plan generation. + +This script generates a deterministic shard execution plan from +`run_linux_unified_gate.py` manifest so Linux CI can fan out jobs with +consistent shard slicing and command contracts. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import shlex +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Sequence + + +def _load_unified_gate_module(): + script_path = Path(__file__).resolve().with_name("run_linux_unified_gate.py") + spec = importlib.util.spec_from_file_location("linux_unified_gate_script", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _load_execution_gate_module(): + script_path = Path(__file__).resolve().with_name("run_p2_linux_unified_execution_gate.py") + spec = importlib.util.spec_from_file_location("p2_linux_unified_execution_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _validate_manifest(test_files: Sequence[str]) -> list[str]: + missing: list[str] = [] + for test_file in test_files: + if not Path(test_file).is_file(): + missing.append(test_file) + return missing + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def build_shard_execution_command( + *, + python_executable: str, + shard_total: int, + shard_index: int, + report_dir: Path, + continue_on_failure: bool, + pytest_args: Sequence[str] | None, +) -> list[str]: + command = [ + python_executable, + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + str(report_dir), + "--shard-total", + str(shard_total), + "--shard-index", + str(shard_index), + ] + if continue_on_failure: + command.append("--continue-on-failure") + if pytest_args: + command.extend(["--", *pytest_args]) + return command + + +def build_shard_plan_payload( + *, + python_executable: str, + test_files: Sequence[str], + shard_total: int, + report_root: Path, + continue_on_failure: bool, + pytest_args: Sequence[str] | None, +) -> dict[str, Any]: + if shard_total < 1: + raise ValueError("shard_total must be >= 1") + + execution_gate = _load_execution_gate_module() + + shards: list[dict[str, Any]] = [] + for shard_index in range(1, shard_total + 1): + selected_test_files = execution_gate.select_shard_test_files( + test_files, + shard_total=shard_total, + shard_index=shard_index, + ) + report_dir = report_root / f"shard-{shard_index}" + command_parts = build_shard_execution_command( + python_executable=python_executable, + shard_total=shard_total, + shard_index=shard_index, + report_dir=report_dir, + continue_on_failure=continue_on_failure, + pytest_args=pytest_args, + ) + shards.append( + { + "shard_index": shard_index, + "total_tests": len(selected_test_files), + "test_files": list(selected_test_files), + "report_dir": str(report_dir), + "summary_path": str(report_dir / "summary.json"), + "command": _format_shell_command(command_parts), + "command_parts": command_parts, + } + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "python_executable": python_executable, + "shard_total": shard_total, + "manifest_total_tests": len(test_files), + "continue_on_failure": continue_on_failure, + "pytest_args": list(pytest_args or []), + "report_root": str(report_root), + "shards": shards, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate deterministic Linux shard execution plan for unified gate" + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use in generated shard commands", + ) + parser.add_argument( + "--shard-total", + type=int, + required=True, + help="Total shard count for Linux parallel jobs", + ) + parser.add_argument( + "--report-root", + default=".claude/reports/linux_unified_gate/shards", + help="Root directory used to derive per-shard report dirs", + ) + parser.add_argument( + "--output", + default=".claude/reports/linux_unified_gate/shard_plan.json", + help="Shard plan output JSON path", + ) + parser.add_argument( + "--continue-on-failure", + action="store_true", + help="Include continue-on-failure flag in generated shard commands", + ) + parser.add_argument( + "--print-commands", + action="store_true", + help="Print per-shard execution commands", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not write output file; only print generated plan details", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to each execution-gate command after '--'", + ) + args = parser.parse_args() + + unified_gate = _load_unified_gate_module() + test_files: tuple[str, ...] = tuple(unified_gate.LINUX_UNIFIED_TEST_FILES) + + missing = _validate_manifest(test_files) + if missing: + for path in missing: + print(f"[p2-linux-shard-plan-gate] missing test file: {path}", file=sys.stderr) + return 2 + + forwarded_args = list(args.pytest_args) + if forwarded_args and forwarded_args[0] == "--": + forwarded_args = forwarded_args[1:] + + try: + payload = build_shard_plan_payload( + python_executable=args.python_executable, + test_files=test_files, + shard_total=args.shard_total, + report_root=Path(args.report_root), + continue_on_failure=args.continue_on_failure, + pytest_args=forwarded_args, + ) + except ValueError as exc: + print(f"[p2-linux-shard-plan-gate] {exc}", file=sys.stderr) + return 2 + + if not args.dry_run: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"shard plan: {output_path}") + + print( + "plan summary: " + f"{payload['manifest_total_tests']} tests across {payload['shard_total']} shards" + ) + for shard in payload["shards"]: + print(f"- shard {shard['shard_index']}: {shard['total_tests']} tests") + + if args.print_commands or args.dry_run: + for shard in payload["shards"]: + print(f"[shard {shard['shard_index']}] {shard['command']}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_unified_execution_gate.py b/scripts/run_p2_linux_unified_execution_gate.py new file mode 100644 index 0000000..ce3b713 --- /dev/null +++ b/scripts/run_p2_linux_unified_execution_gate.py @@ -0,0 +1,281 @@ +"""Phase 2 card P2-09 execution gate for Linux unified validation. + +This script is intended for Linux stage execution. It can: +1) validate unified manifest integrity, +2) print or dry-run generated pytest commands, +3) execute curated test files sequentially and emit junit + json reports. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Sequence + + +def _load_unified_gate_module(): + script_path = Path(__file__).resolve().with_name("run_linux_unified_gate.py") + spec = importlib.util.spec_from_file_location("linux_unified_gate_script", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _validate_manifest(test_files: Sequence[str]) -> list[str]: + missing: list[str] = [] + for test_file in test_files: + if not Path(test_file).is_file(): + missing.append(test_file) + return missing + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _junit_report_path(report_dir: Path, test_file: str) -> Path: + stem = Path(test_file).stem + return report_dir / f"{stem}.xml" + + +def build_gate_commands( + *, + python_executable: str, + test_files: Sequence[str], + report_dir: Path, + extra_pytest_args: Sequence[str] | None = None, +) -> list[list[str]]: + commands: list[list[str]] = [] + for test_file in test_files: + junit_path = _junit_report_path(report_dir, test_file) + command = [ + python_executable, + "-m", + "pytest", + "-q", + "-c", + "pytest.ini", + test_file, + "--junitxml", + str(junit_path), + ] + if extra_pytest_args: + command.extend(extra_pytest_args) + commands.append(command) + return commands + + +def select_shard_test_files( + test_files: Sequence[str], + *, + shard_total: int, + shard_index: int, +) -> list[str]: + if shard_total < 1: + raise ValueError("shard_total must be >= 1") + if shard_index < 1 or shard_index > shard_total: + raise ValueError("shard_index must be within [1, shard_total]") + if shard_total == 1: + return list(test_files) + selected: list[str] = [] + target_mod = shard_index - 1 + for index, test_file in enumerate(test_files): + if index % shard_total == target_mod: + selected.append(test_file) + return selected + + +def _build_result_payload( + *, + python_executable: str, + report_dir: Path, + manifest_test_files: Sequence[str], + selected_test_files: Sequence[str], + shard_total: int, + shard_index: int, + commands: Sequence[Sequence[str]], + per_test_status: Sequence[dict[str, Any]], +) -> dict[str, Any]: + failed = sum(1 for item in per_test_status if item["exit_code"] != 0) + passed = len(per_test_status) - failed + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "python_executable": python_executable, + "report_dir": str(report_dir), + "shard_total": shard_total, + "shard_index": shard_index, + "manifest_total_tests": len(manifest_test_files), + "total_tests": len(selected_test_files), + "passed": passed, + "failed": failed, + "test_files": list(selected_test_files), + "commands": [_format_shell_command(command) for command in commands], + "results": list(per_test_status), + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Execute Linux unified gate manifest for Phase 0-2 regression chain" + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use (default: current interpreter)", + ) + parser.add_argument( + "--list-tests", + action="store_true", + help="List unified gate test files and exit", + ) + parser.add_argument( + "--print-commands", + action="store_true", + help="Print generated per-test pytest commands and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate manifest and print generated commands without executing tests", + ) + parser.add_argument( + "--continue-on-failure", + action="store_true", + help="Continue executing remaining tests after a test command fails", + ) + parser.add_argument( + "--report-dir", + default=".claude/reports/linux_unified_gate", + help="Directory for junit xml + summary json output", + ) + parser.add_argument( + "--shard-total", + type=int, + default=1, + help="Total number of shards for parallel Linux execution (default: 1)", + ) + parser.add_argument( + "--shard-index", + type=int, + default=1, + help="1-based shard index for this job (default: 1)", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to every pytest invocation after '--'", + ) + args = parser.parse_args() + + unified_gate = _load_unified_gate_module() + test_files: tuple[str, ...] = tuple(unified_gate.LINUX_UNIFIED_TEST_FILES) + + if args.list_tests: + for test_file in test_files: + print(test_file) + return 0 + + missing = _validate_manifest(test_files) + if missing: + for path in missing: + print(f"[p2-linux-unified-execution-gate] missing test file: {path}", file=sys.stderr) + return 2 + + try: + selected_test_files = select_shard_test_files( + test_files, + shard_total=args.shard_total, + shard_index=args.shard_index, + ) + except ValueError as exc: + print(f"[p2-linux-unified-execution-gate] {exc}", file=sys.stderr) + return 2 + + forwarded_args = list(args.pytest_args) + if forwarded_args and forwarded_args[0] == "--": + forwarded_args = forwarded_args[1:] + + report_dir = Path(args.report_dir) + commands = build_gate_commands( + python_executable=args.python_executable, + test_files=selected_test_files, + report_dir=report_dir, + extra_pytest_args=forwarded_args, + ) + + if args.print_commands or args.dry_run: + print( + ( + f"# shard {args.shard_index}/{args.shard_total} " + f"(tests: {len(selected_test_files)}/{len(test_files)})" + ) + ) + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {_format_shell_command(command)}") + if args.print_commands and not args.dry_run: + return 0 + if args.dry_run: + return 0 + + report_dir.mkdir(parents=True, exist_ok=True) + project_root = Path(__file__).resolve().parents[1] + if not selected_test_files: + payload = _build_result_payload( + python_executable=args.python_executable, + report_dir=report_dir, + manifest_test_files=test_files, + selected_test_files=selected_test_files, + shard_total=args.shard_total, + shard_index=args.shard_index, + commands=commands, + per_test_status=[], + ) + summary_path = report_dir / "summary.json" + summary_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + print("no tests selected for this shard") + print(f"summary: {summary_path}") + return 0 + + per_test_status: list[dict[str, Any]] = [] + overall_exit = 0 + for index, (test_file, command) in enumerate(zip(selected_test_files, commands), start=1): + print(f"[{index}/{len(commands)}] {_format_shell_command(command)}") + exit_code = subprocess.call(command, cwd=project_root) + per_test_status.append( + { + "test_file": test_file, + "exit_code": exit_code, + "junit_xml": str(_junit_report_path(report_dir, test_file)), + } + ) + if exit_code != 0: + overall_exit = 1 + if not args.continue_on_failure: + break + + payload = _build_result_payload( + python_executable=args.python_executable, + report_dir=report_dir, + manifest_test_files=test_files, + selected_test_files=selected_test_files, + shard_total=args.shard_total, + shard_index=args.shard_index, + commands=commands, + per_test_status=per_test_status, + ) + summary_path = report_dir / "summary.json" + summary_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"summary: {summary_path}") + return overall_exit + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_linux_unified_pipeline_gate.py b/scripts/run_p2_linux_unified_pipeline_gate.py new file mode 100644 index 0000000..dadc356 --- /dev/null +++ b/scripts/run_p2_linux_unified_pipeline_gate.py @@ -0,0 +1,308 @@ +"""Phase 2 card P2-14 gate for Linux unified pipeline orchestration. + +This script composes the Linux validation chain end-to-end: +1) run unified execution gate (P2-09/P2-11), +2) aggregate shard summaries (P2-12), +3) publish final report artifacts (P2-13). +""" + +from __future__ import annotations + +import argparse +import shlex +import subprocess +import sys +from pathlib import Path +from typing import Sequence + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _execution_summary_path(report_dir: Path) -> Path: + return report_dir / "summary.json" + + +def build_execution_command( + *, + python_executable: str, + report_dir: Path, + shard_total: int, + shard_index: int, + continue_on_failure: bool, + pytest_args: Sequence[str] | None, +) -> list[str]: + command = [ + python_executable, + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + str(report_dir), + "--shard-total", + str(shard_total), + "--shard-index", + str(shard_index), + ] + if continue_on_failure: + command.append("--continue-on-failure") + if pytest_args: + command.extend(["--", *pytest_args]) + return command + + +def build_aggregation_command( + *, + python_executable: str, + summary_paths: Sequence[str], + summary_globs: Sequence[str], + artifacts_dir: str | None, + output_path: Path, +) -> list[str]: + command = [ + python_executable, + "scripts/run_p2_linux_shard_aggregation_gate.py", + "--output", + str(output_path), + ] + for path in summary_paths: + command.extend(["--summary", path]) + for pattern in summary_globs: + command.extend(["--summary-glob", pattern]) + if artifacts_dir: + command.extend(["--artifacts-dir", artifacts_dir]) + return command + + +def build_publish_command( + *, + python_executable: str, + merged_summary: Path, + output_json: Path, + output_markdown: Path, +) -> list[str]: + return [ + python_executable, + "scripts/run_p2_linux_report_publish_gate.py", + "--merged-summary", + str(merged_summary), + "--output-json", + str(output_json), + "--output-markdown", + str(output_markdown), + ] + + +def build_pipeline_commands( + *, + python_executable: str, + execution_report_dir: Path, + shard_total: int, + shard_index: int, + continue_on_failure: bool, + skip_execution: bool, + skip_aggregation: bool, + skip_publish: bool, + summary_paths: Sequence[str], + summary_globs: Sequence[str], + artifacts_dir: str | None, + merged_summary_output: Path, + final_report_json: Path, + final_report_markdown: Path, + pytest_args: Sequence[str] | None, +) -> list[tuple[str, list[str]]]: + pipeline: list[tuple[str, list[str]]] = [] + + if not skip_execution: + pipeline.append( + ( + "execution", + build_execution_command( + python_executable=python_executable, + report_dir=execution_report_dir, + shard_total=shard_total, + shard_index=shard_index, + continue_on_failure=continue_on_failure, + pytest_args=pytest_args, + ), + ) + ) + + if not skip_aggregation: + aggregation_summaries = list(summary_paths) + if not skip_execution: + aggregation_summaries.append(str(_execution_summary_path(execution_report_dir))) + pipeline.append( + ( + "aggregation", + build_aggregation_command( + python_executable=python_executable, + summary_paths=aggregation_summaries, + summary_globs=summary_globs, + artifacts_dir=artifacts_dir, + output_path=merged_summary_output, + ), + ) + ) + + if not skip_publish: + pipeline.append( + ( + "publish", + build_publish_command( + python_executable=python_executable, + merged_summary=merged_summary_output, + output_json=final_report_json, + output_markdown=final_report_markdown, + ), + ) + ) + + return pipeline + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run Linux unified validation pipeline: execution -> aggregation -> publish" + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use for all sub-commands (default: current interpreter)", + ) + parser.add_argument( + "--execution-report-dir", + default=".claude/reports/linux_unified_gate", + help="Report dir passed to execution gate (writes summary.json + junit XMLs)", + ) + parser.add_argument( + "--shard-total", + type=int, + default=1, + help="Total shard count forwarded to execution gate (default: 1)", + ) + parser.add_argument( + "--shard-index", + type=int, + default=1, + help="1-based shard index forwarded to execution gate (default: 1)", + ) + parser.add_argument( + "--continue-on-failure", + action="store_true", + help="Forward continue-on-failure flag to execution gate", + ) + parser.add_argument( + "--summary", + action="append", + default=[], + help="Additional summary.json path for aggregation (repeatable)", + ) + parser.add_argument( + "--summary-glob", + action="append", + default=[], + help="Additional summary glob for aggregation (repeatable)", + ) + parser.add_argument( + "--artifacts-dir", + default=None, + help="Directory recursively scanned for summary.json by aggregation gate", + ) + parser.add_argument( + "--merged-summary-output", + default=".claude/reports/linux_unified_gate/merged_summary.json", + help="Merged summary output path for aggregation stage", + ) + parser.add_argument( + "--final-report-json", + default=".claude/reports/linux_unified_gate/final_report.json", + help="Final JSON report path for publish stage", + ) + parser.add_argument( + "--final-report-markdown", + default=".claude/reports/linux_unified_gate/final_report.md", + help="Final Markdown report path for publish stage", + ) + parser.add_argument( + "--skip-execution", + action="store_true", + help="Skip execution stage (expect existing summaries from external jobs)", + ) + parser.add_argument( + "--skip-aggregation", + action="store_true", + help="Skip aggregation stage (expect existing merged summary)", + ) + parser.add_argument( + "--skip-publish", + action="store_true", + help="Skip publish stage", + ) + parser.add_argument( + "--print-commands", + action="store_true", + help="Print planned commands and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Alias of --print-commands", + ) + parser.add_argument( + "--fail-fast", + action="store_true", + help="Stop at first non-zero stage exit code (default keeps running remaining stages)", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to execution gate pytest invocations after '--'", + ) + args = parser.parse_args() + + forwarded_args = list(args.pytest_args) + if forwarded_args and forwarded_args[0] == "--": + forwarded_args = forwarded_args[1:] + + pipeline = build_pipeline_commands( + python_executable=args.python_executable, + execution_report_dir=Path(args.execution_report_dir), + shard_total=args.shard_total, + shard_index=args.shard_index, + continue_on_failure=args.continue_on_failure, + skip_execution=args.skip_execution, + skip_aggregation=args.skip_aggregation, + skip_publish=args.skip_publish, + summary_paths=args.summary, + summary_globs=args.summary_glob, + artifacts_dir=args.artifacts_dir, + merged_summary_output=Path(args.merged_summary_output), + final_report_json=Path(args.final_report_json), + final_report_markdown=Path(args.final_report_markdown), + pytest_args=forwarded_args, + ) + if not pipeline: + print("[p2-linux-unified-pipeline-gate] no stages selected") + return 0 + + if args.print_commands or args.dry_run: + for index, (stage, command) in enumerate(pipeline, start=1): + print(f"{index}. [{stage}] {_format_shell_command(command)}") + if args.dry_run or args.print_commands: + return 0 + + project_root = Path(__file__).resolve().parents[1] + overall_exit = 0 + for index, (stage, command) in enumerate(pipeline, start=1): + print(f"[{index}/{len(pipeline)}] stage={stage} {_format_shell_command(command)}") + exit_code = subprocess.call(command, cwd=project_root) + if exit_code != 0: + if overall_exit == 0: + overall_exit = exit_code + if args.fail_fast: + return exit_code + return overall_exit + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_dispatch_gate.py b/scripts/run_p2_lv_dispatch_gate.py new file mode 100644 index 0000000..9d61161 --- /dev/null +++ b/scripts/run_p2_lv_dispatch_gate.py @@ -0,0 +1,892 @@ +"""Phase 2 card P2-60 gate for Linux CI workflow Linux-validation dispatch. + +This script consumes the P2-59 terminal verdict artifact and converges the +Linux validation dispatch behavior: +1) validate terminal verdict + drift closure contract, +2) build/execute Linux validation command when verdict allows proceeding, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation", + "ready_with_follow_up_for_linux_validation", + "blocked", + "contract_failed", +} +ALLOWED_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation", + "proceed_linux_validation_with_follow_up", + "halt_linux_validation_blocker", + "abort_linux_validation", +} +ALLOWED_TERMINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: set[str] = {"passed", "failed"} + +ALLOWED_LINUX_VALIDATION_DISPATCH_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatched", + "dispatch_failed", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_DISPATCH_DECISIONS: set[str] = { + "dispatch_linux_validation", + "dispatch_linux_validation_with_follow_up", + "hold_linux_validation_blocker", + "abort_linux_validation_dispatch", +} +ALLOWED_LINUX_VALIDATION_DISPATCH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_terminal_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: terminal verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: terminal verdict payload must be object") + + required_fields = ( + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "follow_up_queue_url", + "release_run_id", + "release_run_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_status = _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ) + if terminal_status not in ALLOWED_TERMINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'terminal_verdict_status' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_STATUSES)}" + ) + + terminal_decision = _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ) + if terminal_decision not in ALLOWED_TERMINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'terminal_verdict_decision' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_DECISIONS)}" + ) + + terminal_channel = _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ) + if terminal_channel not in ALLOWED_TERMINAL_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'terminal_verdict_channel' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_CHANNELS)}" + ) + + terminal_exit_code = _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ) + if terminal_exit_code < 0: + raise ValueError(f"{path}: field 'terminal_verdict_exit_code' must be >= 0") + + drift_status = _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ) + if drift_status not in ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: + raise ValueError( + f"{path}: field 'gate_manifest_drift_status' must be one of " + f"{sorted(ALLOWED_GATE_MANIFEST_DRIFT_STATUSES)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": terminal_channel, + "terminal_verdict_status": terminal_status, + "terminal_verdict_decision": terminal_decision, + "terminal_verdict_exit_code": terminal_exit_code, + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_command_parts( + *, + terminal_verdict_report: dict[str, Any], + python_executable: str, + linux_validation_command: str, +) -> list[str]: + if not bool(terminal_verdict_report["terminal_verdict_should_proceed"]): + return [] + if linux_validation_command.strip(): + try: + parts = shlex.split(linux_validation_command) + except ValueError as exc: + raise ValueError(f"invalid --linux-validation-command ({exc})") from exc + if not parts: + raise ValueError("invalid --linux-validation-command (empty command)") + return parts + return [ + python_executable, + "scripts/run_p2_linux_unified_pipeline_gate.py", + "--continue-on-failure", + ] + + +def execute_linux_validation_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"linux validation command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_linux_validation_dispatch_payload( + terminal_verdict_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + linux_validation_timeout_seconds: int, + command_parts: list[str], + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + terminal_status = str(terminal_verdict_report["terminal_verdict_status"]) + terminal_decision = str(terminal_verdict_report["terminal_verdict_decision"]) + terminal_exit_code = int(terminal_verdict_report["terminal_verdict_exit_code"]) + terminal_should_proceed = bool(terminal_verdict_report["terminal_verdict_should_proceed"]) + terminal_requires_manual_action = bool( + terminal_verdict_report["terminal_verdict_requires_manual_action"] + ) + terminal_channel = str(terminal_verdict_report["terminal_verdict_channel"]) + + drift_status = str(terminal_verdict_report["gate_manifest_drift_status"]) + drift_missing_runtime_tests = list( + terminal_verdict_report["gate_manifest_drift_missing_runtime_tests"] + ) + drift_missing_manifest_entries = list( + terminal_verdict_report["gate_manifest_drift_missing_manifest_entries"] + ) + drift_orphan_manifest_entries = list( + terminal_verdict_report["gate_manifest_drift_orphan_manifest_entries"] + ) + + reason_codes = list(terminal_verdict_report["reason_codes"]) + structural_issues = list(terminal_verdict_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_report["missing_artifacts"]) + evidence_manifest = list(terminal_verdict_report["evidence_manifest"]) + + expected_terminal = { + "ready_for_linux_validation": ( + "proceed_linux_validation", + True, + False, + "release", + 0, + ), + "ready_with_follow_up_for_linux_validation": ( + "proceed_linux_validation_with_follow_up", + True, + False, + "follow_up", + None, + ), + "blocked": ("halt_linux_validation_blocker", False, True, "blocker", None), + "contract_failed": ("abort_linux_validation", False, True, "blocker", None), + } + ( + expected_decision, + expected_should_proceed, + expected_requires_manual_action, + expected_channel, + expected_exit_code, + ) = expected_terminal[terminal_status] + + if terminal_decision != expected_decision: + structural_issues.append("terminal_verdict_decision_mismatch") + if terminal_should_proceed != expected_should_proceed: + structural_issues.append("terminal_verdict_should_proceed_mismatch") + if terminal_requires_manual_action != expected_requires_manual_action: + structural_issues.append("terminal_verdict_requires_manual_action_mismatch") + if terminal_channel != expected_channel: + structural_issues.append("terminal_verdict_channel_mismatch") + if expected_exit_code == 0 and terminal_exit_code != 0: + structural_issues.append("terminal_verdict_exit_code_mismatch_ready") + if expected_exit_code is None and terminal_exit_code == 0: + structural_issues.append("terminal_verdict_exit_code_mismatch_non_ready") + + if drift_status != "passed": + structural_issues.append("gate_manifest_drift_failed") + if drift_missing_runtime_tests: + structural_issues.append("gate_manifest_drift_missing_runtime_tests") + if drift_missing_manifest_entries: + structural_issues.append("gate_manifest_drift_missing_manifest_entries") + if drift_orphan_manifest_entries: + structural_issues.append("gate_manifest_drift_orphan_manifest_entries") + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + structural_issues.append("terminal_verdict_evidence_missing") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + linux_validation_command = _format_shell_command(command_parts) if command_parts else "" + linux_validation_decision: str + linux_validation_channel: str + linux_validation_should_dispatch: bool + linux_validation_requires_manual_action: bool + + if terminal_status == "ready_for_linux_validation": + linux_validation_decision = "dispatch_linux_validation" + linux_validation_channel = "release" + linux_validation_should_dispatch = True + linux_validation_requires_manual_action = False + elif terminal_status == "ready_with_follow_up_for_linux_validation": + linux_validation_decision = "dispatch_linux_validation_with_follow_up" + linux_validation_channel = "follow_up" + linux_validation_should_dispatch = True + linux_validation_requires_manual_action = False + elif terminal_status == "blocked": + linux_validation_decision = "hold_linux_validation_blocker" + linux_validation_channel = "blocker" + linux_validation_should_dispatch = False + linux_validation_requires_manual_action = True + else: + linux_validation_decision = "abort_linux_validation_dispatch" + linux_validation_channel = "blocker" + linux_validation_should_dispatch = False + linux_validation_requires_manual_action = True + + dispatch_attempted = False + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + dispatch_error_type = "" + dispatch_error_message = "" + + if structural_issues or missing_artifacts: + dispatch_status = "contract_failed" + dispatch_decision = "abort_linux_validation_dispatch" + dispatch_exit_code = max(1, terminal_exit_code) + linux_validation_should_dispatch = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.extend(structural_issues) + elif not linux_validation_should_dispatch: + dispatch_status = "blocked" + dispatch_decision = "hold_linux_validation_blocker" + dispatch_exit_code = max(1, terminal_exit_code) + reason_codes.append("linux_validation_dispatch_blocked") + elif dry_run: + if linux_validation_channel == "follow_up": + dispatch_status = "ready_with_follow_up_dry_run" + dispatch_exit_code = max(1, terminal_exit_code) + reason_codes.append("linux_validation_dispatch_ready_with_follow_up_dry_run") + else: + dispatch_status = "ready_dry_run" + dispatch_exit_code = 0 + reason_codes = ["linux_validation_dispatch_ready_dry_run"] + dispatch_decision = linux_validation_decision + else: + dispatch_attempted = True + if command_result is None: + dispatch_status = "dispatch_failed" + dispatch_decision = linux_validation_decision + dispatch_exit_code = 1 + reason_codes.append("linux_validation_dispatch_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + dispatch_error_type = str(command_result.get("error_type", "")) + dispatch_error_message = str(command_result.get("error_message", "")) + if command_returncode == 0: + dispatch_status = "dispatched" + dispatch_decision = linux_validation_decision + dispatch_exit_code = ( + max(1, terminal_exit_code) + if linux_validation_channel == "follow_up" + else 0 + ) + reason_codes = ["linux_validation_dispatch_command_succeeded"] + else: + dispatch_status = "dispatch_failed" + dispatch_decision = linux_validation_decision + dispatch_exit_code = 1 + if dispatch_error_type == "command_not_found": + reason_codes.append("linux_validation_dispatch_cli_unavailable") + elif dispatch_error_type == "timeout": + reason_codes.append("linux_validation_dispatch_timeout") + else: + reason_codes.append("linux_validation_dispatch_command_failed") + + if dispatch_status not in ALLOWED_LINUX_VALIDATION_DISPATCH_STATUSES: + raise ValueError("internal: unsupported linux_validation_dispatch_status computed") + if dispatch_decision not in ALLOWED_LINUX_VALIDATION_DISPATCH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_dispatch_decision computed") + if linux_validation_channel not in ALLOWED_LINUX_VALIDATION_DISPATCH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"terminal_verdict_status={terminal_status} " + f"terminal_verdict_decision={terminal_decision} " + f"linux_validation_dispatch_status={dispatch_status} " + f"linux_validation_dispatch_decision={dispatch_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_terminal_verdict_report": str(source_path), + "project_root": str(project_root), + "source_release_final_publish_archive_report": str( + terminal_verdict_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + terminal_verdict_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + terminal_verdict_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": drift_missing_runtime_tests, + "gate_manifest_drift_missing_manifest_entries": drift_missing_manifest_entries, + "gate_manifest_drift_orphan_manifest_entries": drift_orphan_manifest_entries, + "terminal_verdict_status": terminal_status, + "terminal_verdict_decision": terminal_decision, + "terminal_verdict_exit_code": terminal_exit_code, + "terminal_verdict_should_proceed": terminal_should_proceed, + "terminal_verdict_requires_manual_action": terminal_requires_manual_action, + "terminal_verdict_channel": terminal_channel, + "linux_validation_should_dispatch": linux_validation_should_dispatch, + "linux_validation_requires_manual_action": linux_validation_requires_manual_action, + "linux_validation_channel": linux_validation_channel, + "linux_validation_dispatch_status": dispatch_status, + "linux_validation_dispatch_decision": dispatch_decision, + "linux_validation_dispatch_exit_code": int(dispatch_exit_code), + "linux_validation_dispatch_attempted": dispatch_attempted, + "linux_validation_timeout_seconds": linux_validation_timeout_seconds, + "linux_validation_command": linux_validation_command, + "linux_validation_command_parts": command_parts, + "dry_run": dry_run, + "command_returncode": command_returncode, + "command_stdout_tail": command_stdout_tail, + "command_stderr_tail": command_stderr_tail, + "dispatch_error_type": dispatch_error_type, + "dispatch_error_message": dispatch_error_message, + "release_run_id": terminal_verdict_report["release_run_id"], + "release_run_url": str(terminal_verdict_report["release_run_url"]), + "follow_up_queue_url": str(terminal_verdict_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Dispatch Report", + "", + ( + "- Linux Validation Dispatch Status: " + f"**{str(payload['linux_validation_dispatch_status']).upper()}**" + ), + ( + "- Linux Validation Dispatch Decision: " + f"`{payload['linux_validation_dispatch_decision']}`" + ), + ( + "- Linux Validation Dispatch Exit Code: " + f"`{payload['linux_validation_dispatch_exit_code']}`" + ), + ( + "- Linux Validation Should Dispatch: " + f"`{payload['linux_validation_should_dispatch']}`" + ), + ( + "- Linux Validation Requires Manual Action: " + f"`{payload['linux_validation_requires_manual_action']}`" + ), + f"- Linux Validation Channel: `{payload['linux_validation_channel']}`", + f"- Terminal Verdict Status: `{payload['terminal_verdict_status']}`", + f"- Gate Manifest Drift Status: `{payload['gate_manifest_drift_status']}`", + f"- Dispatch Attempted: `{payload['linux_validation_dispatch_attempted']}`", + f"- Dry Run: `{payload['dry_run']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_dispatch_summary']}", + "", + "## Linux Validation Command", + ] + + if payload["linux_validation_command"]: + lines.append(f"- `{payload['linux_validation_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Command Output (tail)"]) + stdout_tail = str(payload["command_stdout_tail"]).strip() + stderr_tail = str(payload["command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + command_returncode = payload["command_returncode"] + return { + "workflow_linux_validation_dispatch_status": str( + payload["linux_validation_dispatch_status"] + ), + "workflow_linux_validation_dispatch_decision": str( + payload["linux_validation_dispatch_decision"] + ), + "workflow_linux_validation_dispatch_exit_code": str( + payload["linux_validation_dispatch_exit_code"] + ), + "workflow_linux_validation_dispatch_should_dispatch": ( + "true" if payload["linux_validation_should_dispatch"] else "false" + ), + "workflow_linux_validation_dispatch_requires_manual_action": ( + "true" if payload["linux_validation_requires_manual_action"] else "false" + ), + "workflow_linux_validation_dispatch_channel": str(payload["linux_validation_channel"]), + "workflow_linux_validation_dispatch_attempted": ( + "true" if payload["linux_validation_dispatch_attempted"] else "false" + ), + "workflow_linux_validation_dispatch_command_returncode": ( + "" if command_returncode is None else str(command_returncode) + ), + "workflow_linux_validation_dispatch_terminal_verdict_status": str( + payload["terminal_verdict_status"] + ), + "workflow_linux_validation_dispatch_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_linux_validation_dispatch_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_dispatch_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_dispatch_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_dispatch_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_dispatch_report_json": str(output_json), + "workflow_linux_validation_dispatch_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Dispatch Linux validation command from P2-59 terminal verdict " + "and publish Linux validation dispatch contract" + ) + ) + parser.add_argument( + "--terminal-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json", + help="P2-59 terminal verdict report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root used to execute Linux validation command", + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable used by default Linux validation command", + ) + parser.add_argument( + "--linux-validation-command", + default="", + help="Optional explicit Linux validation command string", + ) + parser.add_argument( + "--linux-validation-timeout-seconds", + type=int, + default=7200, + help="Linux validation command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json", + help="Output Linux validation dispatch JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md", + help="Output Linux validation dispatch markdown path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation dispatch JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.linux_validation_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-dispatch-gate] " + "--linux-validation-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + source_path = Path(args.terminal_verdict_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_verdict_report = load_terminal_verdict_report(source_path) + command_parts = build_linux_validation_command_parts( + terminal_verdict_report=terminal_verdict_report, + python_executable=args.python_executable, + linux_validation_command=args.linux_validation_command, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-linux-validation-dispatch-gate] {exc}", + file=sys.stderr, + ) + return 2 + + command_result: dict[str, Any] | None = None + should_dispatch = bool(terminal_verdict_report["terminal_verdict_should_proceed"]) + if should_dispatch and not args.dry_run: + command_result = execute_linux_validation_command( + command_parts, + cwd=project_root, + timeout_seconds=args.linux_validation_timeout_seconds, + ) + + payload = build_linux_validation_dispatch_payload( + terminal_verdict_report, + source_path=source_path.resolve(), + project_root=project_root.resolve(), + linux_validation_timeout_seconds=args.linux_validation_timeout_seconds, + command_parts=command_parts, + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation dispatch json: {output_json_path}") + print(f"linux validation dispatch markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation dispatch summary: " + f"linux_validation_dispatch_status={payload['linux_validation_dispatch_status']} " + f"linux_validation_dispatch_decision={payload['linux_validation_dispatch_decision']} " + f"linux_validation_dispatch_exit_code={payload['linux_validation_dispatch_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_dispatch_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_final_publish_archive_gate.py b/scripts/run_p2_lv_final_publish_archive_gate.py new file mode 100644 index 0000000..19668e0 --- /dev/null +++ b/scripts/run_p2_lv_final_publish_archive_gate.py @@ -0,0 +1,927 @@ +"""Phase 2 card P2-66 gate for Linux CI workflow Linux-validation final publish archive. + +This script consumes the P2-65 Linux-validation final verdict publish artifact and converges +one terminal final-publish-archive contract for Linux validation: +1) validate Linux validation final verdict publish + evidence consistency, +2) normalize final publish archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_final_validated", + "announce_linux_validation_final_validated_with_follow_up", + "announce_linux_validation_final_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_release_shipped", + "archive_release_shipped_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_final_verdict_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation final verdict publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation final verdict publish payload must be object") + + required_fields = ( + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_final_verdict_publish_status", + "linux_validation_final_verdict_publish_decision", + "linux_validation_final_verdict_publish_exit_code", + "linux_validation_final_verdict_publish_should_notify", + "linux_validation_final_verdict_publish_requires_manual_action", + "linux_validation_final_verdict_publish_channel", + "linux_validation_final_verdict_publish_attempted", + "linux_validation_final_verdict_publish_timeout_seconds", + "linux_validation_final_verdict_publish_command", + "linux_validation_final_verdict_publish_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_final_verdict_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_final_verdict_publish_status"], + field="linux_validation_final_verdict_publish_status", + path=path, + ) + if verdict_status not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_final_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_final_verdict_publish_decision"], + field="linux_validation_final_verdict_publish_decision", + path=path, + ) + if verdict_decision not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_final_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_final_verdict_publish_exit_code"], + field="linux_validation_final_verdict_publish_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_final_verdict_publish_exit_code' must be >= 0") + + verdict_channel = _coerce_str( + payload["linux_validation_final_verdict_publish_channel"], + field="linux_validation_final_verdict_publish_channel", + path=path, + ) + if verdict_channel not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_final_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_final_verdict_publish_status": verdict_status, + "linux_validation_final_verdict_publish_decision": verdict_decision, + "linux_validation_final_verdict_publish_exit_code": verdict_exit_code, + "linux_validation_final_verdict_publish_should_notify": _coerce_bool( + payload["linux_validation_final_verdict_publish_should_notify"], + field="linux_validation_final_verdict_publish_should_notify", + path=path, + ), + "linux_validation_final_verdict_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_final_verdict_publish_requires_manual_action"], + field="linux_validation_final_verdict_publish_requires_manual_action", + path=path, + ), + "linux_validation_final_verdict_publish_channel": verdict_channel, + "linux_validation_final_verdict_publish_attempted": _coerce_bool( + payload["linux_validation_final_verdict_publish_attempted"], + field="linux_validation_final_verdict_publish_attempted", + path=path, + ), + "linux_validation_final_verdict_publish_timeout_seconds": _coerce_int( + payload["linux_validation_final_verdict_publish_timeout_seconds"], + field="linux_validation_final_verdict_publish_timeout_seconds", + path=path, + ), + "linux_validation_final_verdict_publish_command": _coerce_str( + payload["linux_validation_final_verdict_publish_command"], + field="linux_validation_final_verdict_publish_command", + path=path, + ), + "linux_validation_final_verdict_publish_command_parts": _coerce_str_list( + payload["linux_validation_final_verdict_publish_command_parts"], + field="linux_validation_final_verdict_publish_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_final_verdict_publish_summary": _coerce_str( + payload["linux_validation_final_verdict_publish_summary"], + field="linux_validation_final_verdict_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_final_publish_archive_payload( + linux_validation_final_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str(linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_status"]) + verdict_decision = str(linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_decision"]) + verdict_exit_code = int(linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_exit_code"]) + verdict_should_notify = bool( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_should_notify"] + ) + verdict_requires_manual_action = bool( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_requires_manual_action"] + ) + verdict_channel = str(linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_channel"]) + + reason_codes = list(linux_validation_final_verdict_publish_report["reason_codes"]) + structural_issues = list(linux_validation_final_verdict_publish_report["structural_issues"]) + missing_artifacts = list(linux_validation_final_verdict_publish_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_final_verdict_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_final_publish_archive_evidence_missing") + + expected_verdict = { + "published": ( + "announce_linux_validation_final_validated", + 0, + True, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_final_validated_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_linux_validation_final_blocker", + None, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append("linux_validation_final_publish_decision_mismatch") + if verdict_should_notify != expected_should_notify: + structural_issues.append("linux_validation_final_publish_should_notify_mismatch") + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append("linux_validation_final_publish_requires_manual_action_mismatch") + if verdict_channel != expected_channel: + structural_issues.append("linux_validation_final_publish_channel_mismatch") + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append("linux_validation_final_verdict_publish_exit_code_mismatch_published") + if expected_exit_code is None and verdict_exit_code == 0 and verdict_status != "published": + structural_issues.append("linux_validation_final_verdict_publish_exit_code_mismatch_non_published") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_archive: bool + publish_requires_manual_action: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_archive" + publish_exit_code = max(1, verdict_exit_code) + publish_should_archive = False + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "published": + publish_status = "archived" + publish_decision = "archive_release_shipped" + publish_exit_code = 0 + publish_should_archive = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["linux_validation_final_publish_archive_archived"] + elif verdict_status == "published_with_follow_up": + publish_status = "archived_with_follow_up" + publish_decision = "archive_release_shipped_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_archive = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("linux_validation_final_publish_archive_archived_with_follow_up") + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "archive_release_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_archive = False + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_final_publish_archive_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_archive" + publish_exit_code = max(1, verdict_exit_code) + publish_should_archive = False + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_final_publish_archive_upstream_contract_failed") + + if publish_status not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_STATUSES: + raise ValueError("internal: unsupported linux_validation_final_publish_archive_status computed") + if publish_decision not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_DECISIONS: + raise ValueError("internal: unsupported linux_validation_final_publish_archive_decision computed") + if publish_channel not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_CHANNELS: + raise ValueError("internal: unsupported linux_validation_final_publish_archive_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_final_verdict_publish_status={verdict_status} " + f"linux_validation_final_publish_archive_status={publish_status} " + f"linux_validation_final_publish_archive_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_final_verdict_publish_report": str(source_path), + "source_linux_validation_dispatch_report": str( + linux_validation_final_verdict_publish_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(linux_validation_final_verdict_publish_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_final_verdict_publish_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_final_verdict_publish_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_final_verdict_publish_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_final_verdict_publish_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_final_verdict_publish_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_final_verdict_publish_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_final_verdict_publish_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_final_verdict_publish_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(linux_validation_final_verdict_publish_report["terminal_verdict_status"]), + "terminal_verdict_decision": str( + linux_validation_final_verdict_publish_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_final_verdict_publish_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_final_verdict_publish_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_final_verdict_publish_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(linux_validation_final_verdict_publish_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + linux_validation_final_verdict_publish_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_final_verdict_publish_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(linux_validation_final_verdict_publish_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + linux_validation_final_verdict_publish_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_final_verdict_publish_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_final_verdict_publish_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_final_verdict_publish_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_final_verdict_publish_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_final_verdict_publish_status": verdict_status, + "linux_validation_final_verdict_publish_decision": verdict_decision, + "linux_validation_final_verdict_publish_exit_code": verdict_exit_code, + "linux_validation_final_verdict_publish_should_notify": verdict_should_notify, + "linux_validation_final_verdict_publish_requires_manual_action": verdict_requires_manual_action, + "linux_validation_final_verdict_publish_channel": verdict_channel, + "linux_validation_final_verdict_publish_attempted": bool( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_attempted"] + ), + "linux_validation_final_verdict_publish_timeout_seconds": int( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_timeout_seconds"] + ), + "linux_validation_final_verdict_publish_command": str( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_command"] + ), + "linux_validation_final_verdict_publish_command_parts": list( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_command_parts"] + ), + "dry_run": bool(linux_validation_final_verdict_publish_report["dry_run"]), + "verdict_command_returncode": linux_validation_final_verdict_publish_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str( + linux_validation_final_verdict_publish_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_final_verdict_publish_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str(linux_validation_final_verdict_publish_report["verdict_error_type"]), + "verdict_error_message": str(linux_validation_final_verdict_publish_report["verdict_error_message"]), + "release_run_id": linux_validation_final_verdict_publish_report["release_run_id"], + "release_run_url": str(linux_validation_final_verdict_publish_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_final_verdict_publish_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_final_verdict_publish_report["linux_validation_dispatch_summary"] + ), + "linux_validation_final_verdict_publish_summary": str( + linux_validation_final_verdict_publish_report["linux_validation_final_verdict_publish_summary"] + ), + "linux_validation_final_publish_archive_status": publish_status, + "linux_validation_final_publish_archive_decision": publish_decision, + "linux_validation_final_publish_archive_exit_code": publish_exit_code, + "linux_validation_final_publish_archive_should_archive": publish_should_archive, + "linux_validation_final_publish_archive_requires_manual_action": publish_requires_manual_action, + "linux_validation_final_publish_archive_channel": publish_channel, + "linux_validation_final_publish_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Final Publish Archive Report", + "", + ( + "- Linux Validation Final Publish Archive Status: " + f"**{str(payload['linux_validation_final_publish_archive_status']).upper()}**" + ), + ( + "- Linux Validation Final Publish Archive Decision: " + f"`{payload['linux_validation_final_publish_archive_decision']}`" + ), + ( + "- Linux Validation Final Publish Archive Exit Code: " + f"`{payload['linux_validation_final_publish_archive_exit_code']}`" + ), + ( + "- Linux Validation Final Publish Archive Should Archive: " + f"`{payload['linux_validation_final_publish_archive_should_archive']}`" + ), + ( + "- Linux Validation Final Publish Archive Requires Manual Action: " + f"`{payload['linux_validation_final_publish_archive_requires_manual_action']}`" + ), + ( + "- Linux Validation Final Publish Archive Channel: " + f"`{payload['linux_validation_final_publish_archive_channel']}`" + ), + f"- Linux Validation Final Verdict Status: `{payload['linux_validation_final_verdict_publish_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_final_publish_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_final_publish_archive_status": str( + payload["linux_validation_final_publish_archive_status"] + ), + "workflow_linux_validation_final_publish_archive_decision": str( + payload["linux_validation_final_publish_archive_decision"] + ), + "workflow_linux_validation_final_publish_archive_exit_code": str( + payload["linux_validation_final_publish_archive_exit_code"] + ), + "workflow_linux_validation_final_publish_archive_should_archive": ( + "true" if payload["linux_validation_final_publish_archive_should_archive"] else "false" + ), + "workflow_linux_validation_final_publish_archive_requires_manual_action": ( + "true" + if payload["linux_validation_final_publish_archive_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_final_publish_archive_channel": str( + payload["linux_validation_final_publish_archive_channel"] + ), + "workflow_linux_validation_final_publish_archive_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_final_publish_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_final_publish_archive_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_final_publish_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_final_publish_archive_report_json": str(output_json), + "workflow_linux_validation_final_publish_archive_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation final verdict publish contract " + "from P2-65 Linux validation final verdict publish report" + ) + ) + parser.add_argument( + "--linux-validation-final-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json", + help="P2-65 Linux validation final verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json", + help="Output Linux validation final publish archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md", + help="Output Linux validation final publish archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation final publish archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path(args.linux_validation_final_verdict_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_final_verdict_publish_report(verdict_report_path) + payload = build_linux_validation_final_publish_archive_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-final-publish-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation final publish archive json: {output_json_path}") + print(f"linux validation final publish archive markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation final publish archive summary: " + f"linux_validation_final_publish_archive_status=" + f"{payload['linux_validation_final_publish_archive_status']} " + f"linux_validation_final_publish_archive_decision=" + f"{payload['linux_validation_final_publish_archive_decision']} " + f"linux_validation_final_publish_archive_exit_code=" + f"{payload['linux_validation_final_publish_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_final_publish_archive_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + + + + + + + diff --git a/scripts/run_p2_lv_final_verdict_gate.py b/scripts/run_p2_lv_final_verdict_gate.py new file mode 100644 index 0000000..a6a5020 --- /dev/null +++ b/scripts/run_p2_lv_final_verdict_gate.py @@ -0,0 +1,1081 @@ +"""Phase 2 card P2-64 gate for Linux CI workflow Linux-validation final verdict. + +This script consumes the P2-63 Linux-validation terminal publish artifact and +converges one final Linux validation verdict contract: +1) validate Linux validation terminal publish + evidence consistency, +2) normalize final validation semantics for downstream closure consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_STATUSES: set[str] = { + "terminal_published", + "terminal_published_with_follow_up", + "terminal_blocked", + "terminal_contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_passed", + "announce_linux_validation_terminal_passed_with_follow_up", + "announce_linux_validation_terminal_blocker", + "abort_terminal_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation_terminal", + "accept_linux_validation_terminal_with_follow_up", + "escalate_linux_validation_terminal_blocker", + "abort_linux_validation_terminal_verdict", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_terminal_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation terminal publish payload must be object") + + required_fields = ( + "source_linux_validation_verdict_publish_report", + "source_linux_validation_verdict_report", + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_verdict_status", + "linux_validation_verdict_decision", + "linux_validation_verdict_exit_code", + "linux_validation_passed", + "linux_validation_verdict_requires_manual_action", + "linux_validation_verdict_channel", + "linux_validation_verdict_attempted", + "linux_validation_verdict_timeout_seconds", + "linux_validation_verdict_command", + "linux_validation_verdict_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_verdict_summary", + "linux_validation_verdict_publish_status", + "linux_validation_verdict_publish_decision", + "linux_validation_verdict_publish_exit_code", + "linux_validation_verdict_publish_should_notify", + "linux_validation_verdict_publish_requires_manual_action", + "linux_validation_verdict_publish_channel", + "linux_validation_verdict_publish_summary", + "linux_validation_terminal_publish_status", + "linux_validation_terminal_publish_decision", + "linux_validation_terminal_publish_exit_code", + "linux_validation_terminal_publish_should_notify", + "linux_validation_terminal_publish_ready_for_handoff", + "linux_validation_terminal_publish_requires_manual_action", + "linux_validation_terminal_publish_channel", + "linux_validation_terminal_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_publish_status = _coerce_str( + payload["linux_validation_terminal_publish_status"], + field="linux_validation_terminal_publish_status", + path=path, + ) + if terminal_publish_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_STATUSES)}" + ) + + terminal_publish_decision = _coerce_str( + payload["linux_validation_terminal_publish_decision"], + field="linux_validation_terminal_publish_decision", + path=path, + ) + if terminal_publish_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_DECISIONS)}" + ) + + terminal_publish_exit_code = _coerce_int( + payload["linux_validation_terminal_publish_exit_code"], + field="linux_validation_terminal_publish_exit_code", + path=path, + ) + if terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_publish_exit_code' must be >= 0" + ) + + terminal_publish_channel = _coerce_str( + payload["linux_validation_terminal_publish_channel"], + field="linux_validation_terminal_publish_channel", + path=path, + ) + if terminal_publish_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_verdict_publish_report": _coerce_str( + payload["source_linux_validation_verdict_publish_report"], + field="source_linux_validation_verdict_publish_report", + path=path, + ), + "source_linux_validation_verdict_report": _coerce_str( + payload["source_linux_validation_verdict_report"], + field="source_linux_validation_verdict_report", + path=path, + ), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_verdict_status": _coerce_str( + payload["linux_validation_verdict_status"], + field="linux_validation_verdict_status", + path=path, + ), + "linux_validation_verdict_decision": _coerce_str( + payload["linux_validation_verdict_decision"], + field="linux_validation_verdict_decision", + path=path, + ), + "linux_validation_verdict_exit_code": _coerce_int( + payload["linux_validation_verdict_exit_code"], + field="linux_validation_verdict_exit_code", + path=path, + ), + "linux_validation_passed": _coerce_bool( + payload["linux_validation_passed"], + field="linux_validation_passed", + path=path, + ), + "linux_validation_verdict_requires_manual_action": _coerce_bool( + payload["linux_validation_verdict_requires_manual_action"], + field="linux_validation_verdict_requires_manual_action", + path=path, + ), + "linux_validation_verdict_channel": _coerce_str( + payload["linux_validation_verdict_channel"], + field="linux_validation_verdict_channel", + path=path, + ), + "linux_validation_verdict_attempted": _coerce_bool( + payload["linux_validation_verdict_attempted"], + field="linux_validation_verdict_attempted", + path=path, + ), + "linux_validation_verdict_timeout_seconds": _coerce_int( + payload["linux_validation_verdict_timeout_seconds"], + field="linux_validation_verdict_timeout_seconds", + path=path, + ), + "linux_validation_verdict_command": _coerce_str( + payload["linux_validation_verdict_command"], + field="linux_validation_verdict_command", + path=path, + ), + "linux_validation_verdict_command_parts": _coerce_str_list( + payload["linux_validation_verdict_command_parts"], + field="linux_validation_verdict_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_verdict_summary": _coerce_str( + payload["linux_validation_verdict_summary"], + field="linux_validation_verdict_summary", + path=path, + ), + "linux_validation_verdict_publish_status": _coerce_str( + payload["linux_validation_verdict_publish_status"], + field="linux_validation_verdict_publish_status", + path=path, + ), + "linux_validation_verdict_publish_decision": _coerce_str( + payload["linux_validation_verdict_publish_decision"], + field="linux_validation_verdict_publish_decision", + path=path, + ), + "linux_validation_verdict_publish_exit_code": _coerce_int( + payload["linux_validation_verdict_publish_exit_code"], + field="linux_validation_verdict_publish_exit_code", + path=path, + ), + "linux_validation_verdict_publish_should_notify": _coerce_bool( + payload["linux_validation_verdict_publish_should_notify"], + field="linux_validation_verdict_publish_should_notify", + path=path, + ), + "linux_validation_verdict_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_verdict_publish_requires_manual_action"], + field="linux_validation_verdict_publish_requires_manual_action", + path=path, + ), + "linux_validation_verdict_publish_channel": _coerce_str( + payload["linux_validation_verdict_publish_channel"], + field="linux_validation_verdict_publish_channel", + path=path, + ), + "linux_validation_verdict_publish_summary": _coerce_str( + payload["linux_validation_verdict_publish_summary"], + field="linux_validation_verdict_publish_summary", + path=path, + ), + "linux_validation_terminal_publish_status": terminal_publish_status, + "linux_validation_terminal_publish_decision": terminal_publish_decision, + "linux_validation_terminal_publish_exit_code": terminal_publish_exit_code, + "linux_validation_terminal_publish_should_notify": _coerce_bool( + payload["linux_validation_terminal_publish_should_notify"], + field="linux_validation_terminal_publish_should_notify", + path=path, + ), + "linux_validation_terminal_publish_ready_for_handoff": _coerce_bool( + payload["linux_validation_terminal_publish_ready_for_handoff"], + field="linux_validation_terminal_publish_ready_for_handoff", + path=path, + ), + "linux_validation_terminal_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_publish_requires_manual_action"], + field="linux_validation_terminal_publish_requires_manual_action", + path=path, + ), + "linux_validation_terminal_publish_channel": terminal_publish_channel, + "linux_validation_terminal_publish_summary": _coerce_str( + payload["linux_validation_terminal_publish_summary"], + field="linux_validation_terminal_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_final_verdict_payload( + terminal_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + terminal_status = str(terminal_publish_report["linux_validation_terminal_publish_status"]) + terminal_decision = str(terminal_publish_report["linux_validation_terminal_publish_decision"]) + terminal_exit_code = int(terminal_publish_report["linux_validation_terminal_publish_exit_code"]) + terminal_should_notify = bool( + terminal_publish_report["linux_validation_terminal_publish_should_notify"] + ) + terminal_ready_for_handoff = bool( + terminal_publish_report["linux_validation_terminal_publish_ready_for_handoff"] + ) + terminal_requires_manual_action = bool( + terminal_publish_report["linux_validation_terminal_publish_requires_manual_action"] + ) + terminal_channel = str(terminal_publish_report["linux_validation_terminal_publish_channel"]) + + reason_codes = list(terminal_publish_report["reason_codes"]) + structural_issues = list(terminal_publish_report["structural_issues"]) + missing_artifacts = list(terminal_publish_report["missing_artifacts"]) + evidence_manifest = list(terminal_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_final_verdict_evidence_missing") + + expected_terminal = { + "terminal_published": ( + "announce_linux_validation_terminal_passed", + 0, + True, + True, + False, + "release", + ), + "terminal_published_with_follow_up": ( + "announce_linux_validation_terminal_passed_with_follow_up", + None, + True, + True, + False, + "follow_up", + ), + "terminal_blocked": ( + "announce_linux_validation_terminal_blocker", + None, + True, + False, + True, + "blocker", + ), + "terminal_contract_failed": ( + "abort_terminal_publish", + None, + True, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_ready_for_handoff, + expected_requires_manual_action, + expected_channel, + ) = expected_terminal[terminal_status] + + if terminal_decision != expected_decision: + structural_issues.append("linux_validation_terminal_publish_decision_mismatch") + if terminal_should_notify != expected_should_notify: + structural_issues.append("linux_validation_terminal_publish_should_notify_mismatch") + if terminal_ready_for_handoff != expected_ready_for_handoff: + structural_issues.append("linux_validation_terminal_publish_ready_for_handoff_mismatch") + if terminal_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_publish_requires_manual_action_mismatch" + ) + if terminal_channel != expected_channel: + structural_issues.append("linux_validation_terminal_publish_channel_mismatch") + if expected_exit_code == 0 and terminal_exit_code != 0: + structural_issues.append("linux_validation_terminal_publish_exit_code_mismatch_published") + if expected_exit_code is None and terminal_exit_code == 0 and terminal_status != "terminal_published": + structural_issues.append( + "linux_validation_terminal_publish_exit_code_mismatch_non_published" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + final_status: str + final_decision: str + final_exit_code: int + final_should_accept: bool + final_requires_follow_up: bool + final_should_page_owner: bool + final_channel: str + + if structural_issues or missing_artifacts: + final_status = "contract_failed" + final_decision = "abort_linux_validation_terminal_verdict" + final_exit_code = max(1, terminal_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.extend(structural_issues) + elif terminal_status == "terminal_published": + final_status = "validated" + final_decision = "accept_linux_validation_terminal" + final_exit_code = 0 + final_should_accept = True + final_requires_follow_up = False + final_should_page_owner = False + final_channel = "release" + reason_codes = ["linux_validation_final_verdict_validated"] + elif terminal_status == "terminal_published_with_follow_up": + final_status = "validated_with_follow_up" + final_decision = "accept_linux_validation_terminal_with_follow_up" + final_exit_code = max(1, terminal_exit_code) + final_should_accept = True + final_requires_follow_up = True + final_should_page_owner = False + final_channel = "follow_up" + reason_codes.append("linux_validation_final_verdict_validated_with_follow_up") + elif terminal_status == "terminal_blocked": + final_status = "blocked" + final_decision = "escalate_linux_validation_terminal_blocker" + final_exit_code = max(1, terminal_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.append("linux_validation_final_verdict_blocked") + else: + final_status = "contract_failed" + final_decision = "abort_linux_validation_terminal_verdict" + final_exit_code = max(1, terminal_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.append("linux_validation_final_verdict_upstream_contract_failed") + + if final_status not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported linux_validation_final_verdict_status computed") + if final_decision not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported linux_validation_final_verdict_decision computed") + if final_channel not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_CHANNELS: + raise ValueError("internal: unsupported linux_validation_final_verdict_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_publish_status={terminal_status} " + f"linux_validation_final_verdict_status={final_status} " + f"linux_validation_final_verdict_decision={final_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_publish_report": str(source_path), + "source_linux_validation_verdict_publish_report": str( + terminal_publish_report["source_linux_validation_verdict_publish_report"] + ), + "source_linux_validation_verdict_report": str( + terminal_publish_report["source_linux_validation_verdict_report"] + ), + "source_linux_validation_dispatch_report": str( + terminal_publish_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(terminal_publish_report["project_root"]), + "source_terminal_verdict_report": str(terminal_publish_report["source_terminal_verdict_report"]), + "source_release_final_publish_archive_report": str( + terminal_publish_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + terminal_publish_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + terminal_publish_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str(terminal_publish_report["gate_manifest_drift_status"]), + "gate_manifest_drift_missing_runtime_tests": list( + terminal_publish_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + terminal_publish_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + terminal_publish_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(terminal_publish_report["terminal_verdict_status"]), + "terminal_verdict_decision": str(terminal_publish_report["terminal_verdict_decision"]), + "terminal_verdict_exit_code": int(terminal_publish_report["terminal_verdict_exit_code"]), + "terminal_verdict_should_proceed": bool( + terminal_publish_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + terminal_publish_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(terminal_publish_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + terminal_publish_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + terminal_publish_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(terminal_publish_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + terminal_publish_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + terminal_publish_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + terminal_publish_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + terminal_publish_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": terminal_publish_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_verdict_status": str(terminal_publish_report["linux_validation_verdict_status"]), + "linux_validation_verdict_decision": str( + terminal_publish_report["linux_validation_verdict_decision"] + ), + "linux_validation_verdict_exit_code": int( + terminal_publish_report["linux_validation_verdict_exit_code"] + ), + "linux_validation_passed": bool(terminal_publish_report["linux_validation_passed"]), + "linux_validation_verdict_requires_manual_action": bool( + terminal_publish_report["linux_validation_verdict_requires_manual_action"] + ), + "linux_validation_verdict_channel": str( + terminal_publish_report["linux_validation_verdict_channel"] + ), + "linux_validation_verdict_attempted": bool( + terminal_publish_report["linux_validation_verdict_attempted"] + ), + "linux_validation_verdict_timeout_seconds": int( + terminal_publish_report["linux_validation_verdict_timeout_seconds"] + ), + "linux_validation_verdict_command": str( + terminal_publish_report["linux_validation_verdict_command"] + ), + "linux_validation_verdict_command_parts": list( + terminal_publish_report["linux_validation_verdict_command_parts"] + ), + "dry_run": bool(terminal_publish_report["dry_run"]), + "verdict_command_returncode": terminal_publish_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str(terminal_publish_report["verdict_command_stdout_tail"]), + "verdict_command_stderr_tail": str(terminal_publish_report["verdict_command_stderr_tail"]), + "verdict_error_type": str(terminal_publish_report["verdict_error_type"]), + "verdict_error_message": str(terminal_publish_report["verdict_error_message"]), + "release_run_id": terminal_publish_report["release_run_id"], + "release_run_url": str(terminal_publish_report["release_run_url"]), + "follow_up_queue_url": str(terminal_publish_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + terminal_publish_report["linux_validation_dispatch_summary"] + ), + "linux_validation_verdict_summary": str(terminal_publish_report["linux_validation_verdict_summary"]), + "linux_validation_verdict_publish_status": str( + terminal_publish_report["linux_validation_verdict_publish_status"] + ), + "linux_validation_verdict_publish_decision": str( + terminal_publish_report["linux_validation_verdict_publish_decision"] + ), + "linux_validation_verdict_publish_exit_code": int( + terminal_publish_report["linux_validation_verdict_publish_exit_code"] + ), + "linux_validation_verdict_publish_should_notify": bool( + terminal_publish_report["linux_validation_verdict_publish_should_notify"] + ), + "linux_validation_verdict_publish_requires_manual_action": bool( + terminal_publish_report["linux_validation_verdict_publish_requires_manual_action"] + ), + "linux_validation_verdict_publish_channel": str( + terminal_publish_report["linux_validation_verdict_publish_channel"] + ), + "linux_validation_verdict_publish_summary": str( + terminal_publish_report["linux_validation_verdict_publish_summary"] + ), + "linux_validation_terminal_publish_status": terminal_status, + "linux_validation_terminal_publish_decision": terminal_decision, + "linux_validation_terminal_publish_exit_code": terminal_exit_code, + "linux_validation_terminal_publish_should_notify": terminal_should_notify, + "linux_validation_terminal_publish_ready_for_handoff": terminal_ready_for_handoff, + "linux_validation_terminal_publish_requires_manual_action": terminal_requires_manual_action, + "linux_validation_terminal_publish_channel": terminal_channel, + "linux_validation_terminal_publish_summary": str( + terminal_publish_report["linux_validation_terminal_publish_summary"] + ), + "linux_validation_final_verdict_status": final_status, + "linux_validation_final_verdict_decision": final_decision, + "linux_validation_final_verdict_exit_code": final_exit_code, + "linux_validation_final_should_accept": final_should_accept, + "linux_validation_final_requires_follow_up": final_requires_follow_up, + "linux_validation_final_should_page_owner": final_should_page_owner, + "linux_validation_final_channel": final_channel, + "linux_validation_final_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Final Verdict Report", + "", + ( + "- Linux Validation Final Verdict Status: " + f"**{str(payload['linux_validation_final_verdict_status']).upper()}**" + ), + ( + "- Linux Validation Final Verdict Decision: " + f"`{payload['linux_validation_final_verdict_decision']}`" + ), + ( + "- Linux Validation Final Verdict Exit Code: " + f"`{payload['linux_validation_final_verdict_exit_code']}`" + ), + f"- Linux Validation Final Should Accept: `{payload['linux_validation_final_should_accept']}`", + ( + "- Linux Validation Final Requires Follow Up: " + f"`{payload['linux_validation_final_requires_follow_up']}`" + ), + ( + "- Linux Validation Final Should Page Owner: " + f"`{payload['linux_validation_final_should_page_owner']}`" + ), + f"- Linux Validation Final Channel: `{payload['linux_validation_final_channel']}`", + ( + "- Linux Validation Terminal Publish Status: " + f"`{payload['linux_validation_terminal_publish_status']}`" + ), + ( + "- Linux Validation Terminal Publish Channel: " + f"`{payload['linux_validation_terminal_publish_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_final_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_final_verdict_status": str( + payload["linux_validation_final_verdict_status"] + ), + "workflow_linux_validation_final_verdict_decision": str( + payload["linux_validation_final_verdict_decision"] + ), + "workflow_linux_validation_final_verdict_exit_code": str( + payload["linux_validation_final_verdict_exit_code"] + ), + "workflow_linux_validation_final_should_accept": ( + "true" if payload["linux_validation_final_should_accept"] else "false" + ), + "workflow_linux_validation_final_requires_follow_up": ( + "true" if payload["linux_validation_final_requires_follow_up"] else "false" + ), + "workflow_linux_validation_final_should_page_owner": ( + "true" if payload["linux_validation_final_should_page_owner"] else "false" + ), + "workflow_linux_validation_final_channel": str(payload["linux_validation_final_channel"]), + "workflow_linux_validation_final_terminal_publish_status": str( + payload["linux_validation_terminal_publish_status"] + ), + "workflow_linux_validation_final_terminal_publish_channel": str( + payload["linux_validation_terminal_publish_channel"] + ), + "workflow_linux_validation_final_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_final_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_final_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_final_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_final_report_json": str(output_json), + "workflow_linux_validation_final_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation final verdict contract " + "from P2-63 Linux validation terminal publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json", + help="P2-63 Linux validation terminal publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json", + help="Output Linux validation final verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md", + help="Output Linux validation final verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation final verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_publish_report_path = Path(args.linux_validation_terminal_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_publish_report = load_linux_validation_terminal_publish_report( + terminal_publish_report_path + ) + payload = build_linux_validation_final_verdict_payload( + terminal_publish_report, + source_path=terminal_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-final-verdict-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation final verdict json: {output_json_path}") + print(f"linux validation final verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation final verdict summary: " + f"linux_validation_final_verdict_status={payload['linux_validation_final_verdict_status']} " + f"linux_validation_final_verdict_decision={payload['linux_validation_final_verdict_decision']} " + f"linux_validation_final_verdict_exit_code={payload['linux_validation_final_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_final_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_final_verdict_publish_gate.py b/scripts/run_p2_lv_final_verdict_publish_gate.py new file mode 100644 index 0000000..b8285f9 --- /dev/null +++ b/scripts/run_p2_lv_final_verdict_publish_gate.py @@ -0,0 +1,914 @@ +"""Phase 2 card P2-65 gate for Linux CI workflow Linux-validation final verdict publish. + +This script consumes the P2-64 Linux-validation final verdict artifact and converges +one terminal publish contract for Linux validation: +1) validate Linux validation final verdict + evidence consistency, +2) normalize publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation_terminal", + "accept_linux_validation_terminal_with_follow_up", + "escalate_linux_validation_terminal_blocker", + "abort_linux_validation_terminal_verdict", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_final_validated", + "announce_linux_validation_final_validated_with_follow_up", + "announce_linux_validation_final_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_final_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation final verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation final verdict payload must be object") + + required_fields = ( + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_final_verdict_status", + "linux_validation_final_verdict_decision", + "linux_validation_final_verdict_exit_code", + "linux_validation_final_should_accept", + "linux_validation_final_requires_follow_up", + "linux_validation_final_channel", + "linux_validation_final_verdict_attempted", + "linux_validation_final_verdict_timeout_seconds", + "linux_validation_final_verdict_command", + "linux_validation_final_verdict_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_final_verdict_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_final_verdict_status"], + field="linux_validation_final_verdict_status", + path=path, + ) + if verdict_status not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_final_verdict_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_final_verdict_decision"], + field="linux_validation_final_verdict_decision", + path=path, + ) + if verdict_decision not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_final_verdict_exit_code"], + field="linux_validation_final_verdict_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_final_verdict_exit_code' must be >= 0") + + verdict_channel = _coerce_str( + payload["linux_validation_final_channel"], + field="linux_validation_final_channel", + path=path, + ) + if verdict_channel not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_final_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_final_verdict_status": verdict_status, + "linux_validation_final_verdict_decision": verdict_decision, + "linux_validation_final_verdict_exit_code": verdict_exit_code, + "linux_validation_final_should_accept": _coerce_bool( + payload["linux_validation_final_should_accept"], + field="linux_validation_final_should_accept", + path=path, + ), + "linux_validation_final_requires_follow_up": _coerce_bool( + payload["linux_validation_final_requires_follow_up"], + field="linux_validation_final_requires_follow_up", + path=path, + ), + "linux_validation_final_channel": verdict_channel, + "linux_validation_final_verdict_attempted": _coerce_bool( + payload["linux_validation_final_verdict_attempted"], + field="linux_validation_final_verdict_attempted", + path=path, + ), + "linux_validation_final_verdict_timeout_seconds": _coerce_int( + payload["linux_validation_final_verdict_timeout_seconds"], + field="linux_validation_final_verdict_timeout_seconds", + path=path, + ), + "linux_validation_final_verdict_command": _coerce_str( + payload["linux_validation_final_verdict_command"], + field="linux_validation_final_verdict_command", + path=path, + ), + "linux_validation_final_verdict_command_parts": _coerce_str_list( + payload["linux_validation_final_verdict_command_parts"], + field="linux_validation_final_verdict_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_final_verdict_summary": _coerce_str( + payload["linux_validation_final_verdict_summary"], + field="linux_validation_final_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_final_verdict_publish_payload( + linux_validation_final_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str(linux_validation_final_verdict_report["linux_validation_final_verdict_status"]) + verdict_decision = str(linux_validation_final_verdict_report["linux_validation_final_verdict_decision"]) + verdict_exit_code = int(linux_validation_final_verdict_report["linux_validation_final_verdict_exit_code"]) + verdict_should_accept = bool( + linux_validation_final_verdict_report["linux_validation_final_should_accept"] + ) + verdict_requires_follow_up = bool( + linux_validation_final_verdict_report["linux_validation_final_requires_follow_up"] + ) + verdict_channel = str(linux_validation_final_verdict_report["linux_validation_final_channel"]) + + reason_codes = list(linux_validation_final_verdict_report["reason_codes"]) + structural_issues = list(linux_validation_final_verdict_report["structural_issues"]) + missing_artifacts = list(linux_validation_final_verdict_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_final_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_final_verdict_publish_evidence_missing") + + expected_verdict = { + "validated": ( + "accept_linux_validation_terminal", + 0, + True, + False, + "release", + ), + "validated_with_follow_up": ( + "accept_linux_validation_terminal_with_follow_up", + None, + True, + True, + "follow_up", + ), + "blocked": ( + "escalate_linux_validation_terminal_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_verdict", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_accept, + expected_requires_follow_up, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append("linux_validation_final_verdict_decision_mismatch") + if verdict_should_accept != expected_should_accept: + structural_issues.append("linux_validation_final_should_accept_mismatch") + if verdict_requires_follow_up != expected_requires_follow_up: + structural_issues.append("linux_validation_final_requires_follow_up_mismatch") + if verdict_channel != expected_channel: + structural_issues.append("linux_validation_final_channel_mismatch") + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append("linux_validation_final_verdict_exit_code_mismatch_validated") + if expected_exit_code is None and verdict_exit_code == 0 and verdict_status != "validated": + structural_issues.append("linux_validation_final_verdict_exit_code_mismatch_non_validated") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "validated": + publish_status = "published" + publish_decision = "announce_linux_validation_final_validated" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["linux_validation_final_verdict_publish_published"] + elif verdict_status == "validated_with_follow_up": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_final_validated_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("linux_validation_final_verdict_publish_with_follow_up") + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_final_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_final_verdict_publish_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_final_verdict_publish_upstream_contract_failed") + + if publish_status not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError("internal: unsupported linux_validation_final_verdict_publish_status computed") + if publish_decision not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_final_verdict_publish_decision computed") + if publish_channel not in ALLOWED_LINUX_VALIDATION_FINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_final_verdict_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_final_verdict_status={verdict_status} " + f"linux_validation_final_verdict_publish_status={publish_status} " + f"linux_validation_final_verdict_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_final_verdict_report": str(source_path), + "source_linux_validation_dispatch_report": str( + linux_validation_final_verdict_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(linux_validation_final_verdict_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_final_verdict_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_final_verdict_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_final_verdict_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_final_verdict_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_final_verdict_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_final_verdict_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_final_verdict_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_final_verdict_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(linux_validation_final_verdict_report["terminal_verdict_status"]), + "terminal_verdict_decision": str( + linux_validation_final_verdict_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_final_verdict_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_final_verdict_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_final_verdict_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(linux_validation_final_verdict_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + linux_validation_final_verdict_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_final_verdict_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(linux_validation_final_verdict_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + linux_validation_final_verdict_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_final_verdict_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_final_verdict_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_final_verdict_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_final_verdict_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_final_verdict_status": verdict_status, + "linux_validation_final_verdict_decision": verdict_decision, + "linux_validation_final_verdict_exit_code": verdict_exit_code, + "linux_validation_final_should_accept": verdict_should_accept, + "linux_validation_final_requires_follow_up": verdict_requires_follow_up, + "linux_validation_final_channel": verdict_channel, + "linux_validation_final_verdict_attempted": bool( + linux_validation_final_verdict_report["linux_validation_final_verdict_attempted"] + ), + "linux_validation_final_verdict_timeout_seconds": int( + linux_validation_final_verdict_report["linux_validation_final_verdict_timeout_seconds"] + ), + "linux_validation_final_verdict_command": str( + linux_validation_final_verdict_report["linux_validation_final_verdict_command"] + ), + "linux_validation_final_verdict_command_parts": list( + linux_validation_final_verdict_report["linux_validation_final_verdict_command_parts"] + ), + "dry_run": bool(linux_validation_final_verdict_report["dry_run"]), + "verdict_command_returncode": linux_validation_final_verdict_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str( + linux_validation_final_verdict_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_final_verdict_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str(linux_validation_final_verdict_report["verdict_error_type"]), + "verdict_error_message": str(linux_validation_final_verdict_report["verdict_error_message"]), + "release_run_id": linux_validation_final_verdict_report["release_run_id"], + "release_run_url": str(linux_validation_final_verdict_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_final_verdict_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_final_verdict_report["linux_validation_dispatch_summary"] + ), + "linux_validation_final_verdict_summary": str( + linux_validation_final_verdict_report["linux_validation_final_verdict_summary"] + ), + "linux_validation_final_verdict_publish_status": publish_status, + "linux_validation_final_verdict_publish_decision": publish_decision, + "linux_validation_final_verdict_publish_exit_code": publish_exit_code, + "linux_validation_final_verdict_publish_should_notify": publish_should_notify, + "linux_validation_final_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_final_verdict_publish_channel": publish_channel, + "linux_validation_final_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Final Verdict Publish Report", + "", + ( + "- Linux Validation Final Verdict Publish Status: " + f"**{str(payload['linux_validation_final_verdict_publish_status']).upper()}**" + ), + ( + "- Linux Validation Final Verdict Publish Decision: " + f"`{payload['linux_validation_final_verdict_publish_decision']}`" + ), + ( + "- Linux Validation Final Verdict Publish Exit Code: " + f"`{payload['linux_validation_final_verdict_publish_exit_code']}`" + ), + ( + "- Linux Validation Final Verdict Publish Should Notify: " + f"`{payload['linux_validation_final_verdict_publish_should_notify']}`" + ), + ( + "- Linux Validation Final Verdict Publish Requires Manual Action: " + f"`{payload['linux_validation_final_verdict_publish_requires_manual_action']}`" + ), + ( + "- Linux Validation Final Verdict Publish Channel: " + f"`{payload['linux_validation_final_verdict_publish_channel']}`" + ), + f"- Linux Validation Final Verdict Status: `{payload['linux_validation_final_verdict_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_final_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_final_verdict_publish_status": str( + payload["linux_validation_final_verdict_publish_status"] + ), + "workflow_linux_validation_final_verdict_publish_decision": str( + payload["linux_validation_final_verdict_publish_decision"] + ), + "workflow_linux_validation_final_verdict_publish_exit_code": str( + payload["linux_validation_final_verdict_publish_exit_code"] + ), + "workflow_linux_validation_final_verdict_publish_should_notify": ( + "true" if payload["linux_validation_final_verdict_publish_should_notify"] else "false" + ), + "workflow_linux_validation_final_verdict_publish_requires_manual_action": ( + "true" + if payload["linux_validation_final_verdict_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_final_verdict_publish_channel": str( + payload["linux_validation_final_verdict_publish_channel"] + ), + "workflow_linux_validation_final_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_final_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_final_verdict_publish_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_final_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_final_verdict_publish_report_json": str(output_json), + "workflow_linux_validation_final_verdict_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation final verdict publish contract " + "from P2-64 Linux validation final verdict report" + ) + ) + parser.add_argument( + "--linux-validation-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json", + help="P2-64 Linux validation final verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json", + help="Output Linux validation final verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md", + help="Output Linux validation final verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation final verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path(args.linux_validation_final_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_final_verdict_report(verdict_report_path) + payload = build_linux_validation_final_verdict_publish_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-final-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation final verdict publish json: {output_json_path}") + print(f"linux validation final verdict publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation final verdict publish summary: " + f"linux_validation_final_verdict_publish_status=" + f"{payload['linux_validation_final_verdict_publish_status']} " + f"linux_validation_final_verdict_publish_decision=" + f"{payload['linux_validation_final_verdict_publish_decision']} " + f"linux_validation_final_verdict_publish_exit_code=" + f"{payload['linux_validation_final_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_final_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + diff --git a/scripts/run_p2_lv_td_completion_gate.py b/scripts/run_p2_lv_td_completion_gate.py new file mode 100644 index 0000000..dcd3ffc --- /dev/null +++ b/scripts/run_p2_lv_td_completion_gate.py @@ -0,0 +1,877 @@ +"""Phase 2 card P2-72 gate for Linux CI workflow Linux validation terminal dispatch completion await. + +This script consumes the P2-71 Linux validation terminal dispatch trace artifact and converges to +one completion contract: +1) validate dispatch trace contract and poll command, +2) optionally poll workflow run status until terminal/timeout, +3) emit completion JSON/Markdown + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TRACE_STATUSES: set[str] = { + "blocked", + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatch_failed", + "contract_failed", + "run_tracking_missing", + "run_tracking_ready", + "run_in_progress", + "run_completed_success", + "run_completed_failure", + "run_poll_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_trace_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal dispatch trace report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch trace payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_execution_status", + "linux_validation_terminal_dispatch_execution_decision", + "linux_validation_terminal_dispatch_execution_exit_code", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_attempted", + "linux_validation_terminal_dispatch_trace_status", + "linux_validation_terminal_dispatch_trace_exit_code", + "linux_validation_terminal_should_poll_workflow_run", + "linux_validation_terminal_run_id", + "linux_validation_terminal_run_url", + "linux_validation_terminal_repo_owner", + "linux_validation_terminal_repo_name", + "linux_validation_terminal_poll_command", + "linux_validation_terminal_poll_command_parts", + "linux_validation_terminal_poll_attempted", + "linux_validation_terminal_poll_returncode", + "linux_validation_terminal_poll_status", + "linux_validation_terminal_poll_conclusion", + "linux_validation_terminal_poll_url", + "linux_validation_terminal_poll_error_type", + "linux_validation_terminal_poll_error_message", + "linux_validation_terminal_poll_stdout_tail", + "linux_validation_terminal_poll_stderr_tail", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + trace_status = _coerce_str( + payload["linux_validation_terminal_dispatch_trace_status"], + field="linux_validation_terminal_dispatch_trace_status", + path=path, + ) + if trace_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TRACE_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_trace_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TRACE_STATUSES)}" + ) + + trace_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_trace_exit_code"], + field="linux_validation_terminal_dispatch_trace_exit_code", + path=path, + ) + if trace_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_trace_exit_code' must be >= 0" + ) + + should_poll_workflow_run = _coerce_bool( + payload["linux_validation_terminal_should_poll_workflow_run"], + field="linux_validation_terminal_should_poll_workflow_run", + path=path, + ) + + run_id_raw = payload["linux_validation_terminal_run_id"] + if run_id_raw is None: + run_id = None + else: + run_id = _coerce_int(run_id_raw, field="linux_validation_terminal_run_id", path=path) + if run_id < 1: + raise ValueError( + f"{path}: field 'linux_validation_terminal_run_id' must be >= 1 when present" + ) + + run_url = _coerce_str( + payload["linux_validation_terminal_run_url"], + field="linux_validation_terminal_run_url", + path=path, + ) + repo_owner = _coerce_str( + payload["linux_validation_terminal_repo_owner"], + field="linux_validation_terminal_repo_owner", + path=path, + ) + repo_name = _coerce_str( + payload["linux_validation_terminal_repo_name"], + field="linux_validation_terminal_repo_name", + path=path, + ) + poll_command = _coerce_str( + payload["linux_validation_terminal_poll_command"], + field="linux_validation_terminal_poll_command", + path=path, + ) + poll_command_parts = _coerce_str_list( + payload["linux_validation_terminal_poll_command_parts"], + field="linux_validation_terminal_poll_command_parts", + path=path, + ) + poll_attempted = _coerce_bool( + payload["linux_validation_terminal_poll_attempted"], + field="linux_validation_terminal_poll_attempted", + path=path, + ) + poll_returncode_raw = payload["linux_validation_terminal_poll_returncode"] + if poll_returncode_raw is None: + poll_returncode = None + else: + poll_returncode = _coerce_int( + poll_returncode_raw, + field="linux_validation_terminal_poll_returncode", + path=path, + ) + poll_status = _coerce_str( + payload["linux_validation_terminal_poll_status"], + field="linux_validation_terminal_poll_status", + path=path, + ) + poll_conclusion = _coerce_str( + payload["linux_validation_terminal_poll_conclusion"], + field="linux_validation_terminal_poll_conclusion", + path=path, + ) + poll_url = _coerce_str( + payload["linux_validation_terminal_poll_url"], + field="linux_validation_terminal_poll_url", + path=path, + ) + poll_error_type = _coerce_str( + payload["linux_validation_terminal_poll_error_type"], + field="linux_validation_terminal_poll_error_type", + path=path, + ) + poll_error_message = _coerce_str( + payload["linux_validation_terminal_poll_error_message"], + field="linux_validation_terminal_poll_error_message", + path=path, + ) + poll_stdout_tail = _coerce_str( + payload["linux_validation_terminal_poll_stdout_tail"], + field="linux_validation_terminal_poll_stdout_tail", + path=path, + ) + poll_stderr_tail = _coerce_str( + payload["linux_validation_terminal_poll_stderr_tail"], + field="linux_validation_terminal_poll_stderr_tail", + path=path, + ) + reason_codes = _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path) + structural_issues = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + missing_artifacts = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + + if should_poll_workflow_run: + if run_id is None: + raise ValueError(f"{path}: should_poll_workflow_run=true requires run_id") + if not poll_command_parts: + raise ValueError(f"{path}: should_poll_workflow_run=true requires poll_command_parts") + if len(poll_command_parts) < 3 or poll_command_parts[1:3] != ["run", "view"]: + raise ValueError(f"{path}: poll_command_parts must include ' run view ...' contract") + if shlex.split(poll_command) != poll_command_parts: + raise ValueError(f"{path}: poll_command and poll_command_parts mismatch") + else: + if run_id is not None: + raise ValueError(f"{path}: should_poll_workflow_run=false requires run_id=null") + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_dispatch_execution_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_execution_report"], + field="source_linux_validation_terminal_dispatch_execution_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_report"], + field="source_linux_validation_terminal_dispatch_report", + path=path, + ), + "linux_validation_terminal_dispatch_execution_status": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_status"], + field="linux_validation_terminal_dispatch_execution_status", + path=path, + ), + "linux_validation_terminal_dispatch_execution_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_decision"], + field="linux_validation_terminal_dispatch_execution_decision", + path=path, + ), + "linux_validation_terminal_dispatch_execution_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_execution_exit_code"], + field="linux_validation_terminal_dispatch_execution_exit_code", + path=path, + ), + "linux_validation_terminal_should_dispatch": _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ), + "linux_validation_terminal_dispatch_attempted": _coerce_bool( + payload["linux_validation_terminal_dispatch_attempted"], + field="linux_validation_terminal_dispatch_attempted", + path=path, + ), + "linux_validation_terminal_dispatch_trace_status": trace_status, + "linux_validation_terminal_dispatch_trace_exit_code": trace_exit_code, + "linux_validation_terminal_should_poll_workflow_run": should_poll_workflow_run, + "linux_validation_terminal_run_id": run_id, + "linux_validation_terminal_run_url": run_url, + "linux_validation_terminal_repo_owner": repo_owner, + "linux_validation_terminal_repo_name": repo_name, + "linux_validation_terminal_poll_command": poll_command, + "linux_validation_terminal_poll_command_parts": poll_command_parts, + "linux_validation_terminal_poll_attempted": poll_attempted, + "linux_validation_terminal_poll_returncode": poll_returncode, + "linux_validation_terminal_poll_status": poll_status, + "linux_validation_terminal_poll_conclusion": poll_conclusion, + "linux_validation_terminal_poll_url": poll_url, + "linux_validation_terminal_poll_error_type": poll_error_type, + "linux_validation_terminal_poll_error_message": poll_error_message, + "linux_validation_terminal_poll_stdout_tail": poll_stdout_tail, + "linux_validation_terminal_poll_stderr_tail": poll_stderr_tail, + "release_run_id": _coerce_int(payload["release_run_id"], field="release_run_id", path=path) + if payload["release_run_id"] is not None + else None, + "release_run_url": _coerce_str( + payload["release_run_url"], field="release_run_url", path=path + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], field="follow_up_queue_url", path=path + ), + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def execute_run_poll_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"workflow run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def build_linux_validation_terminal_dispatch_completion_payload( + trace_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + poll_interval_seconds: int, + max_polls: int, + poll_timeout_seconds: int, + dry_run: bool, + allow_in_progress: bool, + poll_executor: Callable[..., dict[str, Any]] = execute_run_poll_command, + sleep_fn: Callable[[float], None] = time.sleep, +) -> dict[str, Any]: + trace_status = str(trace_report["linux_validation_terminal_dispatch_trace_status"]) + should_poll_workflow_run = bool( + trace_report["linux_validation_terminal_should_poll_workflow_run"] + ) + + completion_status = trace_status + completion_exit_code = int( + trace_report["linux_validation_terminal_dispatch_trace_exit_code"] + ) + reason_codes = list(trace_report["reason_codes"]) + + poll_iterations = 0 + poll_attempted = False + poll_returncode = trace_report["linux_validation_terminal_poll_returncode"] + poll_status = str(trace_report["linux_validation_terminal_poll_status"]) + poll_conclusion = str(trace_report["linux_validation_terminal_poll_conclusion"]) + poll_url = str(trace_report["linux_validation_terminal_poll_url"]) + poll_error_type = str(trace_report["linux_validation_terminal_poll_error_type"]) + poll_error_message = str(trace_report["linux_validation_terminal_poll_error_message"]) + poll_stdout_tail = str(trace_report["linux_validation_terminal_poll_stdout_tail"]) + poll_stderr_tail = str(trace_report["linux_validation_terminal_poll_stderr_tail"]) + + should_await = should_poll_workflow_run and trace_status in { + "run_tracking_ready", + "run_in_progress", + } + + if should_await and not dry_run: + for attempt in range(1, max_polls + 1): + poll_iterations = attempt + poll_attempted = True + poll_result = poll_executor( + list(trace_report["linux_validation_terminal_poll_command_parts"]), + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + poll_returncode = int(poll_result["returncode"]) + poll_stdout_tail = str(poll_result["stdout_tail"]) + poll_stderr_tail = str(poll_result["stderr_tail"]) + poll_error_type = str(poll_result["error_type"]) + poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if poll_returncode != 0: + completion_status = "run_poll_failed" + completion_exit_code = 1 + if poll_error_type == "command_not_found": + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_cli_unavailable" + ) + elif poll_error_type == "timeout": + reason_codes.append("linux_validation_terminal_dispatch_run_poll_timeout") + else: + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_command_failed" + ) + break + + if poll_payload is None: + completion_status = "run_poll_failed" + completion_exit_code = 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_payload_invalid" + ) + break + + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + poll_status = poll_status_value + else: + poll_status = "" + if isinstance(poll_conclusion_value, str): + poll_conclusion = poll_conclusion_value + else: + poll_conclusion = "" + if isinstance(poll_url_value, str) and poll_url_value: + poll_url = poll_url_value + + if poll_status == "completed": + if poll_conclusion == "success": + completion_status = "run_completed_success" + completion_exit_code = 0 + reason_codes = [ + "linux_validation_terminal_dispatch_run_completed_success" + ] + else: + completion_status = "run_completed_failure" + completion_exit_code = 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_completed_with_failure" + ) + break + + completion_status = "run_in_progress" + completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_still_in_progress" + ) + if attempt < max_polls: + sleep_fn(float(poll_interval_seconds)) + else: + completion_status = "run_await_timeout" + completion_exit_code = 0 if allow_in_progress else 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_await_timeout" + ) + elif should_await and dry_run: + completion_status = trace_status + completion_exit_code = int( + trace_report["linux_validation_terminal_dispatch_trace_exit_code"] + ) + reason_codes.append( + "linux_validation_terminal_dispatch_completion_poll_skipped_dry_run" + ) + else: + if trace_status == "run_completed_success": + completion_exit_code = 0 + elif trace_status in { + "run_tracking_missing", + "run_completed_failure", + "run_poll_failed", + }: + completion_exit_code = 1 + elif trace_status in {"blocked", "ready_dry_run", "ready_with_follow_up_dry_run"}: + completion_exit_code = int( + trace_report["linux_validation_terminal_dispatch_trace_exit_code"] + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_trace_report": str(source_path), + "project_root": str(project_root), + "source_linux_validation_terminal_dispatch_execution_report": str( + trace_report["source_linux_validation_terminal_dispatch_execution_report"] + ), + "source_linux_validation_terminal_dispatch_report": str( + trace_report["source_linux_validation_terminal_dispatch_report"] + ), + "linux_validation_terminal_dispatch_execution_status": str( + trace_report["linux_validation_terminal_dispatch_execution_status"] + ), + "linux_validation_terminal_dispatch_execution_decision": str( + trace_report["linux_validation_terminal_dispatch_execution_decision"] + ), + "linux_validation_terminal_dispatch_execution_exit_code": int( + trace_report["linux_validation_terminal_dispatch_execution_exit_code"] + ), + "linux_validation_terminal_should_dispatch": bool( + trace_report["linux_validation_terminal_should_dispatch"] + ), + "linux_validation_terminal_dispatch_attempted": bool( + trace_report["linux_validation_terminal_dispatch_attempted"] + ), + "linux_validation_terminal_dispatch_trace_status": trace_status, + "linux_validation_terminal_dispatch_trace_exit_code": int( + trace_report["linux_validation_terminal_dispatch_trace_exit_code"] + ), + "linux_validation_terminal_dispatch_completion_status": completion_status, + "linux_validation_terminal_dispatch_completion_exit_code": int(completion_exit_code), + "dry_run": dry_run, + "allow_in_progress": allow_in_progress, + "should_poll_workflow_run": should_poll_workflow_run, + "linux_validation_terminal_run_id": trace_report["linux_validation_terminal_run_id"], + "linux_validation_terminal_run_url": str( + trace_report["linux_validation_terminal_run_url"] + ), + "linux_validation_terminal_repo_owner": str( + trace_report["linux_validation_terminal_repo_owner"] + ), + "linux_validation_terminal_repo_name": str( + trace_report["linux_validation_terminal_repo_name"] + ), + "linux_validation_terminal_poll_command": str( + trace_report["linux_validation_terminal_poll_command"] + ), + "linux_validation_terminal_poll_command_parts": list( + trace_report["linux_validation_terminal_poll_command_parts"] + ), + "poll_interval_seconds": poll_interval_seconds, + "max_polls": max_polls, + "poll_timeout_seconds": poll_timeout_seconds, + "poll_iterations": poll_iterations, + "poll_attempted": poll_attempted, + "poll_returncode": poll_returncode, + "poll_status": poll_status, + "poll_conclusion": poll_conclusion, + "poll_url": poll_url, + "poll_error_type": poll_error_type, + "poll_error_message": poll_error_message, + "poll_stdout_tail": poll_stdout_tail, + "poll_stderr_tail": poll_stderr_tail, + "reason_codes": _unique(reason_codes), + "structural_issues": list(trace_report["structural_issues"]), + "missing_artifacts": list(trace_report["missing_artifacts"]), + "release_run_id": trace_report["release_run_id"], + "release_run_url": str(trace_report["release_run_url"]), + "follow_up_queue_url": str(trace_report["follow_up_queue_url"]), + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Completion Report", + "", + ( + "- Completion Status: **" + f"{str(payload['linux_validation_terminal_dispatch_completion_status']).upper()}**" + ), + ( + "- Completion Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_completion_exit_code']}`" + ), + f"- Should Poll Workflow Run: `{payload['should_poll_workflow_run']}`", + f"- Poll Attempted: `{payload['poll_attempted']}`", + f"- Poll Iterations: `{payload['poll_iterations']}`", + f"- Run ID: `{payload['linux_validation_terminal_run_id']}`", + f"- Run URL: `{payload['linux_validation_terminal_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Paths", + "", + f"- Source Dispatch Trace Report: `{payload['source_linux_validation_terminal_dispatch_trace_report']}`", + f"- Source Dispatch Execution Report: `{payload['source_linux_validation_terminal_dispatch_execution_report']}`", + f"- Source Dispatch Report: `{payload['source_linux_validation_terminal_dispatch_report']}`", + "", + "## Poll Result", + "", + f"- Poll Return Code: `{payload['poll_returncode']}`", + f"- Poll Status: `{payload['poll_status']}`", + f"- Poll Conclusion: `{payload['poll_conclusion']}`", + f"- Poll URL: `{payload['poll_url']}`", + f"- Poll Error Type: `{payload['poll_error_type']}`", + ] + + if payload["poll_stdout_tail"]: + lines.append("- Poll stdout:") + lines.append("```text") + lines.append(str(payload["poll_stdout_tail"])) + lines.append("```") + if payload["poll_stderr_tail"]: + lines.append("- Poll stderr:") + lines.append("```text") + lines.append(str(payload["poll_stderr_tail"])) + lines.append("```") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["linux_validation_terminal_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_completion_status": str( + payload["linux_validation_terminal_dispatch_completion_status"] + ), + "workflow_linux_validation_terminal_dispatch_completion_exit_code": str( + payload["linux_validation_terminal_dispatch_completion_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_completion_should_poll": ( + "true" if payload["should_poll_workflow_run"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_completion_poll_attempted": ( + "true" if payload["poll_attempted"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_completion_poll_iterations": str( + payload["poll_iterations"] + ), + "workflow_linux_validation_terminal_dispatch_completion_poll_status": str( + payload["poll_status"] + ), + "workflow_linux_validation_terminal_dispatch_completion_poll_conclusion": str( + payload["poll_conclusion"] + ), + "workflow_linux_validation_terminal_dispatch_completion_run_id": ( + "" if run_id is None else str(run_id) + ), + "workflow_linux_validation_terminal_dispatch_completion_run_url": str( + payload["linux_validation_terminal_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_completion_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_completion_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_completion_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Await Linux validation terminal dispatch completion from P2-71 trace report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-trace-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json", + help="P2-71 Linux validation terminal dispatch trace report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root used for optional poll command execution", + ) + parser.add_argument( + "--poll-interval-seconds", + type=int, + default=30, + help="Sleep interval between polls when awaiting completion", + ) + parser.add_argument( + "--max-polls", + type=int, + default=20, + help="Maximum poll attempts when awaiting completion", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Timeout seconds for each poll command execution", + ) + parser.add_argument( + "--allow-in-progress", + action="store_true", + help="Return exit 0 when run remains in progress after max polls", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json", + help="Output completion JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md", + help="Output completion markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print completion JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_interval_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-completion-gate] --poll-interval-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + if args.max_polls < 1: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-completion-gate] --max-polls must be >= 1", + file=sys.stderr, + ) + return 2 + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-completion-gate] --poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + trace_report_path = Path(args.linux_validation_terminal_dispatch_trace_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + trace_report = load_linux_validation_terminal_dispatch_trace_report(trace_report_path) + payload = build_linux_validation_terminal_dispatch_completion_payload( + trace_report, + source_path=trace_report_path.resolve(), + project_root=project_root, + poll_interval_seconds=args.poll_interval_seconds, + max_polls=args.max_polls, + poll_timeout_seconds=args.poll_timeout_seconds, + dry_run=args.dry_run, + allow_in_progress=args.allow_in_progress, + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-completion-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch completion json: {output_json_path}") + print(f"linux validation terminal dispatch completion markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch completion summary: " + f"linux_validation_terminal_dispatch_completion_status=" + f"{payload['linux_validation_terminal_dispatch_completion_status']} " + f"should_poll={payload['should_poll_workflow_run']} " + f"linux_validation_terminal_dispatch_completion_exit_code=" + f"{payload['linux_validation_terminal_dispatch_completion_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_completion_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_dispatch_gate.py b/scripts/run_p2_lv_td_dispatch_gate.py new file mode 100644 index 0000000..6e37161 --- /dev/null +++ b/scripts/run_p2_lv_td_dispatch_gate.py @@ -0,0 +1,1024 @@ +"""Phase 2 card P2-69 gate for Linux CI workflow Linux-validation terminal dispatch. + +This script consumes the P2-68 Linux-validation terminal verdict publish artifact and converges +one terminal dispatch contract for Linux validation: +1) validate Linux validation terminal verdict publish + evidence consistency, +2) normalize terminal dispatch semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_ready", + "announce_linux_validation_terminal_ready_with_follow_up", + "announce_linux_validation_terminal_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS: set[str] = { + "dispatch_linux_validation_terminal", + "dispatch_linux_validation_terminal_with_follow_up", + "hold_linux_validation_terminal_blocker", + "abort_linux_validation_terminal_dispatch", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_terminal_verdict_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal verdict publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation terminal verdict publish payload must be object") + + required_fields = ( + "source_linux_validation_terminal_verdict_report", + "source_linux_validation_dispatch_report", + "source_linux_validation_final_publish_archive_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_terminal_verdict_status", + "linux_validation_terminal_verdict_decision", + "linux_validation_terminal_verdict_exit_code", + "linux_validation_terminal_verdict_should_proceed", + "linux_validation_terminal_verdict_requires_manual_action", + "linux_validation_terminal_verdict_channel", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_terminal_verdict_summary", + "linux_validation_terminal_verdict_publish_status", + "linux_validation_terminal_verdict_publish_decision", + "linux_validation_terminal_verdict_publish_exit_code", + "linux_validation_terminal_verdict_publish_should_notify", + "linux_validation_terminal_verdict_publish_requires_manual_action", + "linux_validation_terminal_verdict_publish_channel", + "linux_validation_terminal_verdict_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + publish_status = _coerce_str( + payload["linux_validation_terminal_verdict_publish_status"], + field="linux_validation_terminal_verdict_publish_status", + path=path, + ) + if publish_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_STATUSES)}" + ) + + publish_decision = _coerce_str( + payload["linux_validation_terminal_verdict_publish_decision"], + field="linux_validation_terminal_verdict_publish_decision", + path=path, + ) + if publish_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_DECISIONS)}" + ) + + publish_exit_code = _coerce_int( + payload["linux_validation_terminal_verdict_publish_exit_code"], + field="linux_validation_terminal_verdict_publish_exit_code", + path=path, + ) + if publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_publish_exit_code' must be >= 0" + ) + + publish_channel = _coerce_str( + payload["linux_validation_terminal_verdict_publish_channel"], + field="linux_validation_terminal_verdict_publish_channel", + path=path, + ) + if publish_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_verdict_report": _coerce_str( + payload["source_linux_validation_terminal_verdict_report"], + field="source_linux_validation_terminal_verdict_report", + path=path, + ), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "source_linux_validation_final_publish_archive_report": _coerce_str( + payload["source_linux_validation_final_publish_archive_report"], + field="source_linux_validation_final_publish_archive_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_terminal_verdict_status": _coerce_str( + payload["linux_validation_terminal_verdict_status"], + field="linux_validation_terminal_verdict_status", + path=path, + ), + "linux_validation_terminal_verdict_decision": _coerce_str( + payload["linux_validation_terminal_verdict_decision"], + field="linux_validation_terminal_verdict_decision", + path=path, + ), + "linux_validation_terminal_verdict_exit_code": _coerce_int( + payload["linux_validation_terminal_verdict_exit_code"], + field="linux_validation_terminal_verdict_exit_code", + path=path, + ), + "linux_validation_terminal_verdict_should_proceed": _coerce_bool( + payload["linux_validation_terminal_verdict_should_proceed"], + field="linux_validation_terminal_verdict_should_proceed", + path=path, + ), + "linux_validation_terminal_verdict_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_verdict_requires_manual_action"], + field="linux_validation_terminal_verdict_requires_manual_action", + path=path, + ), + "linux_validation_terminal_verdict_channel": _coerce_str( + payload["linux_validation_terminal_verdict_channel"], + field="linux_validation_terminal_verdict_channel", + path=path, + ), + "linux_validation_terminal_verdict_publish_status": publish_status, + "linux_validation_terminal_verdict_publish_decision": publish_decision, + "linux_validation_terminal_verdict_publish_exit_code": publish_exit_code, + "linux_validation_terminal_verdict_publish_should_notify": _coerce_bool( + payload["linux_validation_terminal_verdict_publish_should_notify"], + field="linux_validation_terminal_verdict_publish_should_notify", + path=path, + ), + "linux_validation_terminal_verdict_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_verdict_publish_requires_manual_action"], + field="linux_validation_terminal_verdict_publish_requires_manual_action", + path=path, + ), + "linux_validation_terminal_verdict_publish_channel": publish_channel, + "linux_validation_terminal_verdict_publish_summary": _coerce_str( + payload["linux_validation_terminal_verdict_publish_summary"], + field="linux_validation_terminal_verdict_publish_summary", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_terminal_verdict_summary": _coerce_str( + payload["linux_validation_terminal_verdict_summary"], + field="linux_validation_terminal_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_terminal_dispatch_payload( + linux_validation_terminal_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + publish_status = str( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_status" + ] + ) + publish_decision = str( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_decision" + ] + ) + publish_exit_code = int( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_exit_code" + ] + ) + publish_should_notify = bool( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_should_notify" + ] + ) + publish_requires_manual_action = bool( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_requires_manual_action" + ] + ) + publish_channel = str( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_channel" + ] + ) + + reason_codes = list(linux_validation_terminal_verdict_publish_report["reason_codes"]) + structural_issues = list(linux_validation_terminal_verdict_publish_report["structural_issues"]) + missing_artifacts = list(linux_validation_terminal_verdict_publish_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_terminal_verdict_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_terminal_dispatch_evidence_missing") + + expected_publish = { + "published": ( + "announce_linux_validation_terminal_ready", + 0, + True, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_terminal_ready_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_linux_validation_terminal_blocker", + None, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_channel, + ) = expected_publish[publish_status] + + if publish_decision != expected_decision: + structural_issues.append("linux_validation_terminal_dispatch_upstream_decision_mismatch") + if publish_should_notify != expected_should_notify: + structural_issues.append("linux_validation_terminal_dispatch_upstream_should_notify_mismatch") + if publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_upstream_requires_manual_action_mismatch" + ) + if publish_channel != expected_channel: + structural_issues.append("linux_validation_terminal_dispatch_upstream_channel_mismatch") + if expected_exit_code == 0 and publish_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_upstream_exit_code_mismatch_published" + ) + if expected_exit_code is None and publish_exit_code == 0 and publish_status != "published": + structural_issues.append( + "linux_validation_terminal_dispatch_upstream_exit_code_mismatch_non_published" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + dispatch_status: str + dispatch_decision: str + dispatch_exit_code: int + dispatch_should_dispatch: bool + dispatch_requires_manual_action: bool + dispatch_channel: str + + if structural_issues or missing_artifacts: + dispatch_status = "contract_failed" + dispatch_decision = "abort_linux_validation_terminal_dispatch" + dispatch_exit_code = max(1, publish_exit_code) + dispatch_should_dispatch = False + dispatch_requires_manual_action = True + dispatch_channel = "blocker" + reason_codes.extend(structural_issues) + elif publish_status == "published": + dispatch_status = "ready_dry_run" + dispatch_decision = "dispatch_linux_validation_terminal" + dispatch_exit_code = 0 + dispatch_should_dispatch = True + dispatch_requires_manual_action = False + dispatch_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_ready_dry_run"] + elif publish_status == "published_with_follow_up": + dispatch_status = "ready_with_follow_up_dry_run" + dispatch_decision = "dispatch_linux_validation_terminal_with_follow_up" + dispatch_exit_code = max(1, publish_exit_code) + dispatch_should_dispatch = True + dispatch_requires_manual_action = False + dispatch_channel = "follow_up" + reason_codes.append("linux_validation_terminal_dispatch_ready_with_follow_up_dry_run") + elif publish_status == "blocked": + dispatch_status = "blocked" + dispatch_decision = "hold_linux_validation_terminal_blocker" + dispatch_exit_code = max(1, publish_exit_code) + dispatch_should_dispatch = False + dispatch_requires_manual_action = True + dispatch_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_blocked") + else: + dispatch_status = "contract_failed" + dispatch_decision = "abort_linux_validation_terminal_dispatch" + dispatch_exit_code = max(1, publish_exit_code) + dispatch_should_dispatch = False + dispatch_requires_manual_action = True + dispatch_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_upstream_contract_failed") + + if dispatch_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_STATUSES: + raise ValueError("internal: unsupported linux_validation_terminal_dispatch_status computed") + if dispatch_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_terminal_dispatch_decision computed") + if dispatch_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_terminal_dispatch_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_verdict_publish_status={publish_status} " + f"linux_validation_terminal_dispatch_status={dispatch_status} " + f"linux_validation_terminal_dispatch_decision={dispatch_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_verdict_publish_report": str(source_path), + "source_linux_validation_terminal_verdict_report": str( + linux_validation_terminal_verdict_publish_report[ + "source_linux_validation_terminal_verdict_report" + ] + ), + "source_linux_validation_dispatch_report": str( + linux_validation_terminal_verdict_publish_report["source_linux_validation_dispatch_report"] + ), + "source_linux_validation_final_publish_archive_report": str( + linux_validation_terminal_verdict_publish_report[ + "source_linux_validation_final_publish_archive_report" + ] + ), + "project_root": str(linux_validation_terminal_verdict_publish_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_terminal_verdict_publish_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_terminal_verdict_publish_report[ + "source_release_final_publish_archive_report" + ] + ), + "source_gate_manifest_drift_report": str( + linux_validation_terminal_verdict_publish_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_terminal_verdict_publish_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_terminal_verdict_publish_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_terminal_verdict_publish_report[ + "gate_manifest_drift_missing_runtime_tests" + ] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_terminal_verdict_publish_report[ + "gate_manifest_drift_missing_manifest_entries" + ] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_terminal_verdict_publish_report[ + "gate_manifest_drift_orphan_manifest_entries" + ] + ), + "terminal_verdict_status": str( + linux_validation_terminal_verdict_publish_report["terminal_verdict_status"] + ), + "terminal_verdict_decision": str( + linux_validation_terminal_verdict_publish_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_terminal_verdict_publish_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_terminal_verdict_publish_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_terminal_verdict_publish_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str( + linux_validation_terminal_verdict_publish_report["terminal_verdict_channel"] + ), + "linux_validation_should_dispatch": bool( + linux_validation_terminal_verdict_publish_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_terminal_verdict_publish_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str( + linux_validation_terminal_verdict_publish_report["linux_validation_channel"] + ), + "linux_validation_dispatch_status": str( + linux_validation_terminal_verdict_publish_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_terminal_verdict_publish_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_terminal_verdict_publish_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_terminal_verdict_publish_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_terminal_verdict_publish_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_terminal_verdict_status": str( + linux_validation_terminal_verdict_publish_report["linux_validation_terminal_verdict_status"] + ), + "linux_validation_terminal_verdict_decision": str( + linux_validation_terminal_verdict_publish_report["linux_validation_terminal_verdict_decision"] + ), + "linux_validation_terminal_verdict_exit_code": int( + linux_validation_terminal_verdict_publish_report["linux_validation_terminal_verdict_exit_code"] + ), + "linux_validation_terminal_verdict_should_proceed": bool( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_should_proceed" + ] + ), + "linux_validation_terminal_verdict_requires_manual_action": bool( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_requires_manual_action" + ] + ), + "linux_validation_terminal_verdict_channel": str( + linux_validation_terminal_verdict_publish_report["linux_validation_terminal_verdict_channel"] + ), + "linux_validation_terminal_verdict_publish_status": publish_status, + "linux_validation_terminal_verdict_publish_decision": publish_decision, + "linux_validation_terminal_verdict_publish_exit_code": publish_exit_code, + "linux_validation_terminal_verdict_publish_should_notify": publish_should_notify, + "linux_validation_terminal_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_terminal_verdict_publish_channel": publish_channel, + "linux_validation_terminal_verdict_publish_summary": str( + linux_validation_terminal_verdict_publish_report[ + "linux_validation_terminal_verdict_publish_summary" + ] + ), + "dry_run": bool(linux_validation_terminal_verdict_publish_report["dry_run"]), + "verdict_command_returncode": linux_validation_terminal_verdict_publish_report[ + "verdict_command_returncode" + ], + "verdict_command_stdout_tail": str( + linux_validation_terminal_verdict_publish_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_terminal_verdict_publish_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str( + linux_validation_terminal_verdict_publish_report["verdict_error_type"] + ), + "verdict_error_message": str( + linux_validation_terminal_verdict_publish_report["verdict_error_message"] + ), + "release_run_id": linux_validation_terminal_verdict_publish_report["release_run_id"], + "release_run_url": str(linux_validation_terminal_verdict_publish_report["release_run_url"]), + "follow_up_queue_url": str( + linux_validation_terminal_verdict_publish_report["follow_up_queue_url"] + ), + "linux_validation_dispatch_summary": str( + linux_validation_terminal_verdict_publish_report["linux_validation_dispatch_summary"] + ), + "linux_validation_terminal_verdict_summary": str( + linux_validation_terminal_verdict_publish_report["linux_validation_terminal_verdict_summary"] + ), + "linux_validation_terminal_dispatch_status": dispatch_status, + "linux_validation_terminal_dispatch_decision": dispatch_decision, + "linux_validation_terminal_dispatch_exit_code": dispatch_exit_code, + "linux_validation_terminal_should_dispatch": dispatch_should_dispatch, + "linux_validation_terminal_dispatch_requires_manual_action": dispatch_requires_manual_action, + "linux_validation_terminal_dispatch_channel": dispatch_channel, + "linux_validation_terminal_dispatch_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Dispatch Report", + "", + ( + "- Linux Validation Terminal Dispatch Status: " + f"**{str(payload['linux_validation_terminal_dispatch_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Dispatch Decision: " + f"`{payload['linux_validation_terminal_dispatch_decision']}`" + ), + ( + "- Linux Validation Terminal Dispatch Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_exit_code']}`" + ), + ( + "- Linux Validation Terminal Should Dispatch: " + f"`{payload['linux_validation_terminal_should_dispatch']}`" + ), + ( + "- Linux Validation Terminal Dispatch Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_requires_manual_action']}`" + ), + ( + "- Linux Validation Terminal Dispatch Channel: " + f"`{payload['linux_validation_terminal_dispatch_channel']}`" + ), + ( + "- Linux Validation Terminal Verdict Publish Status: " + f"`{payload['linux_validation_terminal_verdict_publish_status']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_status": str( + payload["linux_validation_terminal_dispatch_status"] + ), + "workflow_linux_validation_terminal_dispatch_decision": str( + payload["linux_validation_terminal_dispatch_decision"] + ), + "workflow_linux_validation_terminal_dispatch_exit_code": str( + payload["linux_validation_terminal_dispatch_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_should_dispatch": ( + "true" if payload["linux_validation_terminal_should_dispatch"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_dispatch_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_channel": str( + payload["linux_validation_terminal_dispatch_channel"] + ), + "workflow_linux_validation_terminal_dispatch_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_terminal_dispatch_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation terminal dispatch contract " + "from P2-68 Linux validation terminal verdict publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json", + help="P2-68 Linux validation terminal verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json", + help="Output Linux validation terminal dispatch JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md", + help="Output Linux validation terminal dispatch markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + publish_report_path = Path(args.linux_validation_terminal_verdict_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + publish_report = load_linux_validation_terminal_verdict_publish_report(publish_report_path) + payload = build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch json: {output_json_path}") + print(f"linux validation terminal dispatch markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch summary: " + f"linux_validation_terminal_dispatch_status=" + f"{payload['linux_validation_terminal_dispatch_status']} " + f"linux_validation_terminal_dispatch_decision=" + f"{payload['linux_validation_terminal_dispatch_decision']} " + f"linux_validation_terminal_dispatch_exit_code=" + f"{payload['linux_validation_terminal_dispatch_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_execution_gate.py b/scripts/run_p2_lv_td_execution_gate.py new file mode 100644 index 0000000..f94a303 --- /dev/null +++ b/scripts/run_p2_lv_td_execution_gate.py @@ -0,0 +1,887 @@ +"""Phase 2 card P2-70 gate for Linux CI workflow Linux-validation terminal dispatch execution. + +This script executes (or dry-runs) the P2-69 Linux validation terminal dispatch contract: +1) validates terminal dispatch-ready vs blocked payload contract, +2) executes canonical Linux validation terminal command when ready, +3) emits execution JSON/Markdown + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS: set[str] = { + "dispatch_linux_validation_terminal", + "dispatch_linux_validation_terminal_with_follow_up", + "hold_linux_validation_terminal_blocker", + "abort_linux_validation_terminal_dispatch", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_EXECUTION_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatched", + "dispatch_failed", + "blocked", + "contract_failed", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_terminal_dispatch_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal dispatch report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation terminal dispatch payload must be object") + + required_fields = ( + "source_linux_validation_terminal_verdict_publish_report", + "source_linux_validation_terminal_verdict_report", + "source_linux_validation_dispatch_report", + "source_linux_validation_final_publish_archive_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_terminal_verdict_status", + "linux_validation_terminal_verdict_decision", + "linux_validation_terminal_verdict_exit_code", + "linux_validation_terminal_verdict_should_proceed", + "linux_validation_terminal_verdict_requires_manual_action", + "linux_validation_terminal_verdict_channel", + "linux_validation_terminal_verdict_publish_status", + "linux_validation_terminal_verdict_publish_decision", + "linux_validation_terminal_verdict_publish_exit_code", + "linux_validation_terminal_verdict_publish_should_notify", + "linux_validation_terminal_verdict_publish_requires_manual_action", + "linux_validation_terminal_verdict_publish_channel", + "linux_validation_terminal_verdict_publish_summary", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_terminal_verdict_summary", + "linux_validation_terminal_dispatch_status", + "linux_validation_terminal_dispatch_decision", + "linux_validation_terminal_dispatch_exit_code", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_requires_manual_action", + "linux_validation_terminal_dispatch_channel", + "linux_validation_terminal_dispatch_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + dispatch_status = _coerce_str( + payload["linux_validation_terminal_dispatch_status"], + field="linux_validation_terminal_dispatch_status", + path=path, + ) + if dispatch_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_STATUSES)}" + ) + + dispatch_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_decision"], + field="linux_validation_terminal_dispatch_decision", + path=path, + ) + if dispatch_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS)}" + ) + + dispatch_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_exit_code"], + field="linux_validation_terminal_dispatch_exit_code", + path=path, + ) + if dispatch_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_exit_code' must be >= 0" + ) + + dispatch_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_channel"], + field="linux_validation_terminal_dispatch_channel", + path=path, + ) + if dispatch_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + expected_dispatch = { + "ready_dry_run": ( + "dispatch_linux_validation_terminal", + True, + False, + "release", + 0, + ), + "ready_with_follow_up_dry_run": ( + "dispatch_linux_validation_terminal_with_follow_up", + True, + False, + "follow_up", + None, + ), + "blocked": ( + "hold_linux_validation_terminal_blocker", + False, + True, + "blocker", + None, + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch", + False, + True, + "blocker", + None, + ), + } + ( + expected_decision, + expected_should_dispatch, + expected_requires_manual_action, + expected_channel, + expected_exit_code, + ) = expected_dispatch[dispatch_status] + + should_dispatch = _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ) + requires_manual_action = _coerce_bool( + payload["linux_validation_terminal_dispatch_requires_manual_action"], + field="linux_validation_terminal_dispatch_requires_manual_action", + path=path, + ) + if dispatch_decision != expected_decision: + raise ValueError( + f"{path}: dispatch decision mismatch for status={dispatch_status}; " + f"expected {expected_decision}, got {dispatch_decision}" + ) + if should_dispatch != expected_should_dispatch: + raise ValueError( + f"{path}: linux_validation_terminal_should_dispatch mismatch for status={dispatch_status}" + ) + if requires_manual_action != expected_requires_manual_action: + raise ValueError( + f"{path}: linux_validation_terminal_dispatch_requires_manual_action mismatch for " + f"status={dispatch_status}" + ) + if dispatch_channel != expected_channel: + raise ValueError( + f"{path}: linux_validation_terminal_dispatch_channel mismatch for status={dispatch_status}; " + f"expected {expected_channel}, got {dispatch_channel}" + ) + if expected_exit_code == 0 and dispatch_exit_code != 0: + raise ValueError( + f"{path}: linux_validation_terminal_dispatch_exit_code must be 0 when status=ready_dry_run" + ) + if expected_exit_code is None and dispatch_exit_code == 0 and dispatch_status != "ready_dry_run": + raise ValueError( + f"{path}: linux_validation_terminal_dispatch_exit_code cannot be 0 when status={dispatch_status}" + ) + + payload["release_run_id"] = release_run_id + payload["linux_validation_dispatch_command_returncode"] = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + payload["verdict_command_returncode"] = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + payload["gate_manifest_drift_missing_runtime_tests"] = _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ) + payload["gate_manifest_drift_missing_manifest_entries"] = _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ) + payload["gate_manifest_drift_orphan_manifest_entries"] = _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ) + payload["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + payload["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + payload["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + payload["evidence_manifest"] = _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ) + return payload + + +def build_linux_validation_terminal_command_parts( + *, + terminal_dispatch_report: dict[str, Any], + python_executable: str, + linux_validation_terminal_command: str, +) -> list[str]: + if not bool(terminal_dispatch_report["linux_validation_terminal_should_dispatch"]): + return [] + if linux_validation_terminal_command.strip(): + try: + parts = shlex.split(linux_validation_terminal_command) + except ValueError as exc: + raise ValueError(f"invalid --linux-validation-terminal-command ({exc})") from exc + if not parts: + raise ValueError("invalid --linux-validation-terminal-command (empty command)") + return parts + return [ + python_executable, + "scripts/run_p2_linux_unified_pipeline_gate.py", + "--continue-on-failure", + ] + + +def execute_linux_validation_terminal_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"linux validation terminal command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_linux_validation_terminal_dispatch_execution_payload( + terminal_dispatch_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + linux_validation_terminal_timeout_seconds: int, + command_parts: list[str], + dry_run: bool, + command_result: dict[str, Any] | None, +) -> dict[str, Any]: + upstream_status = str(terminal_dispatch_report["linux_validation_terminal_dispatch_status"]) + upstream_decision = str(terminal_dispatch_report["linux_validation_terminal_dispatch_decision"]) + upstream_exit_code = int(terminal_dispatch_report["linux_validation_terminal_dispatch_exit_code"]) + should_dispatch = bool(terminal_dispatch_report["linux_validation_terminal_should_dispatch"]) + requires_manual_action = bool( + terminal_dispatch_report["linux_validation_terminal_dispatch_requires_manual_action"] + ) + dispatch_channel = str(terminal_dispatch_report["linux_validation_terminal_dispatch_channel"]) + + reason_codes = list(terminal_dispatch_report["reason_codes"]) + structural_issues = list(terminal_dispatch_report["structural_issues"]) + missing_artifacts = list(terminal_dispatch_report["missing_artifacts"]) + + dispatch_attempted = False + command_returncode: int | None = None + command_stdout_tail = "" + command_stderr_tail = "" + dispatch_error_type = "" + dispatch_error_message = "" + + execution_status: str + execution_decision: str + execution_exit_code: int + execution_requires_manual_action: bool + execution_channel: str + + if structural_issues or missing_artifacts: + execution_status = "contract_failed" + execution_decision = "abort_linux_validation_terminal_dispatch" + execution_exit_code = max(1, upstream_exit_code) + execution_requires_manual_action = True + execution_channel = "blocker" + elif not should_dispatch: + if upstream_status == "contract_failed": + execution_status = "contract_failed" + execution_decision = "abort_linux_validation_terminal_dispatch" + else: + execution_status = "blocked" + execution_decision = "hold_linux_validation_terminal_blocker" + execution_exit_code = max(1, upstream_exit_code) + execution_requires_manual_action = True + execution_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_execution_blocked") + elif dry_run: + execution_status = upstream_status + execution_decision = upstream_decision + execution_exit_code = upstream_exit_code + execution_requires_manual_action = requires_manual_action + execution_channel = dispatch_channel + reason_codes.append("linux_validation_terminal_dispatch_execution_dry_run") + else: + dispatch_attempted = True + if command_result is None: + execution_status = "dispatch_failed" + execution_decision = upstream_decision + execution_exit_code = 1 + execution_requires_manual_action = True + execution_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_execution_result_missing") + else: + command_returncode = int(command_result["returncode"]) + command_stdout_tail = str(command_result.get("stdout_tail", "")) + command_stderr_tail = str(command_result.get("stderr_tail", "")) + dispatch_error_type = str(command_result.get("error_type", "")) + dispatch_error_message = str(command_result.get("error_message", "")) + + if command_returncode == 0: + execution_status = "dispatched" + execution_decision = upstream_decision + execution_exit_code = max(1, upstream_exit_code) if dispatch_channel == "follow_up" else 0 + execution_requires_manual_action = False + execution_channel = dispatch_channel + reason_codes = ["linux_validation_terminal_dispatch_execution_command_succeeded"] + else: + execution_status = "dispatch_failed" + execution_decision = upstream_decision + execution_exit_code = 1 + execution_requires_manual_action = True + execution_channel = "blocker" + if dispatch_error_type == "command_not_found": + reason_codes.append("linux_validation_terminal_dispatch_execution_cli_unavailable") + elif dispatch_error_type == "timeout": + reason_codes.append("linux_validation_terminal_dispatch_execution_timeout") + else: + reason_codes.append("linux_validation_terminal_dispatch_execution_command_failed") + + if execution_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_EXECUTION_STATUSES: + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_execution_status computed" + ) + if execution_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_DECISIONS: + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_execution_decision computed" + ) + if execution_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_CHANNELS: + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_execution_channel computed" + ) + + reason_codes = _unique(reason_codes) + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + summary = ( + f"linux_validation_terminal_dispatch_status={upstream_status} " + f"linux_validation_terminal_dispatch_execution_status={execution_status} " + f"linux_validation_terminal_dispatch_execution_decision={execution_decision}" + ) + + payload = dict(terminal_dispatch_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_report": str(source_path), + "project_root": str(project_root), + "dry_run": dry_run, + "linux_validation_terminal_timeout_seconds": linux_validation_terminal_timeout_seconds, + "linux_validation_terminal_command": _format_shell_command(command_parts), + "linux_validation_terminal_command_parts": command_parts, + "linux_validation_terminal_dispatch_attempted": dispatch_attempted, + "linux_validation_terminal_dispatch_command_returncode": command_returncode, + "linux_validation_terminal_dispatch_command_stdout_tail": command_stdout_tail, + "linux_validation_terminal_dispatch_command_stderr_tail": command_stderr_tail, + "linux_validation_terminal_dispatch_error_type": dispatch_error_type, + "linux_validation_terminal_dispatch_error_message": dispatch_error_message, + "linux_validation_terminal_dispatch_execution_status": execution_status, + "linux_validation_terminal_dispatch_execution_decision": execution_decision, + "linux_validation_terminal_dispatch_execution_exit_code": execution_exit_code, + "linux_validation_terminal_dispatch_execution_requires_manual_action": ( + execution_requires_manual_action + ), + "linux_validation_terminal_dispatch_execution_channel": execution_channel, + "linux_validation_terminal_dispatch_execution_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Dispatch Execution Report", + "", + ( + "- Linux Validation Terminal Dispatch Execution Status: " + f"**{str(payload['linux_validation_terminal_dispatch_execution_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Dispatch Execution Decision: " + f"`{payload['linux_validation_terminal_dispatch_execution_decision']}`" + ), + ( + "- Linux Validation Terminal Dispatch Execution Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_execution_exit_code']}`" + ), + ( + "- Linux Validation Terminal Dispatch Attempted: " + f"`{payload['linux_validation_terminal_dispatch_attempted']}`" + ), + ( + "- Linux Validation Terminal Should Dispatch: " + f"`{payload['linux_validation_terminal_should_dispatch']}`" + ), + f"- Dry Run: `{payload['dry_run']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Terminal Dispatch Command", + ] + + if payload["linux_validation_terminal_command"]: + lines.append(f"- `{payload['linux_validation_terminal_command']}`") + else: + lines.append("- none") + + lines.extend( + [ + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_execution_summary']}", + "", + "## Reason Codes", + ] + ) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Command Output (tail)"]) + stdout_tail = str(payload["linux_validation_terminal_dispatch_command_stdout_tail"]).strip() + stderr_tail = str(payload["linux_validation_terminal_dispatch_command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + dispatch_command_returncode = payload["linux_validation_terminal_dispatch_command_returncode"] + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_execution_status": str( + payload["linux_validation_terminal_dispatch_execution_status"] + ), + "workflow_linux_validation_terminal_dispatch_execution_decision": str( + payload["linux_validation_terminal_dispatch_execution_decision"] + ), + "workflow_linux_validation_terminal_dispatch_execution_exit_code": str( + payload["linux_validation_terminal_dispatch_execution_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_execution_should_dispatch": ( + "true" if payload["linux_validation_terminal_should_dispatch"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_execution_attempted": ( + "true" if payload["linux_validation_terminal_dispatch_attempted"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_execution_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_dispatch_execution_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_execution_channel": str( + payload["linux_validation_terminal_dispatch_execution_channel"] + ), + "workflow_linux_validation_terminal_dispatch_execution_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_execution_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_execution_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_execution_command_returncode": ( + "" if dispatch_command_returncode is None else str(dispatch_command_returncode) + ), + "workflow_linux_validation_terminal_dispatch_execution_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_execution_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_execution_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Execute Linux CI workflow Linux validation terminal dispatch contract " + "from P2-69 Linux validation terminal dispatch report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json", + help="P2-69 Linux validation terminal dispatch report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for Linux validation terminal command execution", + ) + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable used by default terminal command", + ) + parser.add_argument( + "--linux-validation-terminal-command", + default="", + help="Optional explicit Linux validation terminal command string", + ) + parser.add_argument( + "--linux-validation-terminal-timeout-seconds", + type=int, + default=7200, + help="Linux validation terminal command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json", + help="Output Linux validation terminal dispatch execution JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md", + help="Output Linux validation terminal dispatch execution markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch execution JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.linux_validation_terminal_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-execution-gate] " + "--linux-validation-terminal-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + source_path = Path(args.linux_validation_terminal_dispatch_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_dispatch_report = load_linux_validation_terminal_dispatch_report(source_path) + command_parts = build_linux_validation_terminal_command_parts( + terminal_dispatch_report=terminal_dispatch_report, + python_executable=args.python_executable, + linux_validation_terminal_command=args.linux_validation_terminal_command, + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-execution-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + command_result: dict[str, Any] | None = None + if bool(terminal_dispatch_report["linux_validation_terminal_should_dispatch"]) and not args.dry_run: + command_result = execute_linux_validation_terminal_command( + command_parts, + cwd=project_root, + timeout_seconds=args.linux_validation_terminal_timeout_seconds, + ) + + payload = build_linux_validation_terminal_dispatch_execution_payload( + terminal_dispatch_report, + source_path=source_path.resolve(), + project_root=project_root.resolve(), + linux_validation_terminal_timeout_seconds=args.linux_validation_terminal_timeout_seconds, + command_parts=command_parts, + dry_run=args.dry_run, + command_result=command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch execution json: {output_json_path}") + print(f"linux validation terminal dispatch execution markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch execution summary: " + f"linux_validation_terminal_dispatch_execution_status=" + f"{payload['linux_validation_terminal_dispatch_execution_status']} " + f"linux_validation_terminal_dispatch_attempted=" + f"{payload['linux_validation_terminal_dispatch_attempted']} " + f"linux_validation_terminal_dispatch_execution_exit_code=" + f"{payload['linux_validation_terminal_dispatch_execution_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_execution_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_final_publish_archive_gate.py b/scripts/run_p2_lv_td_final_publish_archive_gate.py new file mode 100644 index 0000000..91ba1f6 --- /dev/null +++ b/scripts/run_p2_lv_td_final_publish_archive_gate.py @@ -0,0 +1,816 @@ +"""Phase 2 card P2-76 gate for Linux CI workflow Linux validation terminal dispatch final publish archive. + +This script consumes the P2-75 Linux validation terminal dispatch final verdict publish artifact and +converges one final publish archive contract: +1) validate Linux validation terminal dispatch final verdict publish contract consistency, +2) normalize final publish archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_validated", + "announce_linux_validation_terminal_dispatch_validated_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_verdict_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_linux_validation_terminal_dispatch_validated", + "archive_linux_validation_terminal_dispatch_validated_with_follow_up", + "archive_linux_validation_terminal_dispatch_in_progress", + "archive_linux_validation_terminal_dispatch_blocker", + "archive_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_publish_archive", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_final_verdict_publish_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch final verdict publish report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch final verdict publish payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_verdict_status", + "linux_validation_terminal_dispatch_final_verdict_decision", + "linux_validation_terminal_dispatch_final_verdict_exit_code", + "linux_validation_terminal_dispatch_final_should_accept", + "linux_validation_terminal_dispatch_final_requires_follow_up", + "linux_validation_terminal_dispatch_final_should_page_owner", + "linux_validation_terminal_dispatch_final_channel", + "linux_validation_terminal_dispatch_final_verdict_publish_status", + "linux_validation_terminal_dispatch_final_verdict_publish_decision", + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code", + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify", + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action", + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner", + "linux_validation_terminal_dispatch_final_verdict_publish_channel", + "linux_validation_terminal_dispatch_final_verdict_publish_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_status"], + field="linux_validation_terminal_dispatch_final_verdict_publish_status", + path=path, + ) + if ( + verdict_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"], + field="linux_validation_terminal_dispatch_final_verdict_publish_decision", + path=path, + ) + if ( + verdict_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"], + field="linux_validation_terminal_dispatch_final_verdict_publish_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_publish_exit_code' must be >= 0" + ) + + verdict_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"], + field="linux_validation_terminal_dispatch_final_verdict_publish_channel", + path=path, + ) + if ( + verdict_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_final_verdict_publish_status"] = verdict_status + report["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = verdict_decision + report["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = verdict_exit_code + report["linux_validation_terminal_dispatch_final_verdict_publish_should_notify"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_final_verdict_publish_should_notify"], + field="linux_validation_terminal_dispatch_final_verdict_publish_should_notify", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ], + field="linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action", + path=path, + ) + report["linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner"] = ( + _coerce_bool( + payload["linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner"], + field="linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner", + path=path, + ) + ) + report["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = verdict_channel + report["linux_validation_terminal_dispatch_final_verdict_status"] = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_status"], + field="linux_validation_terminal_dispatch_final_verdict_status", + path=path, + ) + report["linux_validation_terminal_dispatch_final_channel"] = _coerce_str( + payload["linux_validation_terminal_dispatch_final_channel"], + field="linux_validation_terminal_dispatch_final_channel", + path=path, + ) + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_final_publish_archive_payload( + final_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_status" + ] + ) + verdict_decision = str( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_decision" + ] + ) + verdict_exit_code = int( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code" + ] + ) + verdict_should_notify = bool( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify" + ] + ) + verdict_requires_manual_action = bool( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] + ) + verdict_should_page_owner = bool( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] + ) + verdict_channel = str( + final_verdict_publish_report[ + "linux_validation_terminal_dispatch_final_verdict_publish_channel" + ] + ) + + reason_codes = list(final_verdict_publish_report["reason_codes"]) + structural_issues = list(final_verdict_publish_report["structural_issues"]) + missing_artifacts = list(final_verdict_publish_report["missing_artifacts"]) + + expected_verdict = { + "published": ( + "announce_linux_validation_terminal_dispatch_validated", + 0, + True, + False, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_terminal_dispatch_validated_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "announce_linux_validation_terminal_dispatch_in_progress", + 0, + True, + False, + False, + None, + ), + "blocked": ( + "announce_linux_validation_terminal_dispatch_blocker", + None, + True, + True, + True, + "blocker", + ), + "failed": ( + "announce_linux_validation_terminal_dispatch_failure", + None, + True, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_final_verdict_publish", + None, + True, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_decision_mismatch" + ) + if verdict_should_notify != expected_should_notify: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify_mismatch" + ) + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action_mismatch" + ) + if verdict_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner_mismatch" + ) + if verdict_status == "in_progress": + if verdict_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_channel_mismatch" + ) + elif expected_channel is not None and verdict_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_channel_mismatch" + ) + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and verdict_exit_code == 0 + and verdict_status + in {"published_with_follow_up", "blocked", "failed", "contract_failed"} + ): + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + archive_status: str + archive_decision: str + archive_exit_code: int + archive_should_archive: bool + archive_requires_manual_action: bool + archive_should_page_owner: bool + archive_channel: str + + if structural_issues or missing_artifacts: + archive_status = "contract_failed" + archive_decision = "abort_linux_validation_terminal_dispatch_final_publish_archive" + archive_exit_code = max(1, verdict_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "published": + archive_status = "archived" + archive_decision = "archive_linux_validation_terminal_dispatch_validated" + archive_exit_code = 0 + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_final_publish_archive_archived"] + elif verdict_status == "published_with_follow_up": + archive_status = "archived_with_follow_up" + archive_decision = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + archive_exit_code = max(1, verdict_exit_code) + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_final_publish_archive_archived_with_follow_up" + ) + elif verdict_status == "in_progress": + archive_status = "in_progress" + archive_decision = "archive_linux_validation_terminal_dispatch_in_progress" + archive_exit_code = 0 + archive_should_archive = False + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" if verdict_channel == "follow_up" else "release" + reason_codes.append( + "linux_validation_terminal_dispatch_final_publish_archive_in_progress" + ) + elif verdict_status == "blocked": + archive_status = "blocked" + archive_decision = "archive_linux_validation_terminal_dispatch_blocker" + archive_exit_code = max(1, verdict_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_publish_archive_blocked") + elif verdict_status == "failed": + archive_status = "failed" + archive_decision = "archive_linux_validation_terminal_dispatch_failure" + archive_exit_code = max(1, verdict_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_publish_archive_failed") + else: + archive_status = "contract_failed" + archive_decision = "abort_linux_validation_terminal_dispatch_final_publish_archive" + archive_exit_code = max(1, verdict_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_final_publish_archive_upstream_contract_failed" + ) + + if ( + archive_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_publish_archive_status computed" + ) + if ( + archive_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_publish_archive_decision computed" + ) + if ( + archive_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_publish_archive_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=" + f"{verdict_status} " + "linux_validation_terminal_dispatch_final_publish_archive_status=" + f"{archive_status} " + "linux_validation_terminal_dispatch_final_publish_archive_decision=" + f"{archive_decision}" + ) + + payload = dict(final_verdict_publish_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": str( + source_path + ), + "linux_validation_terminal_dispatch_final_publish_archive_status": archive_status, + "linux_validation_terminal_dispatch_final_publish_archive_decision": archive_decision, + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": archive_exit_code, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": archive_should_archive, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": archive_requires_manual_action, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": archive_should_page_owner, + "linux_validation_terminal_dispatch_final_publish_archive_channel": archive_channel, + "linux_validation_terminal_dispatch_final_publish_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Final Publish Archive Report", + "", + ( + "- Final Publish Archive Status: " + f"**{str(payload['linux_validation_terminal_dispatch_final_publish_archive_status']).upper()}**" + ), + ( + "- Final Publish Archive Decision: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_decision']}`" + ), + ( + "- Final Publish Archive Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_exit_code']}`" + ), + ( + "- Should Archive: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_should_archive']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_channel']}`" + ), + ( + "- Upstream Publish Status: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_status']}`" + ), + ( + "- Upstream Publish Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_final_publish_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_final_publish_archive_status": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_decision": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_exit_code": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_should_archive": ( + "true" + if payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_channel": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_publish_status": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_publish_channel": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_final_publish_archive_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Archive Linux validation terminal dispatch final publish contract " + "from P2-75 final verdict publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + help="P2-75 Linux validation terminal dispatch final verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + help="Output Linux validation terminal dispatch final publish archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md", + help="Output Linux validation terminal dispatch final publish archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch final publish archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + final_verdict_publish_report_path = Path( + args.linux_validation_terminal_dispatch_final_verdict_publish_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + final_verdict_publish_report = ( + load_linux_validation_terminal_dispatch_final_verdict_publish_report( + final_verdict_publish_report_path + ) + ) + payload = build_linux_validation_terminal_dispatch_final_publish_archive_payload( + final_verdict_publish_report, + source_path=final_verdict_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-final-publish-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch final publish archive json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch final publish archive markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch final publish archive summary: " + "linux_validation_terminal_dispatch_final_publish_archive_status=" + f"{payload['linux_validation_terminal_dispatch_final_publish_archive_status']} " + "linux_validation_terminal_dispatch_final_publish_archive_decision=" + f"{payload['linux_validation_terminal_dispatch_final_publish_archive_decision']} " + "linux_validation_terminal_dispatch_final_publish_archive_exit_code=" + f"{payload['linux_validation_terminal_dispatch_final_publish_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_final_verdict_gate.py b/scripts/run_p2_lv_td_final_verdict_gate.py new file mode 100644 index 0000000..968d8aa --- /dev/null +++ b/scripts/run_p2_lv_td_final_verdict_gate.py @@ -0,0 +1,896 @@ +"""Phase 2 card P2-74 gate for Linux CI workflow Linux validation terminal dispatch final verdict. + +This script consumes the P2-73 Linux validation terminal dispatch terminal publish artifact and +converges one final verdict contract: +1) validate terminal-publish contract consistency, +2) normalize final dispatch-verdict semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_STATUSES: set[str] = { + "terminal_published", + "terminal_published_with_follow_up", + "terminal_in_progress", + "terminal_blocked", + "terminal_failed", + "terminal_contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_completed", + "announce_linux_validation_terminal_dispatch_completed_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failed", + "abort_linux_validation_terminal_dispatch_terminal_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation_terminal_dispatch", + "accept_linux_validation_terminal_dispatch_with_follow_up", + "keep_linux_validation_terminal_dispatch_in_progress", + "escalate_linux_validation_terminal_dispatch_blocker", + "escalate_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_verdict", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_publish_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal publish report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal publish payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_completion_status", + "linux_validation_terminal_dispatch_completion_exit_code", + "linux_validation_terminal_dispatch_execution_status", + "linux_validation_terminal_dispatch_execution_decision", + "linux_validation_terminal_dispatch_execution_exit_code", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_attempted", + "linux_validation_terminal_dispatch_trace_status", + "linux_validation_terminal_dispatch_trace_exit_code", + "should_poll_workflow_run", + "allow_in_progress", + "linux_validation_terminal_run_id", + "linux_validation_terminal_run_url", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_terminal_dispatch_terminal_publish_status", + "linux_validation_terminal_dispatch_terminal_publish_decision", + "linux_validation_terminal_dispatch_terminal_publish_exit_code", + "linux_validation_terminal_dispatch_terminal_publish_should_notify", + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff", + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_publish_channel", + "linux_validation_terminal_dispatch_terminal_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_publish_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_status"], + field="linux_validation_terminal_dispatch_terminal_publish_status", + path=path, + ) + if ( + terminal_publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_STATUSES)}" + ) + + terminal_publish_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_decision"], + field="linux_validation_terminal_dispatch_terminal_publish_decision", + path=path, + ) + if ( + terminal_publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_DECISIONS)}" + ) + + terminal_publish_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_publish_exit_code"], + field="linux_validation_terminal_dispatch_terminal_publish_exit_code", + path=path, + ) + if terminal_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_publish_exit_code' must be >= 0" + ) + + terminal_publish_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_channel"], + field="linux_validation_terminal_dispatch_terminal_publish_channel", + path=path, + ) + if ( + terminal_publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_CHANNELS)}" + ) + + run_id = _coerce_optional_int( + payload["linux_validation_terminal_run_id"], + field="linux_validation_terminal_run_id", + path=path, + ) + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_dispatch_completion_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_completion_report"], + field="source_linux_validation_terminal_dispatch_completion_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_trace_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_trace_report"], + field="source_linux_validation_terminal_dispatch_trace_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_execution_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_execution_report"], + field="source_linux_validation_terminal_dispatch_execution_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_report"], + field="source_linux_validation_terminal_dispatch_report", + path=path, + ), + "linux_validation_terminal_dispatch_completion_status": _coerce_str( + payload["linux_validation_terminal_dispatch_completion_status"], + field="linux_validation_terminal_dispatch_completion_status", + path=path, + ), + "linux_validation_terminal_dispatch_completion_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_completion_exit_code"], + field="linux_validation_terminal_dispatch_completion_exit_code", + path=path, + ), + "linux_validation_terminal_dispatch_execution_status": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_status"], + field="linux_validation_terminal_dispatch_execution_status", + path=path, + ), + "linux_validation_terminal_dispatch_execution_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_decision"], + field="linux_validation_terminal_dispatch_execution_decision", + path=path, + ), + "linux_validation_terminal_dispatch_execution_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_execution_exit_code"], + field="linux_validation_terminal_dispatch_execution_exit_code", + path=path, + ), + "linux_validation_terminal_should_dispatch": _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ), + "linux_validation_terminal_dispatch_attempted": _coerce_bool( + payload["linux_validation_terminal_dispatch_attempted"], + field="linux_validation_terminal_dispatch_attempted", + path=path, + ), + "linux_validation_terminal_dispatch_trace_status": _coerce_str( + payload["linux_validation_terminal_dispatch_trace_status"], + field="linux_validation_terminal_dispatch_trace_status", + path=path, + ), + "linux_validation_terminal_dispatch_trace_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_trace_exit_code"], + field="linux_validation_terminal_dispatch_trace_exit_code", + path=path, + ), + "should_poll_workflow_run": _coerce_bool( + payload["should_poll_workflow_run"], + field="should_poll_workflow_run", + path=path, + ), + "allow_in_progress": _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ), + "linux_validation_terminal_run_id": run_id, + "linux_validation_terminal_run_url": _coerce_str( + payload["linux_validation_terminal_run_url"], + field="linux_validation_terminal_run_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_status": terminal_publish_status, + "linux_validation_terminal_dispatch_terminal_publish_decision": terminal_publish_decision, + "linux_validation_terminal_dispatch_terminal_publish_exit_code": terminal_publish_exit_code, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_should_notify"], + field="linux_validation_terminal_dispatch_terminal_publish_should_notify", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff"], + field="linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_requires_manual_action"], + field="linux_validation_terminal_dispatch_terminal_publish_requires_manual_action", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_channel": terminal_publish_channel, + "linux_validation_terminal_dispatch_terminal_publish_summary": _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_summary"], + field="linux_validation_terminal_dispatch_terminal_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + } + + +def build_linux_validation_terminal_dispatch_final_verdict_payload( + terminal_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + terminal_publish_status = str( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_status"] + ) + terminal_publish_decision = str( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_decision"] + ) + terminal_publish_exit_code = int( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_exit_code"] + ) + terminal_publish_should_notify = bool( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_should_notify"] + ) + terminal_publish_ready_for_handoff = bool( + terminal_publish_report[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] + ) + terminal_publish_requires_manual_action = bool( + terminal_publish_report[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] + ) + terminal_publish_channel = str( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_channel"] + ) + + reason_codes = list(terminal_publish_report["reason_codes"]) + structural_issues = list(terminal_publish_report["structural_issues"]) + missing_artifacts = list(terminal_publish_report["missing_artifacts"]) + + expected_contract = { + "terminal_published": ( + "announce_linux_validation_terminal_dispatch_completed", + 0, + True, + True, + False, + "release", + ), + "terminal_published_with_follow_up": ( + "announce_linux_validation_terminal_dispatch_completed_with_follow_up", + None, + True, + True, + False, + "follow_up", + ), + "terminal_in_progress": ( + "announce_linux_validation_terminal_dispatch_in_progress", + 0, + True, + False, + False, + None, + ), + "terminal_blocked": ( + "announce_linux_validation_terminal_dispatch_blocker", + None, + True, + False, + True, + "blocker", + ), + "terminal_failed": ( + "announce_linux_validation_terminal_dispatch_failed", + None, + True, + False, + True, + "blocker", + ), + "terminal_contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_publish", + None, + True, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_ready_for_handoff, + expected_requires_manual_action, + expected_channel, + ) = expected_contract[terminal_publish_status] + + if terminal_publish_decision != expected_decision: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_decision_mismatch") + if terminal_publish_should_notify != expected_should_notify: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_should_notify_mismatch") + if terminal_publish_ready_for_handoff != expected_ready_for_handoff: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff_mismatch") + if terminal_publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_requires_manual_action_mismatch") + if expected_channel is not None and terminal_publish_channel != expected_channel: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_channel_mismatch") + if expected_exit_code == 0 and terminal_publish_exit_code != 0: + structural_issues.append("linux_validation_terminal_dispatch_terminal_publish_exit_code_mismatch_zero") + if expected_exit_code is None and terminal_publish_exit_code == 0 and terminal_publish_status in { + "terminal_published_with_follow_up", + "terminal_blocked", + "terminal_failed", + "terminal_contract_failed", + }: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_publish_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + final_status: str + final_decision: str + final_exit_code: int + final_should_accept: bool + final_requires_follow_up: bool + final_should_page_owner: bool + final_channel: str + + if structural_issues or missing_artifacts: + final_status = "contract_failed" + final_decision = "abort_linux_validation_terminal_dispatch_final_verdict" + final_exit_code = max(1, terminal_publish_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.extend(structural_issues) + elif terminal_publish_status == "terminal_contract_failed": + final_status = "contract_failed" + final_decision = "abort_linux_validation_terminal_dispatch_final_verdict" + final_exit_code = max(1, terminal_publish_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_upstream_contract_failed") + elif terminal_publish_status == "terminal_published": + final_status = "validated" + final_decision = "accept_linux_validation_terminal_dispatch" + final_exit_code = 0 + final_should_accept = True + final_requires_follow_up = False + final_should_page_owner = False + final_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_final_verdict_validated"] + elif terminal_publish_status == "terminal_published_with_follow_up": + final_status = "validated_with_follow_up" + final_decision = "accept_linux_validation_terminal_dispatch_with_follow_up" + final_exit_code = max(1, terminal_publish_exit_code) + final_should_accept = True + final_requires_follow_up = True + final_should_page_owner = False + final_channel = "follow_up" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_validated_with_follow_up") + elif terminal_publish_status == "terminal_in_progress": + final_status = "in_progress" + final_decision = "keep_linux_validation_terminal_dispatch_in_progress" + final_exit_code = 0 + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = False + final_channel = ( + "follow_up" + if terminal_publish_channel == "follow_up" + else "release" + ) + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_in_progress") + elif terminal_publish_status == "terminal_blocked": + final_status = "blocked" + final_decision = "escalate_linux_validation_terminal_dispatch_blocker" + final_exit_code = max(1, terminal_publish_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_blocked") + else: + final_status = "failed" + final_decision = "escalate_linux_validation_terminal_dispatch_failure" + final_exit_code = max(1, terminal_publish_exit_code) + final_should_accept = False + final_requires_follow_up = True + final_should_page_owner = True + final_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_failed") + + if ( + final_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_verdict_status computed" + ) + if ( + final_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_verdict_decision computed" + ) + if final_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_CHANNELS: + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_dispatch_terminal_publish_status={terminal_publish_status} " + f"linux_validation_terminal_dispatch_final_verdict_status={final_status} " + f"linux_validation_terminal_dispatch_final_verdict_decision={final_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_publish_report": str(source_path), + "source_linux_validation_terminal_dispatch_completion_report": str( + terminal_publish_report["source_linux_validation_terminal_dispatch_completion_report"] + ), + "source_linux_validation_terminal_dispatch_trace_report": str( + terminal_publish_report["source_linux_validation_terminal_dispatch_trace_report"] + ), + "source_linux_validation_terminal_dispatch_execution_report": str( + terminal_publish_report["source_linux_validation_terminal_dispatch_execution_report"] + ), + "source_linux_validation_terminal_dispatch_report": str( + terminal_publish_report["source_linux_validation_terminal_dispatch_report"] + ), + "linux_validation_terminal_dispatch_completion_status": str( + terminal_publish_report["linux_validation_terminal_dispatch_completion_status"] + ), + "linux_validation_terminal_dispatch_completion_exit_code": int( + terminal_publish_report["linux_validation_terminal_dispatch_completion_exit_code"] + ), + "linux_validation_terminal_dispatch_execution_status": str( + terminal_publish_report["linux_validation_terminal_dispatch_execution_status"] + ), + "linux_validation_terminal_dispatch_execution_decision": str( + terminal_publish_report["linux_validation_terminal_dispatch_execution_decision"] + ), + "linux_validation_terminal_dispatch_execution_exit_code": int( + terminal_publish_report["linux_validation_terminal_dispatch_execution_exit_code"] + ), + "linux_validation_terminal_should_dispatch": bool( + terminal_publish_report["linux_validation_terminal_should_dispatch"] + ), + "linux_validation_terminal_dispatch_attempted": bool( + terminal_publish_report["linux_validation_terminal_dispatch_attempted"] + ), + "linux_validation_terminal_dispatch_trace_status": str( + terminal_publish_report["linux_validation_terminal_dispatch_trace_status"] + ), + "linux_validation_terminal_dispatch_trace_exit_code": int( + terminal_publish_report["linux_validation_terminal_dispatch_trace_exit_code"] + ), + "should_poll_workflow_run": bool( + terminal_publish_report["should_poll_workflow_run"] + ), + "allow_in_progress": bool(terminal_publish_report["allow_in_progress"]), + "linux_validation_terminal_run_id": terminal_publish_report[ + "linux_validation_terminal_run_id" + ], + "linux_validation_terminal_run_url": str( + terminal_publish_report["linux_validation_terminal_run_url"] + ), + "release_run_id": terminal_publish_report["release_run_id"], + "release_run_url": str(terminal_publish_report["release_run_url"]), + "follow_up_queue_url": str(terminal_publish_report["follow_up_queue_url"]), + "linux_validation_terminal_dispatch_terminal_publish_status": terminal_publish_status, + "linux_validation_terminal_dispatch_terminal_publish_decision": terminal_publish_decision, + "linux_validation_terminal_dispatch_terminal_publish_exit_code": terminal_publish_exit_code, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": terminal_publish_should_notify, + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": terminal_publish_ready_for_handoff, + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": terminal_publish_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_publish_channel": terminal_publish_channel, + "linux_validation_terminal_dispatch_terminal_publish_summary": str( + terminal_publish_report["linux_validation_terminal_dispatch_terminal_publish_summary"] + ), + "linux_validation_terminal_dispatch_final_verdict_status": final_status, + "linux_validation_terminal_dispatch_final_verdict_decision": final_decision, + "linux_validation_terminal_dispatch_final_verdict_exit_code": final_exit_code, + "linux_validation_terminal_dispatch_final_should_accept": final_should_accept, + "linux_validation_terminal_dispatch_final_requires_follow_up": final_requires_follow_up, + "linux_validation_terminal_dispatch_final_should_page_owner": final_should_page_owner, + "linux_validation_terminal_dispatch_final_channel": final_channel, + "linux_validation_terminal_dispatch_final_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Final Verdict Report", + "", + ( + "- Final Verdict Status: " + f"**{str(payload['linux_validation_terminal_dispatch_final_verdict_status']).upper()}**" + ), + ( + "- Final Verdict Decision: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_decision']}`" + ), + ( + "- Final Verdict Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_exit_code']}`" + ), + ( + "- Final Should Accept: " + f"`{payload['linux_validation_terminal_dispatch_final_should_accept']}`" + ), + ( + "- Final Requires Follow Up: " + f"`{payload['linux_validation_terminal_dispatch_final_requires_follow_up']}`" + ), + ( + "- Final Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_final_should_page_owner']}`" + ), + ( + "- Final Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_channel']}`" + ), + ( + "- Terminal Publish Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_status']}`" + ), + ( + "- Terminal Publish Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_final_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_final_verdict_status": str( + payload["linux_validation_terminal_dispatch_final_verdict_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_decision": str( + payload["linux_validation_terminal_dispatch_final_verdict_decision"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_exit_code": str( + payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_final_should_accept": ( + "true" + if payload["linux_validation_terminal_dispatch_final_should_accept"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_requires_follow_up": ( + "true" + if payload["linux_validation_terminal_dispatch_final_requires_follow_up"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_should_page_owner": ( + "true" + if payload["linux_validation_terminal_dispatch_final_should_page_owner"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_channel": str( + payload["linux_validation_terminal_dispatch_final_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_terminal_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_terminal_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_final_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_final_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_final_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux validation terminal dispatch final verdict contract " + "from P2-73 terminal publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + help="P2-73 Linux validation terminal dispatch terminal publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + help="Output Linux validation terminal dispatch final verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md", + help="Output Linux validation terminal dispatch final verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch final verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_publish_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_publish_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_publish_report = ( + load_linux_validation_terminal_dispatch_terminal_publish_report( + terminal_publish_report_path + ) + ) + payload = build_linux_validation_terminal_dispatch_final_verdict_payload( + terminal_publish_report, + source_path=terminal_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-final-verdict-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch final verdict json: {output_json_path}") + print(f"linux validation terminal dispatch final verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch final verdict summary: " + "linux_validation_terminal_dispatch_final_verdict_status=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_status']} " + "linux_validation_terminal_dispatch_final_verdict_decision=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_decision']} " + "linux_validation_terminal_dispatch_final_verdict_exit_code=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_final_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_final_verdict_publish_gate.py b/scripts/run_p2_lv_td_final_verdict_publish_gate.py new file mode 100644 index 0000000..c8d048a --- /dev/null +++ b/scripts/run_p2_lv_td_final_verdict_publish_gate.py @@ -0,0 +1,967 @@ +"""Phase 2 card P2-75 gate for Linux CI workflow Linux validation terminal dispatch final verdict publish. + +This script consumes the P2-74 Linux validation terminal dispatch final verdict artifact and +converges one terminal publish contract: +1) validate Linux validation terminal dispatch final verdict contract, +2) normalize terminal publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation_terminal_dispatch", + "accept_linux_validation_terminal_dispatch_with_follow_up", + "keep_linux_validation_terminal_dispatch_in_progress", + "escalate_linux_validation_terminal_dispatch_blocker", + "escalate_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_verdict", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_validated", + "announce_linux_validation_terminal_dispatch_validated_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_verdict_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_final_verdict_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch final verdict report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch final verdict payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_completion_status", + "linux_validation_terminal_dispatch_completion_exit_code", + "linux_validation_terminal_dispatch_execution_status", + "linux_validation_terminal_dispatch_execution_decision", + "linux_validation_terminal_dispatch_execution_exit_code", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_attempted", + "linux_validation_terminal_dispatch_trace_status", + "linux_validation_terminal_dispatch_trace_exit_code", + "should_poll_workflow_run", + "allow_in_progress", + "linux_validation_terminal_run_id", + "linux_validation_terminal_run_url", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_terminal_dispatch_terminal_publish_status", + "linux_validation_terminal_dispatch_terminal_publish_decision", + "linux_validation_terminal_dispatch_terminal_publish_exit_code", + "linux_validation_terminal_dispatch_terminal_publish_should_notify", + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff", + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_publish_channel", + "linux_validation_terminal_dispatch_terminal_publish_summary", + "linux_validation_terminal_dispatch_final_verdict_status", + "linux_validation_terminal_dispatch_final_verdict_decision", + "linux_validation_terminal_dispatch_final_verdict_exit_code", + "linux_validation_terminal_dispatch_final_should_accept", + "linux_validation_terminal_dispatch_final_requires_follow_up", + "linux_validation_terminal_dispatch_final_should_page_owner", + "linux_validation_terminal_dispatch_final_channel", + "linux_validation_terminal_dispatch_final_verdict_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + final_verdict_status = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_status"], + field="linux_validation_terminal_dispatch_final_verdict_status", + path=path, + ) + if ( + final_verdict_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_STATUSES)}" + ) + + final_verdict_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_decision"], + field="linux_validation_terminal_dispatch_final_verdict_decision", + path=path, + ) + if ( + final_verdict_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_DECISIONS)}" + ) + + final_verdict_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_final_verdict_exit_code"], + field="linux_validation_terminal_dispatch_final_verdict_exit_code", + path=path, + ) + if final_verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_verdict_exit_code' must be >= 0" + ) + + final_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_final_channel"], + field="linux_validation_terminal_dispatch_final_channel", + path=path, + ) + if ( + final_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_CHANNELS)}" + ) + + run_id = _coerce_optional_int( + payload["linux_validation_terminal_run_id"], + field="linux_validation_terminal_run_id", + path=path, + ) + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_dispatch_terminal_publish_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_terminal_publish_report"], + field="source_linux_validation_terminal_dispatch_terminal_publish_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_completion_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_completion_report"], + field="source_linux_validation_terminal_dispatch_completion_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_trace_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_trace_report"], + field="source_linux_validation_terminal_dispatch_trace_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_execution_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_execution_report"], + field="source_linux_validation_terminal_dispatch_execution_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_report"], + field="source_linux_validation_terminal_dispatch_report", + path=path, + ), + "linux_validation_terminal_dispatch_completion_status": _coerce_str( + payload["linux_validation_terminal_dispatch_completion_status"], + field="linux_validation_terminal_dispatch_completion_status", + path=path, + ), + "linux_validation_terminal_dispatch_completion_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_completion_exit_code"], + field="linux_validation_terminal_dispatch_completion_exit_code", + path=path, + ), + "linux_validation_terminal_dispatch_execution_status": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_status"], + field="linux_validation_terminal_dispatch_execution_status", + path=path, + ), + "linux_validation_terminal_dispatch_execution_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_decision"], + field="linux_validation_terminal_dispatch_execution_decision", + path=path, + ), + "linux_validation_terminal_dispatch_execution_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_execution_exit_code"], + field="linux_validation_terminal_dispatch_execution_exit_code", + path=path, + ), + "linux_validation_terminal_should_dispatch": _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ), + "linux_validation_terminal_dispatch_attempted": _coerce_bool( + payload["linux_validation_terminal_dispatch_attempted"], + field="linux_validation_terminal_dispatch_attempted", + path=path, + ), + "linux_validation_terminal_dispatch_trace_status": _coerce_str( + payload["linux_validation_terminal_dispatch_trace_status"], + field="linux_validation_terminal_dispatch_trace_status", + path=path, + ), + "linux_validation_terminal_dispatch_trace_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_trace_exit_code"], + field="linux_validation_terminal_dispatch_trace_exit_code", + path=path, + ), + "should_poll_workflow_run": _coerce_bool( + payload["should_poll_workflow_run"], + field="should_poll_workflow_run", + path=path, + ), + "allow_in_progress": _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ), + "linux_validation_terminal_run_id": run_id, + "linux_validation_terminal_run_url": _coerce_str( + payload["linux_validation_terminal_run_url"], + field="linux_validation_terminal_run_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_status": _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_status"], + field="linux_validation_terminal_dispatch_terminal_publish_status", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_decision"], + field="linux_validation_terminal_dispatch_terminal_publish_decision", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_publish_exit_code"], + field="linux_validation_terminal_dispatch_terminal_publish_exit_code", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_should_notify": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_should_notify"], + field="linux_validation_terminal_dispatch_terminal_publish_should_notify", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff"], + field="linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_publish_requires_manual_action"], + field="linux_validation_terminal_dispatch_terminal_publish_requires_manual_action", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_channel": _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_channel"], + field="linux_validation_terminal_dispatch_terminal_publish_channel", + path=path, + ), + "linux_validation_terminal_dispatch_terminal_publish_summary": _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_publish_summary"], + field="linux_validation_terminal_dispatch_terminal_publish_summary", + path=path, + ), + "linux_validation_terminal_dispatch_final_verdict_status": final_verdict_status, + "linux_validation_terminal_dispatch_final_verdict_decision": final_verdict_decision, + "linux_validation_terminal_dispatch_final_verdict_exit_code": final_verdict_exit_code, + "linux_validation_terminal_dispatch_final_should_accept": _coerce_bool( + payload["linux_validation_terminal_dispatch_final_should_accept"], + field="linux_validation_terminal_dispatch_final_should_accept", + path=path, + ), + "linux_validation_terminal_dispatch_final_requires_follow_up": _coerce_bool( + payload["linux_validation_terminal_dispatch_final_requires_follow_up"], + field="linux_validation_terminal_dispatch_final_requires_follow_up", + path=path, + ), + "linux_validation_terminal_dispatch_final_should_page_owner": _coerce_bool( + payload["linux_validation_terminal_dispatch_final_should_page_owner"], + field="linux_validation_terminal_dispatch_final_should_page_owner", + path=path, + ), + "linux_validation_terminal_dispatch_final_channel": final_channel, + "linux_validation_terminal_dispatch_final_verdict_summary": _coerce_str( + payload["linux_validation_terminal_dispatch_final_verdict_summary"], + field="linux_validation_terminal_dispatch_final_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + } + + +def build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + final_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + final_verdict_status = str( + final_verdict_report["linux_validation_terminal_dispatch_final_verdict_status"] + ) + final_verdict_decision = str( + final_verdict_report["linux_validation_terminal_dispatch_final_verdict_decision"] + ) + final_verdict_exit_code = int( + final_verdict_report["linux_validation_terminal_dispatch_final_verdict_exit_code"] + ) + final_should_accept = bool( + final_verdict_report["linux_validation_terminal_dispatch_final_should_accept"] + ) + final_requires_follow_up = bool( + final_verdict_report["linux_validation_terminal_dispatch_final_requires_follow_up"] + ) + final_should_page_owner = bool( + final_verdict_report["linux_validation_terminal_dispatch_final_should_page_owner"] + ) + final_channel = str( + final_verdict_report["linux_validation_terminal_dispatch_final_channel"] + ) + + reason_codes = list(final_verdict_report["reason_codes"]) + structural_issues = list(final_verdict_report["structural_issues"]) + missing_artifacts = list(final_verdict_report["missing_artifacts"]) + + expected_contract = { + "validated": ( + "accept_linux_validation_terminal_dispatch", + 0, + True, + False, + False, + "release", + ), + "validated_with_follow_up": ( + "accept_linux_validation_terminal_dispatch_with_follow_up", + None, + True, + True, + False, + "follow_up", + ), + "in_progress": ( + "keep_linux_validation_terminal_dispatch_in_progress", + 0, + False, + True, + False, + None, + ), + "blocked": ( + "escalate_linux_validation_terminal_dispatch_blocker", + None, + False, + True, + True, + "blocker", + ), + "failed": ( + "escalate_linux_validation_terminal_dispatch_failure", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_final_verdict", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_accept, + expected_requires_follow_up, + expected_should_page_owner, + expected_channel, + ) = expected_contract[final_verdict_status] + + if final_verdict_decision != expected_decision: + structural_issues.append("linux_validation_terminal_dispatch_final_verdict_decision_mismatch") + if final_should_accept != expected_should_accept: + structural_issues.append("linux_validation_terminal_dispatch_final_should_accept_mismatch") + if final_requires_follow_up != expected_requires_follow_up: + structural_issues.append("linux_validation_terminal_dispatch_final_requires_follow_up_mismatch") + if final_should_page_owner != expected_should_page_owner: + structural_issues.append("linux_validation_terminal_dispatch_final_should_page_owner_mismatch") + if expected_channel is not None and final_channel != expected_channel: + structural_issues.append("linux_validation_terminal_dispatch_final_channel_mismatch") + if expected_exit_code == 0 and final_verdict_exit_code != 0: + structural_issues.append("linux_validation_terminal_dispatch_final_verdict_exit_code_mismatch_zero") + if expected_exit_code is None and final_verdict_exit_code == 0 and final_verdict_status in { + "validated_with_follow_up", + "blocked", + "failed", + "contract_failed", + }: + structural_issues.append( + "linux_validation_terminal_dispatch_final_verdict_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_should_page_owner: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_final_verdict_publish" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif final_verdict_status == "contract_failed": + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_final_verdict_publish" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_publish_upstream_contract_failed") + elif final_verdict_status == "validated": + publish_status = "published" + publish_decision = "announce_linux_validation_terminal_dispatch_validated" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_final_verdict_publish_validated"] + elif final_verdict_status == "validated_with_follow_up": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_publish_validated_with_follow_up") + elif final_verdict_status == "in_progress": + publish_status = "in_progress" + publish_decision = "announce_linux_validation_terminal_dispatch_in_progress" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" if final_channel == "follow_up" else "release" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_publish_in_progress") + elif final_verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_terminal_dispatch_blocker" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_publish_blocked") + else: + publish_status = "failed" + publish_decision = "announce_linux_validation_terminal_dispatch_failure" + publish_exit_code = max(1, final_verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_final_verdict_publish_failed") + + if ( + publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_verdict_publish_status computed" + ) + if ( + publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_verdict_publish_decision computed" + ) + if publish_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_final_verdict_publish_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_dispatch_final_verdict_status={final_verdict_status} " + f"linux_validation_terminal_dispatch_final_verdict_publish_status={publish_status} " + f"linux_validation_terminal_dispatch_final_verdict_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_final_verdict_report": str(source_path), + "source_linux_validation_terminal_dispatch_terminal_publish_report": str( + final_verdict_report["source_linux_validation_terminal_dispatch_terminal_publish_report"] + ), + "source_linux_validation_terminal_dispatch_completion_report": str( + final_verdict_report["source_linux_validation_terminal_dispatch_completion_report"] + ), + "source_linux_validation_terminal_dispatch_trace_report": str( + final_verdict_report["source_linux_validation_terminal_dispatch_trace_report"] + ), + "source_linux_validation_terminal_dispatch_execution_report": str( + final_verdict_report["source_linux_validation_terminal_dispatch_execution_report"] + ), + "source_linux_validation_terminal_dispatch_report": str( + final_verdict_report["source_linux_validation_terminal_dispatch_report"] + ), + "linux_validation_terminal_dispatch_completion_status": str( + final_verdict_report["linux_validation_terminal_dispatch_completion_status"] + ), + "linux_validation_terminal_dispatch_completion_exit_code": int( + final_verdict_report["linux_validation_terminal_dispatch_completion_exit_code"] + ), + "linux_validation_terminal_dispatch_execution_status": str( + final_verdict_report["linux_validation_terminal_dispatch_execution_status"] + ), + "linux_validation_terminal_dispatch_execution_decision": str( + final_verdict_report["linux_validation_terminal_dispatch_execution_decision"] + ), + "linux_validation_terminal_dispatch_execution_exit_code": int( + final_verdict_report["linux_validation_terminal_dispatch_execution_exit_code"] + ), + "linux_validation_terminal_should_dispatch": bool( + final_verdict_report["linux_validation_terminal_should_dispatch"] + ), + "linux_validation_terminal_dispatch_attempted": bool( + final_verdict_report["linux_validation_terminal_dispatch_attempted"] + ), + "linux_validation_terminal_dispatch_trace_status": str( + final_verdict_report["linux_validation_terminal_dispatch_trace_status"] + ), + "linux_validation_terminal_dispatch_trace_exit_code": int( + final_verdict_report["linux_validation_terminal_dispatch_trace_exit_code"] + ), + "should_poll_workflow_run": bool( + final_verdict_report["should_poll_workflow_run"] + ), + "allow_in_progress": bool(final_verdict_report["allow_in_progress"]), + "linux_validation_terminal_run_id": final_verdict_report[ + "linux_validation_terminal_run_id" + ], + "linux_validation_terminal_run_url": str( + final_verdict_report["linux_validation_terminal_run_url"] + ), + "release_run_id": final_verdict_report["release_run_id"], + "release_run_url": str(final_verdict_report["release_run_url"]), + "follow_up_queue_url": str(final_verdict_report["follow_up_queue_url"]), + "linux_validation_terminal_dispatch_terminal_publish_status": str( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_status"] + ), + "linux_validation_terminal_dispatch_terminal_publish_decision": str( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_decision"] + ), + "linux_validation_terminal_dispatch_terminal_publish_exit_code": int( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_exit_code"] + ), + "linux_validation_terminal_dispatch_terminal_publish_should_notify": bool( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_should_notify"] + ), + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": bool( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff"] + ), + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": bool( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_requires_manual_action"] + ), + "linux_validation_terminal_dispatch_terminal_publish_channel": str( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_channel"] + ), + "linux_validation_terminal_dispatch_terminal_publish_summary": str( + final_verdict_report["linux_validation_terminal_dispatch_terminal_publish_summary"] + ), + "linux_validation_terminal_dispatch_final_verdict_status": final_verdict_status, + "linux_validation_terminal_dispatch_final_verdict_decision": final_verdict_decision, + "linux_validation_terminal_dispatch_final_verdict_exit_code": final_verdict_exit_code, + "linux_validation_terminal_dispatch_final_should_accept": final_should_accept, + "linux_validation_terminal_dispatch_final_requires_follow_up": final_requires_follow_up, + "linux_validation_terminal_dispatch_final_should_page_owner": final_should_page_owner, + "linux_validation_terminal_dispatch_final_channel": final_channel, + "linux_validation_terminal_dispatch_final_verdict_summary": str( + final_verdict_report["linux_validation_terminal_dispatch_final_verdict_summary"] + ), + "linux_validation_terminal_dispatch_final_verdict_publish_status": publish_status, + "linux_validation_terminal_dispatch_final_verdict_publish_decision": publish_decision, + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code": publish_exit_code, + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify": publish_should_notify, + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner": publish_should_page_owner, + "linux_validation_terminal_dispatch_final_verdict_publish_channel": publish_channel, + "linux_validation_terminal_dispatch_final_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Final Verdict Publish Report", + "", + ( + "- Final Verdict Publish Status: " + f"**{str(payload['linux_validation_terminal_dispatch_final_verdict_publish_status']).upper()}**" + ), + ( + "- Final Verdict Publish Decision: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_decision']}`" + ), + ( + "- Final Verdict Publish Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_exit_code']}`" + ), + ( + "- Should Notify: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_should_notify']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_publish_channel']}`" + ), + ( + "- Final Verdict Status: " + f"`{payload['linux_validation_terminal_dispatch_final_verdict_status']}`" + ), + ( + "- Final Verdict Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_final_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_status": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_decision": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_exit_code": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_should_notify": ( + "true" + if payload["linux_validation_terminal_dispatch_final_verdict_publish_should_notify"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner": ( + "true" + if payload["linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_channel": str( + payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_final_status": str( + payload["linux_validation_terminal_dispatch_final_verdict_status"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_final_channel": str( + payload["linux_validation_terminal_dispatch_final_channel"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux validation terminal dispatch final verdict publish contract " + "from P2-74 final verdict report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + help="P2-74 Linux validation terminal dispatch final verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + help="Output Linux validation terminal dispatch final verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md", + help="Output Linux validation terminal dispatch final verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch final verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + final_verdict_report_path = Path( + args.linux_validation_terminal_dispatch_final_verdict_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + final_verdict_report = load_linux_validation_terminal_dispatch_final_verdict_report( + final_verdict_report_path + ) + payload = build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-final-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch final verdict publish json: {output_json_path}") + print(f"linux validation terminal dispatch final verdict publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch final verdict publish summary: " + "linux_validation_terminal_dispatch_final_verdict_publish_status=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_publish_status']} " + "linux_validation_terminal_dispatch_final_verdict_publish_decision=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_publish_decision']} " + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code=" + f"{payload['linux_validation_terminal_dispatch_final_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_terminal_publish_gate.py b/scripts/run_p2_lv_td_terminal_publish_gate.py new file mode 100644 index 0000000..b2684ac --- /dev/null +++ b/scripts/run_p2_lv_td_terminal_publish_gate.py @@ -0,0 +1,684 @@ +"""Phase 2 card P2-73 gate for Linux CI workflow Linux validation terminal dispatch terminal publish. + +This script consumes the P2-72 Linux validation terminal dispatch completion artifact and +converges one terminal publish contract: +1) validate Linux validation terminal dispatch completion contract, +2) normalize terminal publish semantics for downstream handoff consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_COMPLETION_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatch_failed", + "blocked", + "contract_failed", + "run_tracking_ready", + "run_tracking_missing", + "run_poll_failed", + "run_in_progress", + "run_completed_success", + "run_completed_failure", + "run_await_timeout", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_STATUSES: set[str] = { + "terminal_published", + "terminal_published_with_follow_up", + "terminal_in_progress", + "terminal_blocked", + "terminal_failed", + "terminal_contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_completed", + "announce_linux_validation_terminal_dispatch_completed_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failed", + "abort_linux_validation_terminal_dispatch_terminal_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_completion_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal dispatch completion report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch completion payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_trace_report", + "project_root", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_execution_status", + "linux_validation_terminal_dispatch_execution_decision", + "linux_validation_terminal_dispatch_execution_exit_code", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_attempted", + "linux_validation_terminal_dispatch_trace_status", + "linux_validation_terminal_dispatch_trace_exit_code", + "linux_validation_terminal_dispatch_completion_status", + "linux_validation_terminal_dispatch_completion_exit_code", + "dry_run", + "allow_in_progress", + "should_poll_workflow_run", + "linux_validation_terminal_run_id", + "linux_validation_terminal_run_url", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + completion_status = _coerce_str( + payload["linux_validation_terminal_dispatch_completion_status"], + field="linux_validation_terminal_dispatch_completion_status", + path=path, + ) + if ( + completion_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_COMPLETION_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_completion_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_COMPLETION_STATUSES)}" + ) + + completion_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_completion_exit_code"], + field="linux_validation_terminal_dispatch_completion_exit_code", + path=path, + ) + if completion_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_completion_exit_code' must be >= 0" + ) + + run_id = _coerce_optional_int( + payload["linux_validation_terminal_run_id"], + field="linux_validation_terminal_run_id", + path=path, + ) + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_dispatch_trace_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_trace_report"], + field="source_linux_validation_terminal_dispatch_trace_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_linux_validation_terminal_dispatch_execution_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_execution_report"], + field="source_linux_validation_terminal_dispatch_execution_report", + path=path, + ), + "source_linux_validation_terminal_dispatch_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_report"], + field="source_linux_validation_terminal_dispatch_report", + path=path, + ), + "linux_validation_terminal_dispatch_execution_status": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_status"], + field="linux_validation_terminal_dispatch_execution_status", + path=path, + ), + "linux_validation_terminal_dispatch_execution_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_decision"], + field="linux_validation_terminal_dispatch_execution_decision", + path=path, + ), + "linux_validation_terminal_dispatch_execution_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_execution_exit_code"], + field="linux_validation_terminal_dispatch_execution_exit_code", + path=path, + ), + "linux_validation_terminal_should_dispatch": _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ), + "linux_validation_terminal_dispatch_attempted": _coerce_bool( + payload["linux_validation_terminal_dispatch_attempted"], + field="linux_validation_terminal_dispatch_attempted", + path=path, + ), + "linux_validation_terminal_dispatch_trace_status": _coerce_str( + payload["linux_validation_terminal_dispatch_trace_status"], + field="linux_validation_terminal_dispatch_trace_status", + path=path, + ), + "linux_validation_terminal_dispatch_trace_exit_code": _coerce_int( + payload["linux_validation_terminal_dispatch_trace_exit_code"], + field="linux_validation_terminal_dispatch_trace_exit_code", + path=path, + ), + "linux_validation_terminal_dispatch_completion_status": completion_status, + "linux_validation_terminal_dispatch_completion_exit_code": completion_exit_code, + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "allow_in_progress": _coerce_bool( + payload["allow_in_progress"], + field="allow_in_progress", + path=path, + ), + "should_poll_workflow_run": _coerce_bool( + payload["should_poll_workflow_run"], + field="should_poll_workflow_run", + path=path, + ), + "linux_validation_terminal_run_id": run_id, + "linux_validation_terminal_run_url": _coerce_str( + payload["linux_validation_terminal_run_url"], + field="linux_validation_terminal_run_url", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + } + + +def build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + completion_status = str(completion_report["linux_validation_terminal_dispatch_completion_status"]) + completion_exit_code = int(completion_report["linux_validation_terminal_dispatch_completion_exit_code"]) + reason_codes = list(completion_report["reason_codes"]) + structural_issues = list(completion_report["structural_issues"]) + missing_artifacts = list(completion_report["missing_artifacts"]) + + execution_status = str(completion_report["linux_validation_terminal_dispatch_execution_status"]) + execution_decision = str(completion_report["linux_validation_terminal_dispatch_execution_decision"]) + follow_up_queue_url = str(completion_report["follow_up_queue_url"]) + is_follow_up = ( + execution_decision.endswith("_with_follow_up") + or execution_status == "ready_with_follow_up_dry_run" + or bool(follow_up_queue_url) + ) + + if completion_status == "run_completed_success" and completion_exit_code != 0: + structural_issues.append("linux_validation_terminal_dispatch_completion_exit_code_mismatch_success") + if completion_status in { + "run_tracking_missing", + "run_completed_failure", + "run_poll_failed", + "dispatch_failed", + } and completion_exit_code == 0: + structural_issues.append("linux_validation_terminal_dispatch_completion_exit_code_mismatch_failure") + if completion_status == "run_await_timeout": + allow_in_progress = bool(completion_report["allow_in_progress"]) + if allow_in_progress and completion_exit_code != 0: + structural_issues.append("linux_validation_terminal_dispatch_completion_timeout_policy_mismatch") + if not allow_in_progress and completion_exit_code == 0: + structural_issues.append("linux_validation_terminal_dispatch_completion_timeout_policy_mismatch") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + if structural_issues or missing_artifacts: + terminal_publish_status = "terminal_contract_failed" + terminal_publish_decision = "abort_linux_validation_terminal_dispatch_terminal_publish" + terminal_publish_exit_code = max(1, completion_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif completion_status == "contract_failed": + terminal_publish_status = "terminal_contract_failed" + terminal_publish_decision = "abort_linux_validation_terminal_dispatch_terminal_publish" + terminal_publish_exit_code = max(1, completion_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_completion_contract_failed") + elif completion_status == "run_completed_success": + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = True + terminal_publish_requires_manual_action = False + if is_follow_up: + terminal_publish_status = "terminal_published_with_follow_up" + terminal_publish_decision = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + terminal_publish_exit_code = max(1, completion_exit_code) + terminal_publish_channel = "follow_up" + reason_codes.append("linux_validation_terminal_dispatch_terminal_publish_with_follow_up") + else: + terminal_publish_status = "terminal_published" + terminal_publish_decision = "announce_linux_validation_terminal_dispatch_completed" + terminal_publish_exit_code = 0 + terminal_publish_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_terminal_publish_completed"] + elif completion_status in {"run_in_progress", "run_tracking_ready"} or ( + completion_status == "run_await_timeout" + and bool(completion_report["allow_in_progress"]) + and completion_exit_code == 0 + ): + terminal_publish_status = "terminal_in_progress" + terminal_publish_decision = "announce_linux_validation_terminal_dispatch_in_progress" + terminal_publish_exit_code = 0 + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = False + terminal_publish_channel = "follow_up" if is_follow_up else "release" + reason_codes.append("linux_validation_terminal_dispatch_terminal_publish_in_progress") + elif completion_status in {"ready_dry_run", "ready_with_follow_up_dry_run", "blocked"}: + terminal_publish_status = "terminal_blocked" + terminal_publish_decision = "announce_linux_validation_terminal_dispatch_blocker" + terminal_publish_exit_code = max(1, completion_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_publish_blocked") + else: + terminal_publish_status = "terminal_failed" + terminal_publish_decision = "announce_linux_validation_terminal_dispatch_failed" + terminal_publish_exit_code = max(1, completion_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_publish_failed") + + if ( + terminal_publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_publish_status computed" + ) + if ( + terminal_publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_publish_decision computed" + ) + if ( + terminal_publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_PUBLISH_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_publish_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_dispatch_completion_status={completion_status} " + f"linux_validation_terminal_dispatch_terminal_publish_status={terminal_publish_status} " + f"linux_validation_terminal_dispatch_terminal_publish_decision={terminal_publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_completion_report": str(source_path), + "source_linux_validation_terminal_dispatch_trace_report": str( + completion_report["source_linux_validation_terminal_dispatch_trace_report"] + ), + "source_linux_validation_terminal_dispatch_execution_report": str( + completion_report["source_linux_validation_terminal_dispatch_execution_report"] + ), + "source_linux_validation_terminal_dispatch_report": str( + completion_report["source_linux_validation_terminal_dispatch_report"] + ), + "linux_validation_terminal_dispatch_completion_status": completion_status, + "linux_validation_terminal_dispatch_completion_exit_code": completion_exit_code, + "linux_validation_terminal_dispatch_execution_status": execution_status, + "linux_validation_terminal_dispatch_execution_decision": execution_decision, + "linux_validation_terminal_dispatch_execution_exit_code": int( + completion_report["linux_validation_terminal_dispatch_execution_exit_code"] + ), + "linux_validation_terminal_should_dispatch": bool( + completion_report["linux_validation_terminal_should_dispatch"] + ), + "linux_validation_terminal_dispatch_attempted": bool( + completion_report["linux_validation_terminal_dispatch_attempted"] + ), + "linux_validation_terminal_dispatch_trace_status": str( + completion_report["linux_validation_terminal_dispatch_trace_status"] + ), + "linux_validation_terminal_dispatch_trace_exit_code": int( + completion_report["linux_validation_terminal_dispatch_trace_exit_code"] + ), + "should_poll_workflow_run": bool(completion_report["should_poll_workflow_run"]), + "allow_in_progress": bool(completion_report["allow_in_progress"]), + "linux_validation_terminal_run_id": completion_report["linux_validation_terminal_run_id"], + "linux_validation_terminal_run_url": str(completion_report["linux_validation_terminal_run_url"]), + "release_run_id": completion_report["release_run_id"], + "release_run_url": str(completion_report["release_run_url"]), + "follow_up_queue_url": follow_up_queue_url, + "linux_validation_terminal_dispatch_terminal_publish_status": terminal_publish_status, + "linux_validation_terminal_dispatch_terminal_publish_decision": terminal_publish_decision, + "linux_validation_terminal_dispatch_terminal_publish_exit_code": terminal_publish_exit_code, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": terminal_publish_should_notify, + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": terminal_publish_ready_for_handoff, + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": terminal_publish_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_publish_channel": terminal_publish_channel, + "linux_validation_terminal_dispatch_terminal_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Publish Report", + "", + ( + "- Terminal Publish Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_publish_status']).upper()}**" + ), + ( + "- Terminal Publish Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_decision']}`" + ), + ( + "- Terminal Publish Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_exit_code']}`" + ), + ( + "- Should Notify: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_should_notify']}`" + ), + ( + "- Ready For Handoff: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_requires_manual_action']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_publish_channel']}`" + ), + f"- Completion Status: `{payload['linux_validation_terminal_dispatch_completion_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_decision": str( + payload["linux_validation_terminal_dispatch_terminal_publish_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_publish_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_should_notify": ( + "true" + if payload["linux_validation_terminal_dispatch_terminal_publish_should_notify"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": ( + "true" + if payload["linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_dispatch_terminal_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_publish_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_terminal_publish_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux validation terminal dispatch terminal publish contract " + "from P2-72 completion report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-completion-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json", + help="P2-72 Linux validation terminal dispatch completion report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + help="Output Linux validation terminal dispatch terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md", + help="Output Linux validation terminal dispatch terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + completion_report_path = Path(args.linux_validation_terminal_dispatch_completion_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + completion_report = load_linux_validation_terminal_dispatch_completion_report( + completion_report_path + ) + payload = build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report, + source_path=completion_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch terminal publish json: {output_json_path}") + print(f"linux validation terminal dispatch terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal publish summary: " + "linux_validation_terminal_dispatch_terminal_publish_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_publish_status']} " + "linux_validation_terminal_dispatch_terminal_publish_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_publish_decision']} " + "linux_validation_terminal_dispatch_terminal_publish_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_terminal_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_terminal_verdict_gate.py b/scripts/run_p2_lv_td_terminal_verdict_gate.py new file mode 100644 index 0000000..1463f4d --- /dev/null +++ b/scripts/run_p2_lv_td_terminal_verdict_gate.py @@ -0,0 +1,789 @@ +"""Phase 2 card P2-77 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict. + +This script consumes the P2-76 Linux validation terminal dispatch final publish archive artifact and +converges one terminal verdict contract: +1) validate Linux validation terminal dispatch final publish archive contract consistency, +2) normalize terminal verdict semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_linux_validation_terminal_dispatch_validated", + "archive_linux_validation_terminal_dispatch_validated_with_follow_up", + "archive_linux_validation_terminal_dispatch_in_progress", + "archive_linux_validation_terminal_dispatch_blocker", + "archive_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_final_publish_archive", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation_terminal_dispatch", + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation_terminal_dispatch", + "proceed_linux_validation_terminal_dispatch_with_follow_up", + "hold_linux_validation_terminal_dispatch_in_progress", + "halt_linux_validation_terminal_dispatch_blocker", + "halt_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_final_publish_archive_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch final publish archive report not found" + ) + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch final publish archive payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_publish_archive_status", + "linux_validation_terminal_dispatch_final_publish_archive_decision", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code", + "linux_validation_terminal_dispatch_final_publish_archive_should_archive", + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_final_publish_archive_channel", + "linux_validation_terminal_dispatch_final_publish_archive_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + archive_status = _coerce_str( + payload["linux_validation_terminal_dispatch_final_publish_archive_status"], + field="linux_validation_terminal_dispatch_final_publish_archive_status", + path=path, + ) + if ( + archive_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_publish_archive_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_STATUSES)}" + ) + + archive_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_final_publish_archive_decision"], + field="linux_validation_terminal_dispatch_final_publish_archive_decision", + path=path, + ) + if ( + archive_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_publish_archive_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_DECISIONS)}" + ) + + archive_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"], + field="linux_validation_terminal_dispatch_final_publish_archive_exit_code", + path=path, + ) + if archive_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_publish_archive_exit_code' must be >= 0" + ) + + archive_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_final_publish_archive_channel"], + field="linux_validation_terminal_dispatch_final_publish_archive_channel", + path=path, + ) + if ( + archive_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_final_publish_archive_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_FINAL_PUBLISH_ARCHIVE_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_final_publish_archive_status"] = archive_status + report["linux_validation_terminal_dispatch_final_publish_archive_decision"] = archive_decision + report["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = archive_exit_code + report["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"], + field="linux_validation_terminal_dispatch_final_publish_archive_should_archive", + path=path, + ) + report["linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action"], + field="linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + path=path, + ) + report["linux_validation_terminal_dispatch_final_publish_archive_should_page_owner"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_final_publish_archive_should_page_owner"], + field="linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_final_publish_archive_channel"] = archive_channel + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_terminal_verdict_payload( + final_publish_archive_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + archive_status = str( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_status" + ] + ) + archive_decision = str( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_decision" + ] + ) + archive_exit_code = int( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_exit_code" + ] + ) + archive_should_archive = bool( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_should_archive" + ] + ) + archive_requires_manual_action = bool( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + ) + archive_should_page_owner = bool( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + ) + archive_channel = str( + final_publish_archive_report[ + "linux_validation_terminal_dispatch_final_publish_archive_channel" + ] + ) + + reason_codes = list(final_publish_archive_report["reason_codes"]) + structural_issues = list(final_publish_archive_report["structural_issues"]) + missing_artifacts = list(final_publish_archive_report["missing_artifacts"]) + + expected_archive = { + "archived": ( + "archive_linux_validation_terminal_dispatch_validated", + 0, + True, + False, + False, + "release", + ), + "archived_with_follow_up": ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "archive_linux_validation_terminal_dispatch_in_progress", + 0, + False, + False, + False, + None, + ), + "blocked": ( + "archive_linux_validation_terminal_dispatch_blocker", + None, + False, + True, + True, + "blocker", + ), + "failed": ( + "archive_linux_validation_terminal_dispatch_failure", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_final_publish_archive", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_archive, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_archive[archive_status] + + if archive_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_decision_mismatch" + ) + if archive_should_archive != expected_should_archive: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_should_archive_mismatch" + ) + if archive_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action_mismatch" + ) + if archive_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner_mismatch" + ) + if archive_status == "in_progress": + if archive_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_channel_mismatch" + ) + elif expected_channel is not None and archive_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_channel_mismatch" + ) + if expected_exit_code == 0 and archive_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and archive_exit_code == 0 + and archive_status + in {"archived_with_follow_up", "blocked", "failed", "contract_failed"} + ): + structural_issues.append( + "linux_validation_terminal_dispatch_final_publish_archive_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + terminal_status: str + terminal_decision: str + terminal_exit_code: int + terminal_should_proceed: bool + terminal_requires_manual_action: bool + terminal_should_page_owner: bool + terminal_channel: str + + if structural_issues or missing_artifacts: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict" + terminal_exit_code = max(1, archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_should_page_owner = True + terminal_channel = "blocker" + reason_codes.extend(structural_issues) + elif archive_status == "archived": + terminal_status = "ready_for_linux_validation_terminal_dispatch" + terminal_decision = "proceed_linux_validation_terminal_dispatch" + terminal_exit_code = 0 + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_should_page_owner = False + terminal_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_terminal_verdict_ready"] + elif archive_status == "archived_with_follow_up": + terminal_status = "ready_with_follow_up_for_linux_validation_terminal_dispatch" + terminal_decision = "proceed_linux_validation_terminal_dispatch_with_follow_up" + terminal_exit_code = max(1, archive_exit_code) + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_should_page_owner = False + terminal_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_ready_with_follow_up" + ) + elif archive_status == "in_progress": + terminal_status = "in_progress" + terminal_decision = "hold_linux_validation_terminal_dispatch_in_progress" + terminal_exit_code = 0 + terminal_should_proceed = False + terminal_requires_manual_action = False + terminal_should_page_owner = False + terminal_channel = "follow_up" if archive_channel == "follow_up" else "release" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_in_progress") + elif archive_status == "blocked": + terminal_status = "blocked" + terminal_decision = "halt_linux_validation_terminal_dispatch_blocker" + terminal_exit_code = max(1, archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_should_page_owner = True + terminal_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_blocked") + elif archive_status == "failed": + terminal_status = "failed" + terminal_decision = "halt_linux_validation_terminal_dispatch_failure" + terminal_exit_code = max(1, archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_should_page_owner = True + terminal_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_failed") + else: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict" + terminal_exit_code = max(1, archive_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_should_page_owner = True + terminal_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_upstream_contract_failed" + ) + + if ( + terminal_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_status computed" + ) + if ( + terminal_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_decision computed" + ) + if ( + terminal_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_final_publish_archive_status=" + f"{archive_status} " + "linux_validation_terminal_dispatch_terminal_verdict_status=" + f"{terminal_status} " + "linux_validation_terminal_dispatch_terminal_verdict_decision=" + f"{terminal_decision}" + ) + + payload = dict(final_publish_archive_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_final_publish_archive_report": str( + source_path + ), + "linux_validation_terminal_dispatch_terminal_verdict_status": terminal_status, + "linux_validation_terminal_dispatch_terminal_verdict_decision": terminal_decision, + "linux_validation_terminal_dispatch_terminal_verdict_exit_code": terminal_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed": terminal_should_proceed, + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action": terminal_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner": terminal_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_channel": terminal_channel, + "linux_validation_terminal_dispatch_terminal_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Report", + "", + ( + "- Terminal Verdict Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_status']).upper()}**" + ), + ( + "- Terminal Verdict Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_decision']}`" + ), + ( + "- Terminal Verdict Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_channel']}`" + ), + ( + "- Upstream Final Publish Archive Status: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_status']}`" + ), + ( + "- Upstream Final Publish Archive Channel: " + f"`{payload['linux_validation_terminal_dispatch_final_publish_archive_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_should_proceed": ( + "true" + if payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_should_page_owner": ( + "true" + if payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_final_publish_archive_status": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_final_publish_archive_channel": str( + payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Converge Linux validation terminal dispatch terminal verdict contract " + "from P2-76 final publish archive report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-final-publish-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + help="P2-76 Linux validation terminal dispatch final publish archive report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json", + help="Output Linux validation terminal dispatch terminal verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md", + help="Output Linux validation terminal dispatch terminal verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + final_publish_archive_report_path = Path( + args.linux_validation_terminal_dispatch_final_publish_archive_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + final_publish_archive_report = ( + load_linux_validation_terminal_dispatch_final_publish_archive_report( + final_publish_archive_report_path + ) + ) + payload = build_linux_validation_terminal_dispatch_terminal_verdict_payload( + final_publish_archive_report, + source_path=final_publish_archive_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict summary: " + "linux_validation_terminal_dispatch_terminal_verdict_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_trace_gate.py b/scripts/run_p2_lv_td_trace_gate.py new file mode 100644 index 0000000..5c81f19 --- /dev/null +++ b/scripts/run_p2_lv_td_trace_gate.py @@ -0,0 +1,811 @@ +"""Phase 2 card P2-71 gate for Linux CI workflow Linux-validation terminal dispatch traceability. + +This script consumes the P2-70 Linux-validation terminal dispatch execution artifact +and converges one trace contract: +1) validate terminal-dispatch execution contract, +2) extract workflow run reference (run_id/url) from execution output tails, +3) optionally poll workflow run status immediately, +4) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +RUN_URL_PATTERN = re.compile( + r"https://github\.com/(?P[A-Za-z0-9_.-]+)/(?P[A-Za-z0-9_.-]+)/actions/runs/(?P\d+)" +) + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_execution_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal dispatch execution report not found") + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch execution payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_execution_status", + "linux_validation_terminal_dispatch_execution_decision", + "linux_validation_terminal_dispatch_execution_exit_code", + "linux_validation_terminal_dispatch_execution_requires_manual_action", + "linux_validation_terminal_dispatch_execution_channel", + "linux_validation_terminal_should_dispatch", + "linux_validation_terminal_dispatch_attempted", + "linux_validation_terminal_command", + "linux_validation_terminal_command_parts", + "linux_validation_terminal_dispatch_command_returncode", + "linux_validation_terminal_dispatch_command_stdout_tail", + "linux_validation_terminal_dispatch_command_stderr_tail", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + execution_status = _coerce_str( + payload["linux_validation_terminal_dispatch_execution_status"], + field="linux_validation_terminal_dispatch_execution_status", + path=path, + ) + allowed_execution_statuses = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatched", + "dispatch_failed", + "blocked", + "contract_failed", + } + if execution_status not in allowed_execution_statuses: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_execution_status' must be one of " + f"{sorted(allowed_execution_statuses)}" + ) + + execution_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_execution_exit_code"], + field="linux_validation_terminal_dispatch_execution_exit_code", + path=path, + ) + if execution_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_execution_exit_code' must be >= 0" + ) + + should_dispatch = _coerce_bool( + payload["linux_validation_terminal_should_dispatch"], + field="linux_validation_terminal_should_dispatch", + path=path, + ) + dispatch_attempted = _coerce_bool( + payload["linux_validation_terminal_dispatch_attempted"], + field="linux_validation_terminal_dispatch_attempted", + path=path, + ) + command_returncode = _coerce_optional_int( + payload["linux_validation_terminal_dispatch_command_returncode"], + field="linux_validation_terminal_dispatch_command_returncode", + path=path, + ) + + if execution_status in {"ready_dry_run", "ready_with_follow_up_dry_run"}: + if not should_dispatch: + raise ValueError( + f"{path}: {execution_status} requires linux_validation_terminal_should_dispatch=true" + ) + if dispatch_attempted: + raise ValueError( + f"{path}: {execution_status} requires linux_validation_terminal_dispatch_attempted=false" + ) + elif execution_status == "dispatched": + if not should_dispatch: + raise ValueError(f"{path}: dispatched requires linux_validation_terminal_should_dispatch=true") + if not dispatch_attempted: + raise ValueError(f"{path}: dispatched requires linux_validation_terminal_dispatch_attempted=true") + if command_returncode != 0: + raise ValueError( + f"{path}: dispatched requires linux_validation_terminal_dispatch_command_returncode=0" + ) + elif execution_status == "dispatch_failed": + if not should_dispatch: + raise ValueError( + f"{path}: dispatch_failed requires linux_validation_terminal_should_dispatch=true" + ) + if not dispatch_attempted: + raise ValueError( + f"{path}: dispatch_failed requires linux_validation_terminal_dispatch_attempted=true" + ) + else: + if should_dispatch: + raise ValueError( + f"{path}: {execution_status} requires linux_validation_terminal_should_dispatch=false" + ) + + release_run_id = _coerce_optional_int(payload["release_run_id"], field="release_run_id", path=path) + if release_run_id is not None and release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_terminal_dispatch_report": _coerce_str( + payload["source_linux_validation_terminal_dispatch_report"], + field="source_linux_validation_terminal_dispatch_report", + path=path, + ), + "linux_validation_terminal_dispatch_execution_status": execution_status, + "linux_validation_terminal_dispatch_execution_decision": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_decision"], + field="linux_validation_terminal_dispatch_execution_decision", + path=path, + ), + "linux_validation_terminal_dispatch_execution_exit_code": execution_exit_code, + "linux_validation_terminal_dispatch_execution_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_dispatch_execution_requires_manual_action"], + field="linux_validation_terminal_dispatch_execution_requires_manual_action", + path=path, + ), + "linux_validation_terminal_dispatch_execution_channel": _coerce_str( + payload["linux_validation_terminal_dispatch_execution_channel"], + field="linux_validation_terminal_dispatch_execution_channel", + path=path, + ), + "linux_validation_terminal_should_dispatch": should_dispatch, + "linux_validation_terminal_dispatch_attempted": dispatch_attempted, + "linux_validation_terminal_command": _coerce_str( + payload["linux_validation_terminal_command"], + field="linux_validation_terminal_command", + path=path, + ), + "linux_validation_terminal_command_parts": _coerce_str_list( + payload["linux_validation_terminal_command_parts"], + field="linux_validation_terminal_command_parts", + path=path, + ), + "linux_validation_terminal_dispatch_command_returncode": command_returncode, + "linux_validation_terminal_dispatch_command_stdout_tail": _coerce_str( + payload["linux_validation_terminal_dispatch_command_stdout_tail"], + field="linux_validation_terminal_dispatch_command_stdout_tail", + path=path, + ), + "linux_validation_terminal_dispatch_command_stderr_tail": _coerce_str( + payload["linux_validation_terminal_dispatch_command_stderr_tail"], + field="linux_validation_terminal_dispatch_command_stderr_tail", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "reason_codes": _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + } + + +def extract_run_reference(*, stdout_tail: str, stderr_tail: str) -> dict[str, Any]: + merged = "\n".join(item for item in (stdout_tail, stderr_tail) if item) + match = RUN_URL_PATTERN.search(merged) + if not match: + return { + "run_url": "", + "run_id": None, + "repo_owner": "", + "repo_name": "", + } + run_url = match.group(0) + return { + "run_url": run_url, + "run_id": int(match.group("run_id")), + "repo_owner": match.group("owner"), + "repo_name": match.group("repo"), + } + + +def build_run_poll_command( + *, + gh_executable: str, + run_id: int, + repo_owner: str, + repo_name: str, +) -> list[str]: + command = [ + gh_executable, + "run", + "view", + str(run_id), + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + ] + if repo_owner and repo_name: + command.extend(["--repo", f"{repo_owner}/{repo_name}"]) + return command + + +def execute_run_poll_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "payload": None, + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "payload": None, + "error_type": "timeout", + "error_message": f"run poll timeout after {timeout_seconds}s", + } + + stdout_text = completed.stdout or "" + stderr_text = completed.stderr or "" + poll_payload: dict[str, Any] | None = None + error_type = "" + error_message = "" + if completed.returncode == 0: + try: + parsed = json.loads(stdout_text) + except json.JSONDecodeError as exc: + error_type = "invalid_poll_json" + error_message = str(exc) + else: + if isinstance(parsed, dict): + poll_payload = parsed + else: + error_type = "invalid_poll_payload" + error_message = "poll payload must be object" + + return { + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(stdout_text), + "stderr_tail": _tail_text(stderr_text), + "payload": poll_payload, + "error_type": error_type, + "error_message": error_message, + } + + +def build_linux_validation_terminal_dispatch_trace_payload( + terminal_dispatch_execution_report: dict[str, Any], + *, + source_path: Path, + gh_executable: str, + poll_now: bool, + dry_run: bool, + project_root: Path, + poll_timeout_seconds: int, +) -> dict[str, Any]: + execution_status = str( + terminal_dispatch_execution_report["linux_validation_terminal_dispatch_execution_status"] + ) + reason_codes = list(terminal_dispatch_execution_report["reason_codes"]) + structural_issues = list(terminal_dispatch_execution_report["structural_issues"]) + missing_artifacts = list(terminal_dispatch_execution_report["missing_artifacts"]) + + run_ref = extract_run_reference( + stdout_tail=str( + terminal_dispatch_execution_report[ + "linux_validation_terminal_dispatch_command_stdout_tail" + ] + ), + stderr_tail=str( + terminal_dispatch_execution_report[ + "linux_validation_terminal_dispatch_command_stderr_tail" + ] + ), + ) + run_id = run_ref["run_id"] + run_url = str(run_ref["run_url"]) + repo_owner = str(run_ref["repo_owner"]) + repo_name = str(run_ref["repo_name"]) + + should_poll_workflow_run = execution_status == "dispatched" and run_id is not None + trace_status = execution_status + trace_exit_code = int( + terminal_dispatch_execution_report["linux_validation_terminal_dispatch_execution_exit_code"] + ) + poll_command_parts: list[str] = [] + poll_command = "" + poll_attempted = False + poll_returncode: int | None = None + poll_status = "" + poll_conclusion = "" + poll_url = run_url + poll_error_type = "" + poll_error_message = "" + poll_stdout_tail = "" + poll_stderr_tail = "" + + if structural_issues or missing_artifacts: + trace_status = "contract_failed" + trace_exit_code = max(1, trace_exit_code) + reason_codes.append("linux_validation_terminal_dispatch_trace_contract_failed") + elif execution_status == "dispatched": + if run_id is None: + trace_status = "run_tracking_missing" + trace_exit_code = 1 + reason_codes.append("linux_validation_terminal_dispatch_run_reference_missing") + else: + trace_status = "run_tracking_ready" + trace_exit_code = 0 + poll_command_parts = build_run_poll_command( + gh_executable=gh_executable, + run_id=int(run_id), + repo_owner=repo_owner, + repo_name=repo_name, + ) + poll_command = _format_shell_command(poll_command_parts) + + if poll_now and not dry_run: + poll_attempted = True + poll_result = execute_run_poll_command( + poll_command_parts, + cwd=project_root, + timeout_seconds=poll_timeout_seconds, + ) + poll_returncode = int(poll_result["returncode"]) + poll_stdout_tail = str(poll_result["stdout_tail"]) + poll_stderr_tail = str(poll_result["stderr_tail"]) + poll_error_type = str(poll_result["error_type"]) + poll_error_message = str(poll_result["error_message"]) + poll_payload = poll_result["payload"] + + if poll_returncode != 0: + trace_status = "run_poll_failed" + trace_exit_code = 1 + if poll_error_type == "command_not_found": + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_cli_unavailable" + ) + elif poll_error_type == "timeout": + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_timeout" + ) + else: + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_command_failed" + ) + elif poll_payload is None: + trace_status = "run_poll_failed" + trace_exit_code = 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_poll_payload_invalid" + ) + else: + poll_status_value = poll_payload.get("status") + poll_conclusion_value = poll_payload.get("conclusion") + poll_url_value = poll_payload.get("url") + if isinstance(poll_status_value, str): + poll_status = poll_status_value + if isinstance(poll_conclusion_value, str): + poll_conclusion = poll_conclusion_value + if isinstance(poll_url_value, str) and poll_url_value: + poll_url = poll_url_value + + if poll_status == "completed": + if poll_conclusion == "success": + trace_status = "run_completed_success" + trace_exit_code = 0 + reason_codes = [ + "linux_validation_terminal_dispatch_run_completed_success" + ] + else: + trace_status = "run_completed_failure" + trace_exit_code = 1 + reason_codes.append( + "linux_validation_terminal_dispatch_run_completed_with_failure" + ) + else: + trace_status = "run_in_progress" + trace_exit_code = 0 + reason_codes.append( + "linux_validation_terminal_dispatch_run_still_in_progress" + ) + elif execution_status in {"dispatch_failed", "blocked", "contract_failed"}: + trace_exit_code = max(1, trace_exit_code) + elif execution_status in {"ready_dry_run", "ready_with_follow_up_dry_run"}: + trace_exit_code = max(0, trace_exit_code) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_execution_report": str(source_path), + "project_root": str(project_root), + "source_linux_validation_terminal_dispatch_report": str( + terminal_dispatch_execution_report["source_linux_validation_terminal_dispatch_report"] + ), + "linux_validation_terminal_dispatch_execution_status": execution_status, + "linux_validation_terminal_dispatch_execution_decision": str( + terminal_dispatch_execution_report[ + "linux_validation_terminal_dispatch_execution_decision" + ] + ), + "linux_validation_terminal_dispatch_execution_exit_code": int( + terminal_dispatch_execution_report[ + "linux_validation_terminal_dispatch_execution_exit_code" + ] + ), + "linux_validation_terminal_should_dispatch": bool( + terminal_dispatch_execution_report["linux_validation_terminal_should_dispatch"] + ), + "linux_validation_terminal_dispatch_attempted": bool( + terminal_dispatch_execution_report["linux_validation_terminal_dispatch_attempted"] + ), + "linux_validation_terminal_dispatch_trace_status": trace_status, + "linux_validation_terminal_dispatch_trace_exit_code": int(trace_exit_code), + "poll_now": poll_now, + "dry_run": dry_run, + "linux_validation_terminal_should_poll_workflow_run": should_poll_workflow_run, + "linux_validation_terminal_run_id": run_id, + "linux_validation_terminal_run_url": run_url, + "linux_validation_terminal_repo_owner": repo_owner, + "linux_validation_terminal_repo_name": repo_name, + "linux_validation_terminal_poll_command": poll_command, + "linux_validation_terminal_poll_command_parts": poll_command_parts, + "linux_validation_terminal_poll_attempted": poll_attempted, + "linux_validation_terminal_poll_returncode": poll_returncode, + "linux_validation_terminal_poll_status": poll_status, + "linux_validation_terminal_poll_conclusion": poll_conclusion, + "linux_validation_terminal_poll_url": poll_url, + "linux_validation_terminal_poll_error_type": poll_error_type, + "linux_validation_terminal_poll_error_message": poll_error_message, + "linux_validation_terminal_poll_stdout_tail": poll_stdout_tail, + "linux_validation_terminal_poll_stderr_tail": poll_stderr_tail, + "release_run_id": terminal_dispatch_execution_report["release_run_id"], + "release_run_url": str(terminal_dispatch_execution_report["release_run_url"]), + "follow_up_queue_url": str(terminal_dispatch_execution_report["follow_up_queue_url"]), + "dispatch_execution_reason_codes": list(terminal_dispatch_execution_report["reason_codes"]), + "reason_codes": _unique(reason_codes), + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Dispatch Trace Report", + "", + ( + "- Linux Validation Terminal Dispatch Trace Status: " + f"**{str(payload['linux_validation_terminal_dispatch_trace_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Dispatch Trace Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_trace_exit_code']}`" + ), + ( + "- Should Poll Workflow Run: " + f"`{payload['linux_validation_terminal_should_poll_workflow_run']}`" + ), + f"- Poll Attempted: `{payload['linux_validation_terminal_poll_attempted']}`", + f"- Run ID: `{payload['linux_validation_terminal_run_id']}`", + f"- Run URL: `{payload['linux_validation_terminal_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Poll Command", + ] + + if payload["linux_validation_terminal_poll_command"]: + lines.append(f"- `{payload['linux_validation_terminal_poll_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Poll Result"]) + lines.append( + f"- Poll Return Code: `{payload['linux_validation_terminal_poll_returncode']}`" + ) + lines.append(f"- Poll Status: `{payload['linux_validation_terminal_poll_status']}`") + lines.append( + f"- Poll Conclusion: `{payload['linux_validation_terminal_poll_conclusion']}`" + ) + lines.append(f"- Poll URL: `{payload['linux_validation_terminal_poll_url']}`") + lines.append( + f"- Poll Error Type: `{payload['linux_validation_terminal_poll_error_type']}`" + ) + + if payload["linux_validation_terminal_poll_stdout_tail"]: + lines.append("- Poll stdout:") + lines.append("```text") + lines.append(str(payload["linux_validation_terminal_poll_stdout_tail"])) + lines.append("```") + if payload["linux_validation_terminal_poll_stderr_tail"]: + lines.append("- Poll stderr:") + lines.append("```text") + lines.append(str(payload["linux_validation_terminal_poll_stderr_tail"])) + lines.append("```") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + run_id = payload["linux_validation_terminal_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_trace_status": str( + payload["linux_validation_terminal_dispatch_trace_status"] + ), + "workflow_linux_validation_terminal_dispatch_trace_should_poll": ( + "true" if payload["linux_validation_terminal_should_poll_workflow_run"] else "false" + ), + "workflow_linux_validation_terminal_dispatch_trace_run_id": ( + "" if run_id is None else str(run_id) + ), + "workflow_linux_validation_terminal_dispatch_trace_run_url": str( + payload["linux_validation_terminal_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_trace_poll_status": str( + payload["linux_validation_terminal_poll_status"] + ), + "workflow_linux_validation_terminal_dispatch_trace_poll_conclusion": str( + payload["linux_validation_terminal_poll_conclusion"] + ), + "workflow_linux_validation_terminal_dispatch_trace_exit_code": str( + payload["linux_validation_terminal_dispatch_trace_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_trace_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_trace_report_json": str(output_json), + "workflow_linux_validation_terminal_dispatch_trace_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Build Linux CI workflow Linux validation terminal dispatch trace report " + "from P2-70 execution artifact" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-execution-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json", + help="P2-70 Linux validation terminal dispatch execution report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root for optional poll command execution", + ) + parser.add_argument( + "--gh-executable", + default="gh", + help="GitHub CLI executable for poll command generation/execution", + ) + parser.add_argument( + "--poll-timeout-seconds", + type=int, + default=600, + help="Workflow run poll timeout in seconds when --poll-now is enabled", + ) + parser.add_argument( + "--poll-now", + action="store_true", + help="Execute workflow run poll command when run reference is available", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json", + help="Output Linux validation terminal dispatch trace JSON report path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md", + help="Output Linux validation terminal dispatch trace markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch trace JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.poll_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-trace-gate] " + "--poll-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + report_path = Path(args.linux_validation_terminal_dispatch_execution_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + project_root = Path(args.project_root).resolve() + + try: + execution_report = load_linux_validation_terminal_dispatch_execution_report(report_path) + payload = build_linux_validation_terminal_dispatch_trace_payload( + execution_report, + source_path=report_path.resolve(), + gh_executable=args.gh_executable, + poll_now=args.poll_now, + dry_run=args.dry_run, + project_root=project_root, + poll_timeout_seconds=args.poll_timeout_seconds, + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-trace-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal dispatch trace json: {output_json_path}") + print(f"linux validation terminal dispatch trace markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch trace summary: " + f"linux_validation_terminal_dispatch_trace_status=" + f"{payload['linux_validation_terminal_dispatch_trace_status']} " + f"should_poll={payload['linux_validation_terminal_should_poll_workflow_run']} " + f"linux_validation_terminal_dispatch_trace_exit_code=" + f"{payload['linux_validation_terminal_dispatch_trace_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_trace_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_tverdict_closure_gate.py b/scripts/run_p2_lv_td_tverdict_closure_gate.py new file mode 100644 index 0000000..1ccb589 --- /dev/null +++ b/scripts/run_p2_lv_td_tverdict_closure_gate.py @@ -0,0 +1,950 @@ +"""Phase 2 card P2-80 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict closure. + +This script consumes: +1) P2-79 Linux validation terminal dispatch terminal verdict publish archive report, +2) P2-57 Linux gate manifest drift report. + +It converges one terminal verdict closure contract for downstream Linux-stage consumers. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_DECISIONS: set[ + str +] = { + "archive_linux_validation_terminal_dispatch_ready", + "archive_linux_validation_terminal_dispatch_ready_with_follow_up", + "archive_linux_validation_terminal_dispatch_in_progress", + "archive_linux_validation_terminal_dispatch_blocker", + "archive_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_CHANNELS: set[ + str +] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: set[str] = {"passed", "failed"} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_STATUSES: set[str] = { + "ready_for_linux_validation_terminal_dispatch", + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_DECISIONS: set[str] = { + "proceed_linux_validation_terminal_dispatch", + "proceed_linux_validation_terminal_dispatch_with_follow_up", + "hold_linux_validation_terminal_dispatch_in_progress", + "halt_linux_validation_terminal_dispatch_blocker", + "halt_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict publish archive report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict publish archive payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_terminal_verdict_publish_report", + "source_linux_validation_terminal_dispatch_terminal_verdict_report", + "source_linux_validation_terminal_dispatch_final_publish_archive_report", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_terminal_verdict_publish_status", + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision", + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify", + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel", + "linux_validation_terminal_dispatch_terminal_verdict_publish_summary", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + archive_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status", + path=path, + ) + if ( + archive_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_STATUSES)}" + ) + + archive_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision", + path=path, + ) + if ( + archive_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_DECISIONS)}" + ) + + archive_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code", + path=path, + ) + if archive_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code' must be >= 0" + ) + + archive_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel", + path=path, + ) + if ( + archive_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status"] = ( + archive_status + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision"] = ( + archive_decision + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code"] = ( + archive_exit_code + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel"] = ( + archive_channel + ) + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def load_gate_manifest_drift_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: gate manifest drift report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError(f"{path}: gate manifest drift payload must be object") + + required_fields = ( + "status", + "missing_runtime_tests", + "missing_manifest_entries", + "orphan_manifest_entries", + "structural_issues", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + status = _coerce_str(payload["status"], field="status", path=path) + if status not in ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: + raise ValueError( + f"{path}: field 'status' must be one of {sorted(ALLOWED_GATE_MANIFEST_DRIFT_STATUSES)}" + ) + + return { + "status": status, + "missing_runtime_tests": _coerce_str_list( + payload["missing_runtime_tests"], + field="missing_runtime_tests", + path=path, + ), + "missing_manifest_entries": _coerce_str_list( + payload["missing_manifest_entries"], + field="missing_manifest_entries", + path=path, + ), + "orphan_manifest_entries": _coerce_str_list( + payload["orphan_manifest_entries"], + field="orphan_manifest_entries", + path=path, + ), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + } + + +def build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + terminal_verdict_publish_archive_report: dict[str, Any], + gate_manifest_drift_report: dict[str, Any], + *, + archive_source_path: Path, + drift_source_path: Path, +) -> dict[str, Any]: + archive_status = str( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + ) + archive_decision = str( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + ) + archive_exit_code = int( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + ) + archive_should_archive = bool( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + ) + archive_requires_manual_action = bool( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + ) + archive_should_page_owner = bool( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + ) + archive_channel = str( + terminal_verdict_publish_archive_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + ) + + reason_codes = list(terminal_verdict_publish_archive_report["reason_codes"]) + structural_issues = list(terminal_verdict_publish_archive_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_publish_archive_report["missing_artifacts"]) + + expected_archive = { + "archived": ( + "archive_linux_validation_terminal_dispatch_ready", + 0, + True, + False, + False, + "release", + ), + "archived_with_follow_up": ( + "archive_linux_validation_terminal_dispatch_ready_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "archive_linux_validation_terminal_dispatch_in_progress", + 0, + False, + False, + False, + None, + ), + "blocked": ( + "archive_linux_validation_terminal_dispatch_blocker", + None, + False, + True, + True, + "blocker", + ), + "failed": ( + "archive_linux_validation_terminal_dispatch_failure", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_archive, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_archive[archive_status] + + if archive_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision_mismatch" + ) + if archive_should_archive != expected_should_archive: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive_mismatch" + ) + if archive_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action_mismatch" + ) + if archive_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner_mismatch" + ) + if archive_status == "in_progress": + if archive_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel_mismatch" + ) + elif expected_channel is not None and archive_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel_mismatch" + ) + if expected_exit_code == 0 and archive_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and archive_exit_code == 0 + and archive_status + in {"archived_with_follow_up", "blocked", "failed", "contract_failed"} + ): + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code_mismatch_nonzero" + ) + + drift_status = str(gate_manifest_drift_report["status"]) + drift_missing_runtime_tests = list(gate_manifest_drift_report["missing_runtime_tests"]) + drift_missing_manifest_entries = list( + gate_manifest_drift_report["missing_manifest_entries"] + ) + drift_orphan_manifest_entries = list(gate_manifest_drift_report["orphan_manifest_entries"]) + drift_structural_issues = list(gate_manifest_drift_report["structural_issues"]) + + if drift_status != "passed": + structural_issues.append("gate_manifest_drift_failed") + if drift_missing_runtime_tests: + structural_issues.append("gate_manifest_drift_missing_runtime_tests") + if drift_missing_manifest_entries: + structural_issues.append("gate_manifest_drift_missing_manifest_entries") + if drift_orphan_manifest_entries: + structural_issues.append("gate_manifest_drift_orphan_manifest_entries") + if drift_structural_issues: + structural_issues.append("gate_manifest_drift_structural_issues") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + closure_status: str + closure_decision: str + closure_exit_code: int + closure_should_proceed: bool + closure_requires_manual_action: bool + closure_should_page_owner: bool + closure_channel: str + + if structural_issues or missing_artifacts: + closure_status = "contract_failed" + closure_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_closure" + closure_exit_code = max(1, archive_exit_code) + closure_should_proceed = False + closure_requires_manual_action = True + closure_should_page_owner = True + closure_channel = "blocker" + reason_codes.extend(structural_issues) + elif archive_status == "archived": + closure_status = "ready_for_linux_validation_terminal_dispatch" + closure_decision = "proceed_linux_validation_terminal_dispatch" + closure_exit_code = 0 + closure_should_proceed = True + closure_requires_manual_action = False + closure_should_page_owner = False + closure_channel = "release" + reason_codes = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_ready", + ] + elif archive_status == "archived_with_follow_up": + closure_status = "ready_with_follow_up_for_linux_validation_terminal_dispatch" + closure_decision = "proceed_linux_validation_terminal_dispatch_with_follow_up" + closure_exit_code = max(1, archive_exit_code) + closure_should_proceed = True + closure_requires_manual_action = False + closure_should_page_owner = False + closure_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_ready_with_follow_up" + ) + elif archive_status == "in_progress": + closure_status = "in_progress" + closure_decision = "hold_linux_validation_terminal_dispatch_in_progress" + closure_exit_code = 0 + closure_should_proceed = False + closure_requires_manual_action = False + closure_should_page_owner = False + closure_channel = "follow_up" if archive_channel == "follow_up" else "release" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_closure_in_progress") + elif archive_status == "blocked": + closure_status = "blocked" + closure_decision = "halt_linux_validation_terminal_dispatch_blocker" + closure_exit_code = max(1, archive_exit_code) + closure_should_proceed = False + closure_requires_manual_action = True + closure_should_page_owner = True + closure_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_closure_blocked") + elif archive_status == "failed": + closure_status = "failed" + closure_decision = "halt_linux_validation_terminal_dispatch_failure" + closure_exit_code = max(1, archive_exit_code) + closure_should_proceed = False + closure_requires_manual_action = True + closure_should_page_owner = True + closure_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_closure_failed") + else: + closure_status = "contract_failed" + closure_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_closure" + closure_exit_code = max(1, archive_exit_code) + closure_should_proceed = False + closure_requires_manual_action = True + closure_should_page_owner = True + closure_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_upstream_contract_failed" + ) + + if ( + closure_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_status computed" + ) + if ( + closure_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_decision computed" + ) + if ( + closure_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status=" + f"{archive_status} " + "gate_manifest_drift_status=" + f"{drift_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=" + f"{closure_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision=" + f"{closure_decision}" + ) + + payload = dict(terminal_verdict_publish_archive_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report": str( + archive_source_path + ), + "source_gate_manifest_drift_report": str(drift_source_path), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": drift_missing_runtime_tests, + "gate_manifest_drift_missing_manifest_entries": drift_missing_manifest_entries, + "gate_manifest_drift_orphan_manifest_entries": drift_orphan_manifest_entries, + "linux_validation_terminal_dispatch_terminal_verdict_closure_status": closure_status, + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision": closure_decision, + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code": closure_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed": closure_should_proceed, + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action": closure_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner": closure_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel": closure_channel, + "linux_validation_terminal_dispatch_terminal_verdict_closure_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Closure Report", + "", + ( + "- Terminal Verdict Closure Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_closure_status']).upper()}**" + ), + ( + "- Terminal Verdict Closure Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_decision']}`" + ), + ( + "- Terminal Verdict Closure Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code']}`" + ), + ( + "- Should Proceed: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_channel']}`" + ), + ( + "- Upstream Publish Archive Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status']}`" + ), + ( + "- Upstream Publish Archive Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel']}`" + ), + f"- Gate Manifest Drift Status: `{payload['gate_manifest_drift_status']}`", + ( + "- Drift Missing Runtime Tests: " + f"`{len(payload['gate_manifest_drift_missing_runtime_tests'])}`" + ), + ( + "- Drift Missing Manifest Entries: " + f"`{len(payload['gate_manifest_drift_missing_manifest_entries'])}`" + ), + ( + "- Drift Orphan Manifest Entries: " + f"`{len(payload['gate_manifest_drift_orphan_manifest_entries'])}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_closure_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_missing_runtime_tests": str( + len(payload["gate_manifest_drift_missing_runtime_tests"]) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_missing_manifest_entries": str( + len(payload["gate_manifest_drift_missing_manifest_entries"]) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_orphan_manifest_entries": str( + len(payload["gate_manifest_drift_orphan_manifest_entries"]) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Converge Linux validation terminal dispatch terminal verdict closure contract " + "from P2-79 publish archive and P2-57 manifest drift reports" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json", + help="P2-79 Linux validation terminal dispatch terminal verdict publish archive report JSON path", + ) + parser.add_argument( + "--gate-manifest-drift-report", + default=".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json", + help="P2-57 gate manifest drift report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json", + help="Output Linux validation terminal dispatch terminal verdict closure JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md", + help="Output Linux validation terminal dispatch terminal verdict closure markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict closure JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + archive_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report + ) + drift_report_path = Path(args.gate_manifest_drift_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + archive_report = ( + load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_report_path + ) + ) + drift_report = load_gate_manifest_drift_report(drift_report_path) + payload = ( + build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_report_path.resolve(), + drift_source_path=drift_report_path.resolve(), + ) + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-closure-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict closure json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict closure markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict closure summary: " + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py b/scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py new file mode 100644 index 0000000..e3176b6 --- /dev/null +++ b/scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py @@ -0,0 +1,858 @@ +"""Phase 2 card P2-82 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict closure publish archive. + +This script consumes the P2-81 Linux validation terminal dispatch terminal verdict closure publish artifact and +converges one terminal verdict closure publish archive contract: +1) validate Linux validation terminal dispatch terminal verdict closure publish contract consistency, +2) normalize terminal verdict closure publish archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_ready", + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_STATUSES: set[ + str +] = { + "archived", + "archived_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_DECISIONS: set[ + str +] = { + "archive_linux_validation_terminal_dispatch_ready", + "archive_linux_validation_terminal_dispatch_ready_with_follow_up", + "archive_linux_validation_terminal_dispatch_in_progress", + "archive_linux_validation_terminal_dispatch_blocker", + "archive_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_CHANNELS: set[ + str +] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict closure publish report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict closure publish payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_terminal_verdict_closure_report", + "source_linux_validation_terminal_dispatch_final_publish_archive_report", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_publish_archive_status", + "linux_validation_terminal_dispatch_final_publish_archive_decision", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code", + "linux_validation_terminal_dispatch_final_publish_archive_should_archive", + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_final_publish_archive_channel", + "linux_validation_terminal_dispatch_final_publish_archive_summary", + "linux_validation_terminal_dispatch_terminal_verdict_closure_status", + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision", + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed", + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel", + "linux_validation_terminal_dispatch_terminal_verdict_closure_summary", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_summary", + "gate_manifest_drift_status", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + publish_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status", + path=path, + ) + if ( + publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_STATUSES)}" + ) + + publish_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision", + path=path, + ) + if ( + publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_DECISIONS)}" + ) + + publish_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code", + path=path, + ) + if publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code' must be >= 0" + ) + + publish_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel", + path=path, + ) + if ( + publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] = ( + publish_status + ) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision"] = ( + publish_decision + ) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code"] = ( + publish_exit_code + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] = ( + publish_channel + ) + report["gate_manifest_drift_status"] = _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ) + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + terminal_verdict_closure_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + publish_status = str( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status" + ] + ) + publish_decision = str( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] + ) + publish_exit_code = int( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] + ) + publish_should_notify = bool( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify" + ] + ) + publish_requires_manual_action = bool( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] + ) + publish_should_page_owner = bool( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] + ) + publish_channel = str( + terminal_verdict_closure_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel" + ] + ) + + reason_codes = list(terminal_verdict_closure_publish_report["reason_codes"]) + structural_issues = list(terminal_verdict_closure_publish_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_closure_publish_report["missing_artifacts"]) + + expected_publish = { + "published": ( + "announce_linux_validation_terminal_dispatch_ready", + 0, + True, + False, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "announce_linux_validation_terminal_dispatch_in_progress", + 0, + True, + False, + False, + None, + ), + "blocked": ( + "announce_linux_validation_terminal_dispatch_blocker", + None, + True, + True, + True, + "blocker", + ), + "failed": ( + "announce_linux_validation_terminal_dispatch_failure", + None, + True, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", + None, + True, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_publish[publish_status] + + if publish_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision_mismatch" + ) + if publish_should_notify != expected_should_notify: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify_mismatch" + ) + if publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action_mismatch" + ) + if publish_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner_mismatch" + ) + if publish_status == "in_progress": + if publish_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel_mismatch" + ) + elif expected_channel is not None and publish_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel_mismatch" + ) + if expected_exit_code == 0 and publish_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and publish_exit_code == 0 + and publish_status + in {"published_with_follow_up", "blocked", "failed", "contract_failed"} + ): + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + archive_status: str + archive_decision: str + archive_exit_code: int + archive_should_archive: bool + archive_requires_manual_action: bool + archive_should_page_owner: bool + archive_channel: str + + if structural_issues or missing_artifacts: + archive_status = "contract_failed" + archive_decision = ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.extend(structural_issues) + elif publish_status == "published": + archive_status = "archived" + archive_decision = "archive_linux_validation_terminal_dispatch_ready" + archive_exit_code = 0 + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "release" + reason_codes = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_archived" + ] + elif publish_status == "published_with_follow_up": + archive_status = "archived_with_follow_up" + archive_decision = ( + "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_archived_with_follow_up" + ) + elif publish_status == "in_progress": + archive_status = "in_progress" + archive_decision = "archive_linux_validation_terminal_dispatch_in_progress" + archive_exit_code = 0 + archive_should_archive = False + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" if publish_channel == "follow_up" else "release" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_in_progress" + ) + elif publish_status == "blocked": + archive_status = "blocked" + archive_decision = "archive_linux_validation_terminal_dispatch_blocker" + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_blocked" + ) + elif publish_status == "failed": + archive_status = "failed" + archive_decision = "archive_linux_validation_terminal_dispatch_failure" + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_failed" + ) + else: + archive_status = "contract_failed" + archive_decision = ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_upstream_contract_failed" + ) + + if ( + archive_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status computed" + ) + if ( + archive_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision computed" + ) + if ( + archive_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_ARCHIVE_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status=" + f"{publish_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status=" + f"{archive_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision=" + f"{archive_decision}" + ) + + payload = dict(terminal_verdict_closure_publish_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report": str( + source_path + ), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status": archive_status, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision": archive_decision, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code": archive_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive": archive_should_archive, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action": archive_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner": archive_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel": archive_channel, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Closure Publish Archive Report", + "", + ( + "- Terminal Verdict Closure Publish Archive Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status']).upper()}**" + ), + ( + "- Terminal Verdict Closure Publish Archive Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision']}`" + ), + ( + "- Terminal Verdict Closure Publish Archive Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code']}`" + ), + ( + "- Should Archive: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel']}`" + ), + ( + "- Upstream Terminal Verdict Closure Publish Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status']}`" + ), + ( + "- Upstream Terminal Verdict Closure Publish Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel']}`" + ), + f"- Gate Manifest Drift Status: `{payload['gate_manifest_drift_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Archive Linux validation terminal dispatch terminal verdict closure publish contract " + "from P2-81 terminal verdict closure publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json", + help="P2-81 Linux validation terminal dispatch terminal verdict closure publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json", + help="Output Linux validation terminal dispatch terminal verdict closure publish archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md", + help="Output Linux validation terminal dispatch terminal verdict closure publish archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict closure publish archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_verdict_closure_publish_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_verdict_closure_publish_report = ( + load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + terminal_verdict_closure_publish_report_path + ) + ) + payload = build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + terminal_verdict_closure_publish_report, + source_path=terminal_verdict_closure_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-closure-publish-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict closure publish archive json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict closure publish archive markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict closure publish archive summary: " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code"] + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_tverdict_closure_publish_gate.py b/scripts/run_p2_lv_td_tverdict_closure_publish_gate.py new file mode 100644 index 0000000..f123c7e --- /dev/null +++ b/scripts/run_p2_lv_td_tverdict_closure_publish_gate.py @@ -0,0 +1,806 @@ +"""Phase 2 card P2-81 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict closure publish. + +This script consumes the P2-80 Linux validation terminal dispatch terminal verdict closure artifact and +converges one terminal verdict closure publish contract: +1) validate Linux validation terminal dispatch terminal verdict closure contract, +2) normalize terminal verdict closure publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_STATUSES: set[str] = { + "ready_for_linux_validation_terminal_dispatch", + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_DECISIONS: set[str] = { + "proceed_linux_validation_terminal_dispatch", + "proceed_linux_validation_terminal_dispatch_with_follow_up", + "hold_linux_validation_terminal_dispatch_in_progress", + "halt_linux_validation_terminal_dispatch_blocker", + "halt_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_ready", + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict closure report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict closure payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_final_publish_archive_report", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_publish_archive_status", + "linux_validation_terminal_dispatch_final_publish_archive_decision", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code", + "linux_validation_terminal_dispatch_final_publish_archive_should_archive", + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_final_publish_archive_channel", + "linux_validation_terminal_dispatch_final_publish_archive_summary", + "linux_validation_terminal_dispatch_terminal_verdict_closure_status", + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision", + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed", + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel", + "linux_validation_terminal_dispatch_terminal_verdict_closure_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_status", + path=path, + ) + if ( + verdict_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_decision", + path=path, + ) + if ( + verdict_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code' must be >= 0" + ) + + verdict_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_channel", + path=path, + ) + if ( + verdict_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_closure_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = verdict_status + report["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = verdict_decision + report["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = verdict_exit_code + report["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner"], + field="linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = verdict_channel + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + terminal_verdict_closure_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str( + terminal_verdict_closure_report["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] + ) + verdict_decision = str( + terminal_verdict_closure_report["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] + ) + verdict_exit_code = int( + terminal_verdict_closure_report["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] + ) + verdict_should_proceed = bool( + terminal_verdict_closure_report["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"] + ) + verdict_requires_manual_action = bool( + terminal_verdict_closure_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + ) + verdict_should_page_owner = bool( + terminal_verdict_closure_report[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + ) + verdict_channel = str( + terminal_verdict_closure_report["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] + ) + + reason_codes = list(terminal_verdict_closure_report["reason_codes"]) + structural_issues = list(terminal_verdict_closure_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_closure_report["missing_artifacts"]) + + expected_verdict = { + "ready_for_linux_validation_terminal_dispatch": ( + "proceed_linux_validation_terminal_dispatch", + 0, + True, + False, + False, + "release", + ), + "ready_with_follow_up_for_linux_validation_terminal_dispatch": ( + "proceed_linux_validation_terminal_dispatch_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "hold_linux_validation_terminal_dispatch_in_progress", + 0, + False, + False, + False, + None, + ), + "blocked": ( + "halt_linux_validation_terminal_dispatch_blocker", + None, + False, + True, + True, + "blocker", + ), + "failed": ( + "halt_linux_validation_terminal_dispatch_failure", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_closure", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_proceed, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision_mismatch" + ) + if verdict_should_proceed != expected_should_proceed: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed_mismatch" + ) + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action_mismatch" + ) + if verdict_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner_mismatch" + ) + if verdict_status == "in_progress": + if verdict_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel_mismatch" + ) + elif expected_channel is not None and verdict_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel_mismatch" + ) + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and verdict_exit_code == 0 + and verdict_status + in { + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "blocked", + "failed", + "contract_failed", + } + ): + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_should_page_owner: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "ready_for_linux_validation_terminal_dispatch": + publish_status = "published" + publish_decision = "announce_linux_validation_terminal_dispatch_ready" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready"] + elif verdict_status == "ready_with_follow_up_for_linux_validation_terminal_dispatch": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready_with_follow_up" + ) + elif verdict_status == "in_progress": + publish_status = "in_progress" + publish_decision = "announce_linux_validation_terminal_dispatch_in_progress" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" if verdict_channel == "follow_up" else "release" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_in_progress" + ) + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_terminal_dispatch_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_closure_publish_blocked") + elif verdict_status == "failed": + publish_status = "failed" + publish_decision = "announce_linux_validation_terminal_dispatch_failure" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_closure_publish_failed") + else: + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_upstream_contract_failed" + ) + + if ( + publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status computed" + ) + if ( + publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision computed" + ) + if ( + publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CLOSURE_PUBLISH_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=" + f"{verdict_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status=" + f"{publish_status} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision=" + f"{publish_decision}" + ) + + payload = dict(terminal_verdict_closure_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_verdict_closure_report": str( + source_path + ), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status": publish_status, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision": publish_decision, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code": publish_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify": publish_should_notify, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner": publish_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel": publish_channel, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Closure Publish Report", + "", + ( + "- Terminal Verdict Closure Publish Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status']).upper()}**" + ), + ( + "- Terminal Verdict Closure Publish Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision']}`" + ), + ( + "- Terminal Verdict Closure Publish Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code']}`" + ), + ( + "- Should Notify: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel']}`" + ), + ( + "- Upstream Terminal Verdict Closure Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_status']}`" + ), + ( + "- Upstream Terminal Verdict Closure Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_terminal_verdict_closure_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_terminal_verdict_closure_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux validation terminal dispatch terminal verdict closure publish contract " + "from P2-80 terminal verdict closure report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json", + help="P2-80 Linux validation terminal dispatch terminal verdict closure report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json", + help="Output Linux validation terminal dispatch terminal verdict closure publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md", + help="Output Linux validation terminal dispatch terminal verdict closure publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict closure publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_verdict_closure_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + verdict_report_path + ) + payload = build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-closure-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict closure publish json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict closure publish markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict closure publish summary: " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + diff --git a/scripts/run_p2_lv_td_tverdict_publish_archive_gate.py b/scripts/run_p2_lv_td_tverdict_publish_archive_gate.py new file mode 100644 index 0000000..113d2cc --- /dev/null +++ b/scripts/run_p2_lv_td_tverdict_publish_archive_gate.py @@ -0,0 +1,842 @@ +"""Phase 2 card P2-79 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict publish archive. + +This script consumes the P2-78 Linux validation terminal dispatch terminal verdict publish artifact and +converges one terminal verdict publish archive contract: +1) validate Linux validation terminal dispatch terminal verdict publish contract consistency, +2) normalize terminal verdict publish archive semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_ready", + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_linux_validation_terminal_dispatch_ready", + "archive_linux_validation_terminal_dispatch_ready_with_follow_up", + "archive_linux_validation_terminal_dispatch_in_progress", + "archive_linux_validation_terminal_dispatch_blocker", + "archive_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict publish report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict publish payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_terminal_verdict_report", + "source_linux_validation_terminal_dispatch_final_publish_archive_report", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_publish_archive_status", + "linux_validation_terminal_dispatch_final_publish_archive_decision", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code", + "linux_validation_terminal_dispatch_final_publish_archive_should_archive", + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_final_publish_archive_channel", + "linux_validation_terminal_dispatch_final_publish_archive_summary", + "linux_validation_terminal_dispatch_terminal_verdict_status", + "linux_validation_terminal_dispatch_terminal_verdict_decision", + "linux_validation_terminal_dispatch_terminal_verdict_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed", + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_channel", + "linux_validation_terminal_dispatch_terminal_verdict_summary", + "linux_validation_terminal_dispatch_terminal_verdict_publish_status", + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision", + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify", + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel", + "linux_validation_terminal_dispatch_terminal_verdict_publish_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + publish_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_status", + path=path, + ) + if ( + publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_STATUSES)}" + ) + + publish_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_decision"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_decision", + path=path, + ) + if ( + publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_DECISIONS)}" + ) + + publish_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code", + path=path, + ) + if publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code' must be >= 0" + ) + + publish_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_channel", + path=path, + ) + if ( + publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + publish_status + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_decision"] = ( + publish_decision + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + publish_exit_code + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = _coerce_bool( + payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner"], + field="linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] = ( + publish_channel + ) + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + terminal_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + publish_status = str( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] + ) + publish_decision = str( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] + ) + publish_exit_code = int( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] + ) + publish_should_notify = bool( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify" + ] + ) + publish_requires_manual_action = bool( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] + ) + publish_should_page_owner = bool( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] + ) + publish_channel = str( + terminal_verdict_publish_report[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] + ) + + reason_codes = list(terminal_verdict_publish_report["reason_codes"]) + structural_issues = list(terminal_verdict_publish_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_publish_report["missing_artifacts"]) + + expected_publish = { + "published": ( + "announce_linux_validation_terminal_dispatch_ready", + 0, + True, + False, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "announce_linux_validation_terminal_dispatch_in_progress", + 0, + True, + False, + False, + None, + ), + "blocked": ( + "announce_linux_validation_terminal_dispatch_blocker", + None, + True, + True, + True, + "blocker", + ), + "failed": ( + "announce_linux_validation_terminal_dispatch_failure", + None, + True, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish", + None, + True, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_publish[publish_status] + + if publish_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision_mismatch" + ) + if publish_should_notify != expected_should_notify: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify_mismatch" + ) + if publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action_mismatch" + ) + if publish_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner_mismatch" + ) + if publish_status == "in_progress": + if publish_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel_mismatch" + ) + elif expected_channel is not None and publish_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel_mismatch" + ) + if expected_exit_code == 0 and publish_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and publish_exit_code == 0 + and publish_status + in {"published_with_follow_up", "blocked", "failed", "contract_failed"} + ): + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + archive_status: str + archive_decision: str + archive_exit_code: int + archive_should_archive: bool + archive_requires_manual_action: bool + archive_should_page_owner: bool + archive_channel: str + + if structural_issues or missing_artifacts: + archive_status = "contract_failed" + archive_decision = ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.extend(structural_issues) + elif publish_status == "published": + archive_status = "archived" + archive_decision = "archive_linux_validation_terminal_dispatch_ready" + archive_exit_code = 0 + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "release" + reason_codes = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived" + ] + elif publish_status == "published_with_follow_up": + archive_status = "archived_with_follow_up" + archive_decision = ( + "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = True + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived_with_follow_up" + ) + elif publish_status == "in_progress": + archive_status = "in_progress" + archive_decision = "archive_linux_validation_terminal_dispatch_in_progress" + archive_exit_code = 0 + archive_should_archive = False + archive_requires_manual_action = False + archive_should_page_owner = False + archive_channel = "follow_up" if publish_channel == "follow_up" else "release" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_in_progress" + ) + elif publish_status == "blocked": + archive_status = "blocked" + archive_decision = "archive_linux_validation_terminal_dispatch_blocker" + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_blocked" + ) + elif publish_status == "failed": + archive_status = "failed" + archive_decision = "archive_linux_validation_terminal_dispatch_failure" + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_failed" + ) + else: + archive_status = "contract_failed" + archive_decision = ( + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" + ) + archive_exit_code = max(1, publish_exit_code) + archive_should_archive = False + archive_requires_manual_action = True + archive_should_page_owner = True + archive_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_upstream_contract_failed" + ) + + if ( + archive_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status computed" + ) + if ( + archive_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision computed" + ) + if ( + archive_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_ARCHIVE_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=" + f"{publish_status} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status=" + f"{archive_status} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision=" + f"{archive_decision}" + ) + + payload = dict(terminal_verdict_publish_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_verdict_publish_report": str( + source_path + ), + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status": archive_status, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision": archive_decision, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code": archive_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive": archive_should_archive, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action": archive_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner": archive_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel": archive_channel, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Publish Archive Report", + "", + ( + "- Terminal Verdict Publish Archive Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status']).upper()}**" + ), + ( + "- Terminal Verdict Publish Archive Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision']}`" + ), + ( + "- Terminal Verdict Publish Archive Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code']}`" + ), + ( + "- Should Archive: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel']}`" + ), + ( + "- Upstream Terminal Verdict Publish Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_status']}`" + ), + ( + "- Upstream Terminal Verdict Publish Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Archive Linux validation terminal dispatch terminal verdict publish contract " + "from P2-78 terminal verdict publish report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json", + help="P2-78 Linux validation terminal dispatch terminal verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json", + help="Output Linux validation terminal dispatch terminal verdict publish archive JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md", + help="Output Linux validation terminal dispatch terminal verdict publish archive markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict publish archive JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + terminal_verdict_publish_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_verdict_publish_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + terminal_verdict_publish_report = ( + load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + terminal_verdict_publish_report_path + ) + ) + payload = build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + terminal_verdict_publish_report, + source_path=terminal_verdict_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-publish-archive-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict publish archive json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict publish archive markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict publish archive summary: " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code"] + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_td_tverdict_publish_gate.py b/scripts/run_p2_lv_td_tverdict_publish_gate.py new file mode 100644 index 0000000..1c3950a --- /dev/null +++ b/scripts/run_p2_lv_td_tverdict_publish_gate.py @@ -0,0 +1,801 @@ +"""Phase 2 card P2-78 gate for Linux CI workflow Linux validation terminal dispatch terminal verdict publish. + +This script consumes the P2-77 Linux validation terminal dispatch terminal verdict artifact and +converges one terminal verdict publish contract: +1) validate Linux validation terminal dispatch terminal verdict contract, +2) normalize terminal verdict publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation_terminal_dispatch", + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation_terminal_dispatch", + "proceed_linux_validation_terminal_dispatch_with_follow_up", + "hold_linux_validation_terminal_dispatch_in_progress", + "halt_linux_validation_terminal_dispatch_blocker", + "halt_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "in_progress", + "blocked", + "failed", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_dispatch_ready", + "announce_linux_validation_terminal_dispatch_ready_with_follow_up", + "announce_linux_validation_terminal_dispatch_in_progress", + "announce_linux_validation_terminal_dispatch_blocker", + "announce_linux_validation_terminal_dispatch_failure", + "abort_linux_validation_terminal_dispatch_terminal_verdict_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def load_linux_validation_terminal_dispatch_terminal_verdict_report( + path: Path, +) -> dict[str, Any]: + if not path.is_file(): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict report not found" + ) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + + if not isinstance(payload, dict): + raise ValueError( + f"{path}: Linux validation terminal dispatch terminal verdict payload must be object" + ) + + required_fields = ( + "source_linux_validation_terminal_dispatch_final_publish_archive_report", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report", + "source_linux_validation_terminal_dispatch_final_verdict_report", + "source_linux_validation_terminal_dispatch_terminal_publish_report", + "source_linux_validation_terminal_dispatch_completion_report", + "source_linux_validation_terminal_dispatch_trace_report", + "source_linux_validation_terminal_dispatch_execution_report", + "source_linux_validation_terminal_dispatch_report", + "linux_validation_terminal_dispatch_final_publish_archive_status", + "linux_validation_terminal_dispatch_final_publish_archive_decision", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code", + "linux_validation_terminal_dispatch_final_publish_archive_should_archive", + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action", + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner", + "linux_validation_terminal_dispatch_final_publish_archive_channel", + "linux_validation_terminal_dispatch_final_publish_archive_summary", + "linux_validation_terminal_dispatch_terminal_verdict_status", + "linux_validation_terminal_dispatch_terminal_verdict_decision", + "linux_validation_terminal_dispatch_terminal_verdict_exit_code", + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed", + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action", + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner", + "linux_validation_terminal_dispatch_terminal_verdict_channel", + "linux_validation_terminal_dispatch_terminal_verdict_summary", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "reason_codes", + "structural_issues", + "missing_artifacts", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_status"], + field="linux_validation_terminal_dispatch_terminal_verdict_status", + path=path, + ) + if ( + verdict_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_STATUSES + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_decision"], + field="linux_validation_terminal_dispatch_terminal_verdict_decision", + path=path, + ) + if ( + verdict_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_DECISIONS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"], + field="linux_validation_terminal_dispatch_terminal_verdict_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_exit_code' must be >= 0" + ) + + verdict_channel = _coerce_str( + payload["linux_validation_terminal_dispatch_terminal_verdict_channel"], + field="linux_validation_terminal_dispatch_terminal_verdict_channel", + path=path, + ) + if ( + verdict_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CHANNELS + ): + raise ValueError( + f"{path}: field 'linux_validation_terminal_dispatch_terminal_verdict_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_CHANNELS)}" + ) + + release_run_id = _coerce_optional_int( + payload["release_run_id"], + field="release_run_id", + path=path, + ) + + report = dict(payload) + report["linux_validation_terminal_dispatch_terminal_verdict_status"] = verdict_status + report["linux_validation_terminal_dispatch_terminal_verdict_decision"] = verdict_decision + report["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = verdict_exit_code + report["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"], + field="linux_validation_terminal_dispatch_terminal_verdict_should_proceed", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action"], + field="linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action", + path=path, + ) + report[ + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner" + ] = _coerce_bool( + payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"], + field="linux_validation_terminal_dispatch_terminal_verdict_should_page_owner", + path=path, + ) + report["linux_validation_terminal_dispatch_terminal_verdict_channel"] = verdict_channel + report["release_run_id"] = release_run_id + report["release_run_url"] = _coerce_str( + payload["release_run_url"], + field="release_run_url", + path=path, + ) + report["follow_up_queue_url"] = _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ) + report["reason_codes"] = _coerce_str_list( + payload["reason_codes"], + field="reason_codes", + path=path, + ) + report["structural_issues"] = _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ) + report["missing_artifacts"] = _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ) + return report + + +def build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + terminal_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str( + terminal_verdict_report["linux_validation_terminal_dispatch_terminal_verdict_status"] + ) + verdict_decision = str( + terminal_verdict_report["linux_validation_terminal_dispatch_terminal_verdict_decision"] + ) + verdict_exit_code = int( + terminal_verdict_report["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] + ) + verdict_should_proceed = bool( + terminal_verdict_report["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] + ) + verdict_requires_manual_action = bool( + terminal_verdict_report[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] + ) + verdict_should_page_owner = bool( + terminal_verdict_report[ + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner" + ] + ) + verdict_channel = str( + terminal_verdict_report["linux_validation_terminal_dispatch_terminal_verdict_channel"] + ) + + reason_codes = list(terminal_verdict_report["reason_codes"]) + structural_issues = list(terminal_verdict_report["structural_issues"]) + missing_artifacts = list(terminal_verdict_report["missing_artifacts"]) + + expected_verdict = { + "ready_for_linux_validation_terminal_dispatch": ( + "proceed_linux_validation_terminal_dispatch", + 0, + True, + False, + False, + "release", + ), + "ready_with_follow_up_for_linux_validation_terminal_dispatch": ( + "proceed_linux_validation_terminal_dispatch_with_follow_up", + None, + True, + False, + False, + "follow_up", + ), + "in_progress": ( + "hold_linux_validation_terminal_dispatch_in_progress", + 0, + False, + False, + False, + None, + ), + "blocked": ( + "halt_linux_validation_terminal_dispatch_blocker", + None, + False, + True, + True, + "blocker", + ), + "failed": ( + "halt_linux_validation_terminal_dispatch_failure", + None, + False, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_terminal_dispatch_terminal_verdict", + None, + False, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_proceed, + expected_requires_manual_action, + expected_should_page_owner, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_decision_mismatch" + ) + if verdict_should_proceed != expected_should_proceed: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed_mismatch" + ) + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action_mismatch" + ) + if verdict_should_page_owner != expected_should_page_owner: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner_mismatch" + ) + if verdict_status == "in_progress": + if verdict_channel not in {"release", "follow_up"}: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_channel_mismatch" + ) + elif expected_channel is not None and verdict_channel != expected_channel: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_channel_mismatch" + ) + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_exit_code_mismatch_zero" + ) + if ( + expected_exit_code is None + and verdict_exit_code == 0 + and verdict_status + in { + "ready_with_follow_up_for_linux_validation_terminal_dispatch", + "blocked", + "failed", + "contract_failed", + } + ): + structural_issues.append( + "linux_validation_terminal_dispatch_terminal_verdict_exit_code_mismatch_nonzero" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_should_page_owner: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "ready_for_linux_validation_terminal_dispatch": + publish_status = "published" + publish_decision = "announce_linux_validation_terminal_dispatch_ready" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "release" + reason_codes = ["linux_validation_terminal_dispatch_terminal_verdict_publish_ready"] + elif verdict_status == "ready_with_follow_up_for_linux_validation_terminal_dispatch": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_ready_with_follow_up" + ) + elif verdict_status == "in_progress": + publish_status = "in_progress" + publish_decision = "announce_linux_validation_terminal_dispatch_in_progress" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_should_page_owner = False + publish_channel = "follow_up" if verdict_channel == "follow_up" else "release" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_in_progress" + ) + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_terminal_dispatch_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_publish_blocked") + elif verdict_status == "failed": + publish_status = "failed" + publish_decision = "announce_linux_validation_terminal_dispatch_failure" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_dispatch_terminal_verdict_publish_failed") + else: + publish_status = "contract_failed" + publish_decision = "abort_linux_validation_terminal_dispatch_terminal_verdict_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_should_page_owner = True + publish_channel = "blocker" + reason_codes.append( + "linux_validation_terminal_dispatch_terminal_verdict_publish_upstream_contract_failed" + ) + + if ( + publish_status + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_STATUSES + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_status computed" + ) + if ( + publish_decision + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_DECISIONS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_decision computed" + ) + if ( + publish_channel + not in ALLOWED_LINUX_VALIDATION_TERMINAL_DISPATCH_TERMINAL_VERDICT_PUBLISH_CHANNELS + ): + raise ValueError( + "internal: unsupported linux_validation_terminal_dispatch_terminal_verdict_publish_channel computed" + ) + + reason_codes = _unique(reason_codes) + summary = ( + "linux_validation_terminal_dispatch_terminal_verdict_status=" + f"{verdict_status} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=" + f"{publish_status} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision=" + f"{publish_decision}" + ) + + payload = dict(terminal_verdict_report) + payload.update( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_dispatch_terminal_verdict_report": str( + source_path + ), + "linux_validation_terminal_dispatch_terminal_verdict_publish_status": publish_status, + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision": publish_decision, + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code": publish_exit_code, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify": publish_should_notify, + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner": publish_should_page_owner, + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel": publish_channel, + "linux_validation_terminal_dispatch_terminal_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + } + ) + return payload + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux Validation Terminal Dispatch Terminal Verdict Publish Report", + "", + ( + "- Terminal Verdict Publish Status: " + f"**{str(payload['linux_validation_terminal_dispatch_terminal_verdict_publish_status']).upper()}**" + ), + ( + "- Terminal Verdict Publish Decision: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_decision']}`" + ), + ( + "- Terminal Verdict Publish Exit Code: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code']}`" + ), + ( + "- Should Notify: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify']}`" + ), + ( + "- Requires Manual Action: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action']}`" + ), + ( + "- Should Page Owner: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner']}`" + ), + ( + "- Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_channel']}`" + ), + ( + "- Upstream Terminal Verdict Status: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_status']}`" + ), + ( + "- Upstream Terminal Verdict Channel: " + f"`{payload['linux_validation_terminal_dispatch_terminal_verdict_channel']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_dispatch_terminal_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_decision": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_decision"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner": ( + "true" + if payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] + else "false" + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_terminal_verdict_status": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_terminal_verdict_channel": str( + payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_run_url": str( + payload["release_run_url"] + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_report_json": str( + output_json + ), + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_report_markdown": str( + output_markdown + ), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux validation terminal dispatch terminal verdict publish contract " + "from P2-77 terminal verdict report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-dispatch-terminal-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json", + help="P2-77 Linux validation terminal dispatch terminal verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json", + help="Output Linux validation terminal dispatch terminal verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md", + help="Output Linux validation terminal dispatch terminal verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal dispatch terminal verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path( + args.linux_validation_terminal_dispatch_terminal_verdict_report + ) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_terminal_dispatch_terminal_verdict_report( + verdict_report_path + ) + payload = build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-dispatch-terminal-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text( + render_markdown_report(payload), + encoding="utf-8", + ) + print( + "linux validation terminal dispatch terminal verdict publish json: " + f"{output_json_path}" + ) + print( + "linux validation terminal dispatch terminal verdict publish markdown: " + f"{output_markdown_path}" + ) + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal dispatch terminal verdict publish summary: " + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_status']} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_decision']} " + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code=" + f"{payload['linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run_p2_lv_terminal_publish_gate.py b/scripts/run_p2_lv_terminal_publish_gate.py new file mode 100644 index 0000000..9b3af43 --- /dev/null +++ b/scripts/run_p2_lv_terminal_publish_gate.py @@ -0,0 +1,1043 @@ +"""Phase 2 card P2-63 gate for Linux CI workflow Linux-validation terminal publish. + +This script consumes the P2-62 Linux-validation verdict publish artifact and +converges one terminal publish contract for Linux validation: +1) validate Linux validation verdict publish + evidence consistency, +2) normalize terminal publish semantics for downstream handoff consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_passed", + "announce_linux_validation_passed_with_follow_up", + "announce_linux_validation_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_STATUSES: set[str] = { + "terminal_published", + "terminal_published_with_follow_up", + "terminal_blocked", + "terminal_contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_passed", + "announce_linux_validation_terminal_passed_with_follow_up", + "announce_linux_validation_terminal_blocker", + "abort_terminal_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_verdict_publish_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation verdict publish report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation verdict publish payload must be object") + + required_fields = ( + "source_linux_validation_verdict_report", + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_verdict_status", + "linux_validation_verdict_decision", + "linux_validation_verdict_exit_code", + "linux_validation_passed", + "linux_validation_verdict_requires_manual_action", + "linux_validation_verdict_channel", + "linux_validation_verdict_attempted", + "linux_validation_verdict_timeout_seconds", + "linux_validation_verdict_command", + "linux_validation_verdict_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_verdict_summary", + "linux_validation_verdict_publish_status", + "linux_validation_verdict_publish_decision", + "linux_validation_verdict_publish_exit_code", + "linux_validation_verdict_publish_should_notify", + "linux_validation_verdict_publish_requires_manual_action", + "linux_validation_verdict_publish_channel", + "linux_validation_verdict_publish_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_publish_status = _coerce_str( + payload["linux_validation_verdict_publish_status"], + field="linux_validation_verdict_publish_status", + path=path, + ) + if verdict_publish_status not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_verdict_publish_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_STATUSES)}" + ) + + verdict_publish_decision = _coerce_str( + payload["linux_validation_verdict_publish_decision"], + field="linux_validation_verdict_publish_decision", + path=path, + ) + if verdict_publish_decision not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_verdict_publish_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_DECISIONS)}" + ) + + verdict_publish_exit_code = _coerce_int( + payload["linux_validation_verdict_publish_exit_code"], + field="linux_validation_verdict_publish_exit_code", + path=path, + ) + if verdict_publish_exit_code < 0: + raise ValueError( + f"{path}: field 'linux_validation_verdict_publish_exit_code' must be >= 0" + ) + + verdict_publish_channel = _coerce_str( + payload["linux_validation_verdict_publish_channel"], + field="linux_validation_verdict_publish_channel", + path=path, + ) + if verdict_publish_channel not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_verdict_publish_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_verdict_report": _coerce_str( + payload["source_linux_validation_verdict_report"], + field="source_linux_validation_verdict_report", + path=path, + ), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_verdict_status": _coerce_str( + payload["linux_validation_verdict_status"], + field="linux_validation_verdict_status", + path=path, + ), + "linux_validation_verdict_decision": _coerce_str( + payload["linux_validation_verdict_decision"], + field="linux_validation_verdict_decision", + path=path, + ), + "linux_validation_verdict_exit_code": _coerce_int( + payload["linux_validation_verdict_exit_code"], + field="linux_validation_verdict_exit_code", + path=path, + ), + "linux_validation_passed": _coerce_bool( + payload["linux_validation_passed"], + field="linux_validation_passed", + path=path, + ), + "linux_validation_verdict_requires_manual_action": _coerce_bool( + payload["linux_validation_verdict_requires_manual_action"], + field="linux_validation_verdict_requires_manual_action", + path=path, + ), + "linux_validation_verdict_channel": _coerce_str( + payload["linux_validation_verdict_channel"], + field="linux_validation_verdict_channel", + path=path, + ), + "linux_validation_verdict_attempted": _coerce_bool( + payload["linux_validation_verdict_attempted"], + field="linux_validation_verdict_attempted", + path=path, + ), + "linux_validation_verdict_timeout_seconds": _coerce_int( + payload["linux_validation_verdict_timeout_seconds"], + field="linux_validation_verdict_timeout_seconds", + path=path, + ), + "linux_validation_verdict_command": _coerce_str( + payload["linux_validation_verdict_command"], + field="linux_validation_verdict_command", + path=path, + ), + "linux_validation_verdict_command_parts": _coerce_str_list( + payload["linux_validation_verdict_command_parts"], + field="linux_validation_verdict_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_verdict_summary": _coerce_str( + payload["linux_validation_verdict_summary"], + field="linux_validation_verdict_summary", + path=path, + ), + "linux_validation_verdict_publish_status": verdict_publish_status, + "linux_validation_verdict_publish_decision": verdict_publish_decision, + "linux_validation_verdict_publish_exit_code": verdict_publish_exit_code, + "linux_validation_verdict_publish_should_notify": _coerce_bool( + payload["linux_validation_verdict_publish_should_notify"], + field="linux_validation_verdict_publish_should_notify", + path=path, + ), + "linux_validation_verdict_publish_requires_manual_action": _coerce_bool( + payload["linux_validation_verdict_publish_requires_manual_action"], + field="linux_validation_verdict_publish_requires_manual_action", + path=path, + ), + "linux_validation_verdict_publish_channel": verdict_publish_channel, + "linux_validation_verdict_publish_summary": _coerce_str( + payload["linux_validation_verdict_publish_summary"], + field="linux_validation_verdict_publish_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_terminal_publish_payload( + linux_validation_verdict_publish_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_publish_status = str( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_status"] + ) + verdict_publish_decision = str( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_decision"] + ) + verdict_publish_exit_code = int( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_exit_code"] + ) + verdict_publish_should_notify = bool( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_should_notify"] + ) + verdict_publish_requires_manual_action = bool( + linux_validation_verdict_publish_report[ + "linux_validation_verdict_publish_requires_manual_action" + ] + ) + verdict_publish_channel = str( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_channel"] + ) + + reason_codes = list(linux_validation_verdict_publish_report["reason_codes"]) + structural_issues = list(linux_validation_verdict_publish_report["structural_issues"]) + missing_artifacts = list(linux_validation_verdict_publish_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_verdict_publish_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_terminal_publish_evidence_missing") + + expected_verdict_publish = { + "published": ( + "announce_linux_validation_passed", + 0, + True, + False, + "release", + ), + "published_with_follow_up": ( + "announce_linux_validation_passed_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "announce_linux_validation_blocker", + None, + True, + True, + "blocker", + ), + "contract_failed": ( + "abort_publish", + None, + True, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_notify, + expected_requires_manual_action, + expected_channel, + ) = expected_verdict_publish[verdict_publish_status] + + if verdict_publish_decision != expected_decision: + structural_issues.append("linux_validation_verdict_publish_decision_mismatch") + if verdict_publish_should_notify != expected_should_notify: + structural_issues.append("linux_validation_verdict_publish_should_notify_mismatch") + if verdict_publish_requires_manual_action != expected_requires_manual_action: + structural_issues.append( + "linux_validation_verdict_publish_requires_manual_action_mismatch" + ) + if verdict_publish_channel != expected_channel: + structural_issues.append("linux_validation_verdict_publish_channel_mismatch") + if expected_exit_code == 0 and verdict_publish_exit_code != 0: + structural_issues.append("linux_validation_verdict_publish_exit_code_mismatch_published") + if ( + expected_exit_code is None + and verdict_publish_exit_code == 0 + and verdict_publish_status != "published" + ): + structural_issues.append( + "linux_validation_verdict_publish_exit_code_mismatch_non_published" + ) + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + terminal_publish_status: str + terminal_publish_decision: str + terminal_publish_exit_code: int + terminal_publish_should_notify: bool + terminal_publish_ready_for_handoff: bool + terminal_publish_requires_manual_action: bool + terminal_publish_channel: str + + if structural_issues or missing_artifacts: + terminal_publish_status = "terminal_contract_failed" + terminal_publish_decision = "abort_terminal_publish" + terminal_publish_exit_code = max(1, verdict_publish_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_publish_status == "published": + terminal_publish_status = "terminal_published" + terminal_publish_decision = "announce_linux_validation_terminal_passed" + terminal_publish_exit_code = 0 + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = True + terminal_publish_requires_manual_action = False + terminal_publish_channel = "release" + reason_codes = ["linux_validation_terminal_publish_terminal_published"] + elif verdict_publish_status == "published_with_follow_up": + terminal_publish_status = "terminal_published_with_follow_up" + terminal_publish_decision = "announce_linux_validation_terminal_passed_with_follow_up" + terminal_publish_exit_code = max(1, verdict_publish_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = True + terminal_publish_requires_manual_action = False + terminal_publish_channel = "follow_up" + reason_codes.append("linux_validation_terminal_publish_terminal_published_with_follow_up") + elif verdict_publish_status == "blocked": + terminal_publish_status = "terminal_blocked" + terminal_publish_decision = "announce_linux_validation_terminal_blocker" + terminal_publish_exit_code = max(1, verdict_publish_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_publish_terminal_blocked") + else: + terminal_publish_status = "terminal_contract_failed" + terminal_publish_decision = "abort_terminal_publish" + terminal_publish_exit_code = max(1, verdict_publish_exit_code) + terminal_publish_should_notify = True + terminal_publish_ready_for_handoff = False + terminal_publish_requires_manual_action = True + terminal_publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_publish_upstream_contract_failed") + + if terminal_publish_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_STATUSES: + raise ValueError("internal: unsupported linux_validation_terminal_publish_status computed") + if terminal_publish_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_terminal_publish_decision computed") + if terminal_publish_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_terminal_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_verdict_publish_status={verdict_publish_status} " + f"linux_validation_terminal_publish_status={terminal_publish_status} " + f"linux_validation_terminal_publish_decision={terminal_publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_verdict_publish_report": str(source_path), + "source_linux_validation_verdict_report": str( + linux_validation_verdict_publish_report["source_linux_validation_verdict_report"] + ), + "source_linux_validation_dispatch_report": str( + linux_validation_verdict_publish_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(linux_validation_verdict_publish_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_verdict_publish_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_verdict_publish_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_verdict_publish_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_verdict_publish_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_verdict_publish_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_verdict_publish_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_verdict_publish_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_verdict_publish_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str( + linux_validation_verdict_publish_report["terminal_verdict_status"] + ), + "terminal_verdict_decision": str( + linux_validation_verdict_publish_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_verdict_publish_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_verdict_publish_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_verdict_publish_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str( + linux_validation_verdict_publish_report["terminal_verdict_channel"] + ), + "linux_validation_should_dispatch": bool( + linux_validation_verdict_publish_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_verdict_publish_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str( + linux_validation_verdict_publish_report["linux_validation_channel"] + ), + "linux_validation_dispatch_status": str( + linux_validation_verdict_publish_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_verdict_publish_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_verdict_publish_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_verdict_publish_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_verdict_publish_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_verdict_status": str( + linux_validation_verdict_publish_report["linux_validation_verdict_status"] + ), + "linux_validation_verdict_decision": str( + linux_validation_verdict_publish_report["linux_validation_verdict_decision"] + ), + "linux_validation_verdict_exit_code": int( + linux_validation_verdict_publish_report["linux_validation_verdict_exit_code"] + ), + "linux_validation_passed": bool( + linux_validation_verdict_publish_report["linux_validation_passed"] + ), + "linux_validation_verdict_requires_manual_action": bool( + linux_validation_verdict_publish_report[ + "linux_validation_verdict_requires_manual_action" + ] + ), + "linux_validation_verdict_channel": str( + linux_validation_verdict_publish_report["linux_validation_verdict_channel"] + ), + "linux_validation_verdict_attempted": bool( + linux_validation_verdict_publish_report["linux_validation_verdict_attempted"] + ), + "linux_validation_verdict_timeout_seconds": int( + linux_validation_verdict_publish_report["linux_validation_verdict_timeout_seconds"] + ), + "linux_validation_verdict_command": str( + linux_validation_verdict_publish_report["linux_validation_verdict_command"] + ), + "linux_validation_verdict_command_parts": list( + linux_validation_verdict_publish_report["linux_validation_verdict_command_parts"] + ), + "dry_run": bool(linux_validation_verdict_publish_report["dry_run"]), + "verdict_command_returncode": linux_validation_verdict_publish_report[ + "verdict_command_returncode" + ], + "verdict_command_stdout_tail": str( + linux_validation_verdict_publish_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_verdict_publish_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str( + linux_validation_verdict_publish_report["verdict_error_type"] + ), + "verdict_error_message": str( + linux_validation_verdict_publish_report["verdict_error_message"] + ), + "release_run_id": linux_validation_verdict_publish_report["release_run_id"], + "release_run_url": str(linux_validation_verdict_publish_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_verdict_publish_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_verdict_publish_report["linux_validation_dispatch_summary"] + ), + "linux_validation_verdict_summary": str( + linux_validation_verdict_publish_report["linux_validation_verdict_summary"] + ), + "linux_validation_verdict_publish_status": verdict_publish_status, + "linux_validation_verdict_publish_decision": verdict_publish_decision, + "linux_validation_verdict_publish_exit_code": verdict_publish_exit_code, + "linux_validation_verdict_publish_should_notify": verdict_publish_should_notify, + "linux_validation_verdict_publish_requires_manual_action": verdict_publish_requires_manual_action, + "linux_validation_verdict_publish_channel": verdict_publish_channel, + "linux_validation_verdict_publish_summary": str( + linux_validation_verdict_publish_report["linux_validation_verdict_publish_summary"] + ), + "linux_validation_terminal_publish_status": terminal_publish_status, + "linux_validation_terminal_publish_decision": terminal_publish_decision, + "linux_validation_terminal_publish_exit_code": terminal_publish_exit_code, + "linux_validation_terminal_publish_should_notify": terminal_publish_should_notify, + "linux_validation_terminal_publish_ready_for_handoff": terminal_publish_ready_for_handoff, + "linux_validation_terminal_publish_requires_manual_action": terminal_publish_requires_manual_action, + "linux_validation_terminal_publish_channel": terminal_publish_channel, + "linux_validation_terminal_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Publish Report", + "", + ( + "- Linux Validation Terminal Publish Status: " + f"**{str(payload['linux_validation_terminal_publish_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Publish Decision: " + f"`{payload['linux_validation_terminal_publish_decision']}`" + ), + ( + "- Linux Validation Terminal Publish Exit Code: " + f"`{payload['linux_validation_terminal_publish_exit_code']}`" + ), + ( + "- Linux Validation Terminal Publish Should Notify: " + f"`{payload['linux_validation_terminal_publish_should_notify']}`" + ), + ( + "- Linux Validation Terminal Publish Ready For Handoff: " + f"`{payload['linux_validation_terminal_publish_ready_for_handoff']}`" + ), + ( + "- Linux Validation Terminal Publish Requires Manual Action: " + f"`{payload['linux_validation_terminal_publish_requires_manual_action']}`" + ), + ( + "- Linux Validation Terminal Publish Channel: " + f"`{payload['linux_validation_terminal_publish_channel']}`" + ), + ( + "- Linux Validation Verdict Publish Status: " + f"`{payload['linux_validation_verdict_publish_status']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_publish_status": str( + payload["linux_validation_terminal_publish_status"] + ), + "workflow_linux_validation_terminal_publish_decision": str( + payload["linux_validation_terminal_publish_decision"] + ), + "workflow_linux_validation_terminal_publish_exit_code": str( + payload["linux_validation_terminal_publish_exit_code"] + ), + "workflow_linux_validation_terminal_publish_should_notify": ( + "true" if payload["linux_validation_terminal_publish_should_notify"] else "false" + ), + "workflow_linux_validation_terminal_publish_ready_for_handoff": ( + "true" + if payload["linux_validation_terminal_publish_ready_for_handoff"] + else "false" + ), + "workflow_linux_validation_terminal_publish_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_publish_channel": str( + payload["linux_validation_terminal_publish_channel"] + ), + "workflow_linux_validation_terminal_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_publish_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_terminal_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_publish_report_json": str(output_json), + "workflow_linux_validation_terminal_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation terminal publish contract " + "from P2-62 Linux validation verdict publish report" + ) + ) + parser.add_argument( + "--linux-validation-verdict-publish-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json", + help="P2-62 Linux validation verdict publish report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json", + help="Output Linux validation terminal publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md", + help="Output Linux validation terminal publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_publish_report_path = Path(args.linux_validation_verdict_publish_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_publish_report = load_linux_validation_verdict_publish_report( + verdict_publish_report_path + ) + payload = build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal publish json: {output_json_path}") + print(f"linux validation terminal publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal publish summary: " + f"linux_validation_terminal_publish_status=" + f"{payload['linux_validation_terminal_publish_status']} " + f"linux_validation_terminal_publish_decision=" + f"{payload['linux_validation_terminal_publish_decision']} " + f"linux_validation_terminal_publish_exit_code=" + f"{payload['linux_validation_terminal_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_terminal_verdict_gate.py b/scripts/run_p2_lv_terminal_verdict_gate.py new file mode 100644 index 0000000..06430e0 --- /dev/null +++ b/scripts/run_p2_lv_terminal_verdict_gate.py @@ -0,0 +1,927 @@ +"""Phase 2 card P2-67 gate for Linux CI workflow Linux-validation terminal verdict. + +This script consumes the P2-66 Linux-validation final publish archive artifact and converges +one terminal verdict contract for Linux validation: +1) validate Linux validation final publish archive + evidence consistency, +2) normalize terminal verdict semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_STATUSES: set[str] = { + "archived", + "archived_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_DECISIONS: set[str] = { + "archive_release_shipped", + "archive_release_shipped_with_follow_up", + "archive_release_blocker", + "abort_archive", +} +ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation", + "ready_with_follow_up_for_linux_validation", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation", + "proceed_linux_validation_with_follow_up", + "halt_linux_validation_blocker", + "abort_linux_validation", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_final_publish_archive_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation final publish archive report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation final publish archive payload must be object") + + required_fields = ( + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_final_publish_archive_status", + "linux_validation_final_publish_archive_decision", + "linux_validation_final_publish_archive_exit_code", + "linux_validation_final_publish_archive_should_archive", + "linux_validation_final_publish_archive_requires_manual_action", + "linux_validation_final_publish_archive_channel", + "linux_validation_final_publish_archive_attempted", + "linux_validation_final_publish_archive_timeout_seconds", + "linux_validation_final_publish_archive_command", + "linux_validation_final_publish_archive_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_final_publish_archive_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_final_publish_archive_status"], + field="linux_validation_final_publish_archive_status", + path=path, + ) + if verdict_status not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_final_publish_archive_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_final_publish_archive_decision"], + field="linux_validation_final_publish_archive_decision", + path=path, + ) + if verdict_decision not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_final_publish_archive_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_final_publish_archive_exit_code"], + field="linux_validation_final_publish_archive_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_final_publish_archive_exit_code' must be >= 0") + + verdict_channel = _coerce_str( + payload["linux_validation_final_publish_archive_channel"], + field="linux_validation_final_publish_archive_channel", + path=path, + ) + if verdict_channel not in ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_final_publish_archive_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_FINAL_PUBLISH_ARCHIVE_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_final_publish_archive_status": verdict_status, + "linux_validation_final_publish_archive_decision": verdict_decision, + "linux_validation_final_publish_archive_exit_code": verdict_exit_code, + "linux_validation_final_publish_archive_should_archive": _coerce_bool( + payload["linux_validation_final_publish_archive_should_archive"], + field="linux_validation_final_publish_archive_should_archive", + path=path, + ), + "linux_validation_final_publish_archive_requires_manual_action": _coerce_bool( + payload["linux_validation_final_publish_archive_requires_manual_action"], + field="linux_validation_final_publish_archive_requires_manual_action", + path=path, + ), + "linux_validation_final_publish_archive_channel": verdict_channel, + "linux_validation_final_publish_archive_attempted": _coerce_bool( + payload["linux_validation_final_publish_archive_attempted"], + field="linux_validation_final_publish_archive_attempted", + path=path, + ), + "linux_validation_final_publish_archive_timeout_seconds": _coerce_int( + payload["linux_validation_final_publish_archive_timeout_seconds"], + field="linux_validation_final_publish_archive_timeout_seconds", + path=path, + ), + "linux_validation_final_publish_archive_command": _coerce_str( + payload["linux_validation_final_publish_archive_command"], + field="linux_validation_final_publish_archive_command", + path=path, + ), + "linux_validation_final_publish_archive_command_parts": _coerce_str_list( + payload["linux_validation_final_publish_archive_command_parts"], + field="linux_validation_final_publish_archive_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_final_publish_archive_summary": _coerce_str( + payload["linux_validation_final_publish_archive_summary"], + field="linux_validation_final_publish_archive_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_terminal_verdict_payload( + linux_validation_final_publish_archive_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_status"] + ) + verdict_decision = str( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_decision"] + ) + verdict_exit_code = int( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_exit_code"] + ) + verdict_should_archive = bool( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_should_archive"] + ) + verdict_requires_manual_action = bool( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_requires_manual_action"] + ) + verdict_channel = str( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_channel"] + ) + + reason_codes = list(linux_validation_final_publish_archive_report["reason_codes"]) + structural_issues = list(linux_validation_final_publish_archive_report["structural_issues"]) + missing_artifacts = list(linux_validation_final_publish_archive_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_final_publish_archive_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_terminal_verdict_evidence_missing") + + expected_archive = { + "archived": ( + "archive_release_shipped", + 0, + True, + False, + "release", + ), + "archived_with_follow_up": ( + "archive_release_shipped_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "archive_release_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_archive", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_archive, + expected_requires_manual_action, + expected_channel, + ) = expected_archive[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append("linux_validation_final_publish_archive_decision_mismatch") + if verdict_should_archive != expected_should_archive: + structural_issues.append("linux_validation_final_publish_archive_should_archive_mismatch") + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append("linux_validation_final_publish_archive_requires_manual_action_mismatch") + if verdict_channel != expected_channel: + structural_issues.append("linux_validation_final_publish_archive_channel_mismatch") + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append("linux_validation_final_publish_archive_exit_code_mismatch_archived") + if expected_exit_code is None and verdict_exit_code == 0 and verdict_status != "archived": + structural_issues.append("linux_validation_final_publish_archive_exit_code_mismatch_non_archived") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + terminal_status: str + terminal_decision: str + terminal_exit_code: int + terminal_should_proceed: bool + terminal_requires_manual_action: bool + terminal_channel: str + + if structural_issues or missing_artifacts: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation" + terminal_exit_code = max(1, verdict_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "archived": + terminal_status = "ready_for_linux_validation" + terminal_decision = "proceed_linux_validation" + terminal_exit_code = 0 + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_channel = "release" + reason_codes = ["linux_validation_terminal_verdict_ready_for_linux_validation"] + elif verdict_status == "archived_with_follow_up": + terminal_status = "ready_with_follow_up_for_linux_validation" + terminal_decision = "proceed_linux_validation_with_follow_up" + terminal_exit_code = max(1, verdict_exit_code) + terminal_should_proceed = True + terminal_requires_manual_action = False + terminal_channel = "follow_up" + reason_codes.append("linux_validation_terminal_verdict_ready_with_follow_up_for_linux_validation") + elif verdict_status == "blocked": + terminal_status = "blocked" + terminal_decision = "halt_linux_validation_blocker" + terminal_exit_code = max(1, verdict_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("linux_validation_terminal_verdict_blocked") + else: + terminal_status = "contract_failed" + terminal_decision = "abort_linux_validation" + terminal_exit_code = max(1, verdict_exit_code) + terminal_should_proceed = False + terminal_requires_manual_action = True + terminal_channel = "blocker" + reason_codes.append("linux_validation_terminal_verdict_upstream_contract_failed") + + if terminal_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_STATUSES: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_status computed") + if terminal_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_DECISIONS: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_decision computed") + if terminal_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_CHANNELS: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_final_publish_archive_status={verdict_status} " + f"linux_validation_terminal_verdict_status={terminal_status} " + f"linux_validation_terminal_verdict_decision={terminal_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_final_publish_archive_report": str(source_path), + "source_linux_validation_dispatch_report": str( + linux_validation_final_publish_archive_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(linux_validation_final_publish_archive_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_final_publish_archive_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_final_publish_archive_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_final_publish_archive_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_final_publish_archive_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_final_publish_archive_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_final_publish_archive_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_final_publish_archive_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_final_publish_archive_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(linux_validation_final_publish_archive_report["terminal_verdict_status"]), + "terminal_verdict_decision": str( + linux_validation_final_publish_archive_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_final_publish_archive_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_final_publish_archive_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_final_publish_archive_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(linux_validation_final_publish_archive_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + linux_validation_final_publish_archive_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_final_publish_archive_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(linux_validation_final_publish_archive_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + linux_validation_final_publish_archive_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_final_publish_archive_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_final_publish_archive_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_final_publish_archive_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_final_publish_archive_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_final_publish_archive_status": verdict_status, + "linux_validation_final_publish_archive_decision": verdict_decision, + "linux_validation_final_publish_archive_exit_code": verdict_exit_code, + "linux_validation_final_publish_archive_should_archive": verdict_should_archive, + "linux_validation_final_publish_archive_requires_manual_action": verdict_requires_manual_action, + "linux_validation_final_publish_archive_channel": verdict_channel, + "linux_validation_final_publish_archive_attempted": bool( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_attempted"] + ), + "linux_validation_final_publish_archive_timeout_seconds": int( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_timeout_seconds"] + ), + "linux_validation_final_publish_archive_command": str( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_command"] + ), + "linux_validation_final_publish_archive_command_parts": list( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_command_parts"] + ), + "dry_run": bool(linux_validation_final_publish_archive_report["dry_run"]), + "verdict_command_returncode": linux_validation_final_publish_archive_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str( + linux_validation_final_publish_archive_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_final_publish_archive_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str(linux_validation_final_publish_archive_report["verdict_error_type"]), + "verdict_error_message": str(linux_validation_final_publish_archive_report["verdict_error_message"]), + "release_run_id": linux_validation_final_publish_archive_report["release_run_id"], + "release_run_url": str(linux_validation_final_publish_archive_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_final_publish_archive_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_final_publish_archive_report["linux_validation_dispatch_summary"] + ), + "linux_validation_final_publish_archive_summary": str( + linux_validation_final_publish_archive_report["linux_validation_final_publish_archive_summary"] + ), + "linux_validation_terminal_verdict_status": terminal_status, + "linux_validation_terminal_verdict_decision": terminal_decision, + "linux_validation_terminal_verdict_exit_code": terminal_exit_code, + "linux_validation_terminal_verdict_should_proceed": terminal_should_proceed, + "linux_validation_terminal_verdict_requires_manual_action": terminal_requires_manual_action, + "linux_validation_terminal_verdict_channel": terminal_channel, + "linux_validation_terminal_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Verdict Report", + "", + ( + "- Linux Validation Terminal Verdict Status: " + f"**{str(payload['linux_validation_terminal_verdict_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Verdict Decision: " + f"`{payload['linux_validation_terminal_verdict_decision']}`" + ), + ( + "- Linux Validation Terminal Verdict Exit Code: " + f"`{payload['linux_validation_terminal_verdict_exit_code']}`" + ), + ( + "- Linux Validation Terminal Verdict Should Proceed: " + f"`{payload['linux_validation_terminal_verdict_should_proceed']}`" + ), + ( + "- Linux Validation Terminal Verdict Requires Manual Action: " + f"`{payload['linux_validation_terminal_verdict_requires_manual_action']}`" + ), + ( + "- Linux Validation Terminal Verdict Channel: " + f"`{payload['linux_validation_terminal_verdict_channel']}`" + ), + ( + "- Linux Validation Final Publish Archive Status: " + f"`{payload['linux_validation_final_publish_archive_status']}`" + ), + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_verdict_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_verdict_status": str( + payload["linux_validation_terminal_verdict_status"] + ), + "workflow_linux_validation_terminal_verdict_decision": str( + payload["linux_validation_terminal_verdict_decision"] + ), + "workflow_linux_validation_terminal_verdict_exit_code": str( + payload["linux_validation_terminal_verdict_exit_code"] + ), + "workflow_linux_validation_terminal_verdict_should_proceed": ( + "true" if payload["linux_validation_terminal_verdict_should_proceed"] else "false" + ), + "workflow_linux_validation_terminal_verdict_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_verdict_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_verdict_channel": str( + payload["linux_validation_terminal_verdict_channel"] + ), + "workflow_linux_validation_terminal_verdict_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_verdict_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_verdict_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_terminal_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_verdict_report_json": str(output_json), + "workflow_linux_validation_terminal_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation terminal verdict contract " + "from P2-66 Linux validation final publish archive report" + ) + ) + parser.add_argument( + "--linux-validation-final-publish-archive-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json", + help="P2-66 Linux validation final publish archive report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json", + help="Output Linux validation terminal verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md", + help="Output Linux validation terminal verdict markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path(args.linux_validation_final_publish_archive_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_final_publish_archive_report(verdict_report_path) + payload = build_linux_validation_terminal_verdict_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-verdict-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal verdict json: {output_json_path}") + print(f"linux validation terminal verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal verdict summary: " + f"linux_validation_terminal_verdict_status=" + f"{payload['linux_validation_terminal_verdict_status']} " + f"linux_validation_terminal_verdict_decision=" + f"{payload['linux_validation_terminal_verdict_decision']} " + f"linux_validation_terminal_verdict_exit_code=" + f"{payload['linux_validation_terminal_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_terminal_verdict_publish_gate.py b/scripts/run_p2_lv_terminal_verdict_publish_gate.py new file mode 100644 index 0000000..46fac1d --- /dev/null +++ b/scripts/run_p2_lv_terminal_verdict_publish_gate.py @@ -0,0 +1,889 @@ +"""Phase 2 card P2-68 gate for Linux CI workflow Linux-validation terminal verdict publish. + +This script consumes the P2-67 Linux-validation terminal verdict artifact and converges +one terminal verdict publish contract for Linux validation: +1) validate Linux validation terminal verdict + evidence consistency, +2) normalize publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation", + "ready_with_follow_up_for_linux_validation", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation", + "proceed_linux_validation_with_follow_up", + "halt_linux_validation_blocker", + "abort_linux_validation", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_terminal_ready", + "announce_linux_validation_terminal_ready_with_follow_up", + "announce_linux_validation_terminal_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_terminal_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation terminal verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation terminal verdict payload must be object") + + required_fields = ( + "source_linux_validation_dispatch_report", + "source_linux_validation_final_publish_archive_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_terminal_verdict_status", + "linux_validation_terminal_verdict_decision", + "linux_validation_terminal_verdict_exit_code", + "linux_validation_terminal_verdict_should_proceed", + "linux_validation_terminal_verdict_requires_manual_action", + "linux_validation_terminal_verdict_channel", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_terminal_verdict_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_terminal_verdict_status"], + field="linux_validation_terminal_verdict_status", + path=path, + ) + if verdict_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_terminal_verdict_decision"], + field="linux_validation_terminal_verdict_decision", + path=path, + ) + if verdict_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_terminal_verdict_exit_code"], + field="linux_validation_terminal_verdict_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_terminal_verdict_exit_code' must be >= 0") + + verdict_channel = _coerce_str( + payload["linux_validation_terminal_verdict_channel"], + field="linux_validation_terminal_verdict_channel", + path=path, + ) + if verdict_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_terminal_verdict_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "source_linux_validation_final_publish_archive_report": _coerce_str( + payload["source_linux_validation_final_publish_archive_report"], + field="source_linux_validation_final_publish_archive_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_terminal_verdict_status": verdict_status, + "linux_validation_terminal_verdict_decision": verdict_decision, + "linux_validation_terminal_verdict_exit_code": verdict_exit_code, + "linux_validation_terminal_verdict_should_proceed": _coerce_bool( + payload["linux_validation_terminal_verdict_should_proceed"], + field="linux_validation_terminal_verdict_should_proceed", + path=path, + ), + "linux_validation_terminal_verdict_requires_manual_action": _coerce_bool( + payload["linux_validation_terminal_verdict_requires_manual_action"], + field="linux_validation_terminal_verdict_requires_manual_action", + path=path, + ), + "linux_validation_terminal_verdict_channel": verdict_channel, + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_terminal_verdict_summary": _coerce_str( + payload["linux_validation_terminal_verdict_summary"], + field="linux_validation_terminal_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_terminal_verdict_publish_payload( + linux_validation_terminal_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str(linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_status"]) + verdict_decision = str(linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_decision"]) + verdict_exit_code = int(linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_exit_code"]) + verdict_should_proceed = bool( + linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_should_proceed"] + ) + verdict_requires_manual_action = bool( + linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_requires_manual_action"] + ) + verdict_channel = str(linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_channel"]) + + reason_codes = list(linux_validation_terminal_verdict_report["reason_codes"]) + structural_issues = list(linux_validation_terminal_verdict_report["structural_issues"]) + missing_artifacts = list(linux_validation_terminal_verdict_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_terminal_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_terminal_verdict_publish_evidence_missing") + + expected_verdict = { + "ready_for_linux_validation": ( + "proceed_linux_validation", + 0, + True, + False, + "release", + ), + "ready_with_follow_up_for_linux_validation": ( + "proceed_linux_validation_with_follow_up", + None, + True, + False, + "follow_up", + ), + "blocked": ( + "halt_linux_validation_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_should_proceed, + expected_requires_manual_action, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append("linux_validation_terminal_verdict_decision_mismatch") + if verdict_should_proceed != expected_should_proceed: + structural_issues.append("linux_validation_terminal_verdict_should_proceed_mismatch") + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append("linux_validation_terminal_verdict_requires_manual_action_mismatch") + if verdict_channel != expected_channel: + structural_issues.append("linux_validation_terminal_verdict_channel_mismatch") + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append("linux_validation_terminal_verdict_exit_code_mismatch_ready_for_linux_validation") + if expected_exit_code is None and verdict_exit_code == 0 and verdict_status != "ready_for_linux_validation": + structural_issues.append("linux_validation_terminal_verdict_exit_code_mismatch_non_ready_for_linux_validation") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "ready_for_linux_validation": + publish_status = "published" + publish_decision = "announce_linux_validation_terminal_ready" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["linux_validation_terminal_verdict_publish_published"] + elif verdict_status == "ready_with_follow_up_for_linux_validation": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_terminal_ready_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("linux_validation_terminal_verdict_publish_with_follow_up") + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_terminal_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_verdict_publish_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_terminal_verdict_publish_upstream_contract_failed") + + if publish_status not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_STATUSES: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_publish_status computed") + if publish_decision not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_publish_decision computed") + if publish_channel not in ALLOWED_LINUX_VALIDATION_TERMINAL_VERDICT_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_terminal_verdict_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_terminal_verdict_status={verdict_status} " + f"linux_validation_terminal_verdict_publish_status={publish_status} " + f"linux_validation_terminal_verdict_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_terminal_verdict_report": str(source_path), + "source_linux_validation_dispatch_report": str( + linux_validation_terminal_verdict_report["source_linux_validation_dispatch_report"] + ), + "source_linux_validation_final_publish_archive_report": str( + linux_validation_terminal_verdict_report["source_linux_validation_final_publish_archive_report"] + ), + "project_root": str(linux_validation_terminal_verdict_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_terminal_verdict_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_terminal_verdict_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_terminal_verdict_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_terminal_verdict_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_terminal_verdict_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_terminal_verdict_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_terminal_verdict_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_terminal_verdict_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(linux_validation_terminal_verdict_report["terminal_verdict_status"]), + "terminal_verdict_decision": str( + linux_validation_terminal_verdict_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_terminal_verdict_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_terminal_verdict_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_terminal_verdict_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(linux_validation_terminal_verdict_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + linux_validation_terminal_verdict_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_terminal_verdict_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(linux_validation_terminal_verdict_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + linux_validation_terminal_verdict_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_terminal_verdict_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_terminal_verdict_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_terminal_verdict_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_terminal_verdict_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_terminal_verdict_status": verdict_status, + "linux_validation_terminal_verdict_decision": verdict_decision, + "linux_validation_terminal_verdict_exit_code": verdict_exit_code, + "linux_validation_terminal_verdict_should_proceed": verdict_should_proceed, + "linux_validation_terminal_verdict_requires_manual_action": verdict_requires_manual_action, + "linux_validation_terminal_verdict_channel": verdict_channel, + "dry_run": bool(linux_validation_terminal_verdict_report["dry_run"]), + "verdict_command_returncode": linux_validation_terminal_verdict_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str( + linux_validation_terminal_verdict_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_terminal_verdict_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str(linux_validation_terminal_verdict_report["verdict_error_type"]), + "verdict_error_message": str(linux_validation_terminal_verdict_report["verdict_error_message"]), + "release_run_id": linux_validation_terminal_verdict_report["release_run_id"], + "release_run_url": str(linux_validation_terminal_verdict_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_terminal_verdict_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_terminal_verdict_report["linux_validation_dispatch_summary"] + ), + "linux_validation_terminal_verdict_summary": str( + linux_validation_terminal_verdict_report["linux_validation_terminal_verdict_summary"] + ), + "linux_validation_terminal_verdict_publish_status": publish_status, + "linux_validation_terminal_verdict_publish_decision": publish_decision, + "linux_validation_terminal_verdict_publish_exit_code": publish_exit_code, + "linux_validation_terminal_verdict_publish_should_notify": publish_should_notify, + "linux_validation_terminal_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_terminal_verdict_publish_channel": publish_channel, + "linux_validation_terminal_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Terminal Verdict Publish Report", + "", + ( + "- Linux Validation Terminal Verdict Publish Status: " + f"**{str(payload['linux_validation_terminal_verdict_publish_status']).upper()}**" + ), + ( + "- Linux Validation Terminal Verdict Publish Decision: " + f"`{payload['linux_validation_terminal_verdict_publish_decision']}`" + ), + ( + "- Linux Validation Terminal Verdict Publish Exit Code: " + f"`{payload['linux_validation_terminal_verdict_publish_exit_code']}`" + ), + ( + "- Linux Validation Terminal Verdict Publish Should Notify: " + f"`{payload['linux_validation_terminal_verdict_publish_should_notify']}`" + ), + ( + "- Linux Validation Terminal Verdict Publish Requires Manual Action: " + f"`{payload['linux_validation_terminal_verdict_publish_requires_manual_action']}`" + ), + ( + "- Linux Validation Terminal Verdict Publish Channel: " + f"`{payload['linux_validation_terminal_verdict_publish_channel']}`" + ), + f"- Linux Validation Terminal Verdict Status: `{payload['linux_validation_terminal_verdict_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_terminal_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_terminal_verdict_publish_status": str( + payload["linux_validation_terminal_verdict_publish_status"] + ), + "workflow_linux_validation_terminal_verdict_publish_decision": str( + payload["linux_validation_terminal_verdict_publish_decision"] + ), + "workflow_linux_validation_terminal_verdict_publish_exit_code": str( + payload["linux_validation_terminal_verdict_publish_exit_code"] + ), + "workflow_linux_validation_terminal_verdict_publish_should_notify": ( + "true" if payload["linux_validation_terminal_verdict_publish_should_notify"] else "false" + ), + "workflow_linux_validation_terminal_verdict_publish_requires_manual_action": ( + "true" + if payload["linux_validation_terminal_verdict_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_terminal_verdict_publish_channel": str( + payload["linux_validation_terminal_verdict_publish_channel"] + ), + "workflow_linux_validation_terminal_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_terminal_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_terminal_verdict_publish_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_terminal_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_terminal_verdict_publish_report_json": str(output_json), + "workflow_linux_validation_terminal_verdict_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation terminal verdict publish contract " + "from P2-67 Linux validation terminal verdict report" + ) + ) + parser.add_argument( + "--linux-validation-terminal-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json", + help="P2-67 Linux validation terminal verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json", + help="Output Linux validation terminal verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md", + help="Output Linux validation terminal verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation terminal verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path(args.linux_validation_terminal_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_terminal_verdict_report(verdict_report_path) + payload = build_linux_validation_terminal_verdict_publish_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-terminal-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation terminal verdict publish json: {output_json_path}") + print(f"linux validation terminal verdict publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation terminal verdict publish summary: " + f"linux_validation_terminal_verdict_publish_status=" + f"{payload['linux_validation_terminal_verdict_publish_status']} " + f"linux_validation_terminal_verdict_publish_decision=" + f"{payload['linux_validation_terminal_verdict_publish_decision']} " + f"linux_validation_terminal_verdict_publish_exit_code=" + f"{payload['linux_validation_terminal_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_terminal_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) + + + + diff --git a/scripts/run_p2_lv_verdict_gate.py b/scripts/run_p2_lv_verdict_gate.py new file mode 100644 index 0000000..60a3b56 --- /dev/null +++ b/scripts/run_p2_lv_verdict_gate.py @@ -0,0 +1,1188 @@ +"""Phase 2 card P2-61 gate for Linux CI workflow Linux-validation verdict. + +This script consumes the P2-60 Linux validation dispatch artifact and converges +one final Linux validation verdict contract: +1) validate dispatch + terminal-verdict + manifest-drift consistency, +2) optionally execute an additional verdict command when dispatch succeeded, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_TERMINAL_VERDICT_STATUSES: set[str] = { + "ready_for_linux_validation", + "ready_with_follow_up_for_linux_validation", + "blocked", + "contract_failed", +} +ALLOWED_TERMINAL_VERDICT_DECISIONS: set[str] = { + "proceed_linux_validation", + "proceed_linux_validation_with_follow_up", + "halt_linux_validation_blocker", + "abort_linux_validation", +} +ALLOWED_TERMINAL_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} +ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: set[str] = {"passed", "failed"} + +ALLOWED_LINUX_VALIDATION_DISPATCH_STATUSES: set[str] = { + "ready_dry_run", + "ready_with_follow_up_dry_run", + "dispatched", + "dispatch_failed", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_DISPATCH_DECISIONS: set[str] = { + "dispatch_linux_validation", + "dispatch_linux_validation_with_follow_up", + "hold_linux_validation_blocker", + "abort_linux_validation_dispatch", +} +ALLOWED_LINUX_VALIDATION_DISPATCH_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_LINUX_VALIDATION_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "validation_failed", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation", + "accept_linux_validation_with_follow_up", + "escalate_linux_validation_failure", + "hold_linux_validation_blocker", + "abort_linux_validation_verdict", +} +ALLOWED_LINUX_VALIDATION_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _tail_text(text: str, *, max_lines: int = 20) -> str: + lines = text.splitlines() + if len(lines) <= max_lines: + return text + return "\n".join(lines[-max_lines:]) + + +def _format_shell_command(parts: list[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_dispatch_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation dispatch report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation dispatch payload must be object") + + required_fields = ( + "source_terminal_verdict_report", + "project_root", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_timeout_seconds", + "linux_validation_command", + "linux_validation_command_parts", + "dry_run", + "command_returncode", + "command_stdout_tail", + "command_stderr_tail", + "dispatch_error_type", + "dispatch_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + terminal_status = _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ) + if terminal_status not in ALLOWED_TERMINAL_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'terminal_verdict_status' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_STATUSES)}" + ) + + terminal_decision = _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ) + if terminal_decision not in ALLOWED_TERMINAL_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'terminal_verdict_decision' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_DECISIONS)}" + ) + + terminal_channel = _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ) + if terminal_channel not in ALLOWED_TERMINAL_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'terminal_verdict_channel' must be one of " + f"{sorted(ALLOWED_TERMINAL_VERDICT_CHANNELS)}" + ) + + drift_status = _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ) + if drift_status not in ALLOWED_GATE_MANIFEST_DRIFT_STATUSES: + raise ValueError( + f"{path}: field 'gate_manifest_drift_status' must be one of " + f"{sorted(ALLOWED_GATE_MANIFEST_DRIFT_STATUSES)}" + ) + + dispatch_status = _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ) + if dispatch_status not in ALLOWED_LINUX_VALIDATION_DISPATCH_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_dispatch_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_DISPATCH_STATUSES)}" + ) + + dispatch_decision = _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ) + if dispatch_decision not in ALLOWED_LINUX_VALIDATION_DISPATCH_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_dispatch_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_DISPATCH_DECISIONS)}" + ) + + dispatch_channel = _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ) + if dispatch_channel not in ALLOWED_LINUX_VALIDATION_DISPATCH_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_DISPATCH_CHANNELS)}" + ) + + terminal_exit_code = _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ) + if terminal_exit_code < 0: + raise ValueError(f"{path}: field 'terminal_verdict_exit_code' must be >= 0") + + dispatch_exit_code = _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ) + if dispatch_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_dispatch_exit_code' must be >= 0") + + linux_validation_timeout_seconds = _coerce_int( + payload["linux_validation_timeout_seconds"], + field="linux_validation_timeout_seconds", + path=path, + ) + if linux_validation_timeout_seconds < 1: + raise ValueError(f"{path}: field 'linux_validation_timeout_seconds' must be >= 1") + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + command_returncode = _coerce_optional_int( + payload["command_returncode"], + field="command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": terminal_status, + "terminal_verdict_decision": terminal_decision, + "terminal_verdict_exit_code": terminal_exit_code, + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": terminal_channel, + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": dispatch_channel, + "linux_validation_dispatch_status": dispatch_status, + "linux_validation_dispatch_decision": dispatch_decision, + "linux_validation_dispatch_exit_code": dispatch_exit_code, + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_timeout_seconds": linux_validation_timeout_seconds, + "linux_validation_command": _coerce_str( + payload["linux_validation_command"], + field="linux_validation_command", + path=path, + ), + "linux_validation_command_parts": _coerce_str_list( + payload["linux_validation_command_parts"], + field="linux_validation_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "command_returncode": command_returncode, + "command_stdout_tail": _coerce_str( + payload["command_stdout_tail"], + field="command_stdout_tail", + path=path, + ), + "command_stderr_tail": _coerce_str( + payload["command_stderr_tail"], + field="command_stderr_tail", + path=path, + ), + "dispatch_error_type": _coerce_str( + payload["dispatch_error_type"], + field="dispatch_error_type", + path=path, + ), + "dispatch_error_message": _coerce_str( + payload["dispatch_error_message"], + field="dispatch_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_verdict_command_parts( + *, + linux_validation_dispatch_report: dict[str, Any], + linux_validation_verdict_command: str, +) -> list[str]: + if str(linux_validation_dispatch_report["linux_validation_dispatch_status"]) != "dispatched": + return [] + if not linux_validation_verdict_command.strip(): + return [] + try: + parts = shlex.split(linux_validation_verdict_command) + except ValueError as exc: + raise ValueError(f"invalid --linux-validation-verdict-command ({exc})") from exc + if not parts: + raise ValueError("invalid --linux-validation-verdict-command (empty command)") + return parts + + +def execute_linux_validation_verdict_command( + command_parts: list[str], + *, + cwd: Path, + timeout_seconds: int, +) -> dict[str, Any]: + try: + completed = subprocess.run( + command_parts, + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + return { + "attempted": True, + "returncode": 127, + "stdout_tail": "", + "stderr_tail": str(exc), + "error_type": "command_not_found", + "error_message": str(exc), + } + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout if isinstance(exc.stdout, str) else "" + stderr = exc.stderr if isinstance(exc.stderr, str) else "" + return { + "attempted": True, + "returncode": 124, + "stdout_tail": _tail_text(stdout), + "stderr_tail": _tail_text(stderr), + "error_type": "timeout", + "error_message": f"linux validation verdict command timeout after {timeout_seconds}s", + } + + return { + "attempted": True, + "returncode": int(completed.returncode), + "stdout_tail": _tail_text(completed.stdout or ""), + "stderr_tail": _tail_text(completed.stderr or ""), + "error_type": "", + "error_message": "", + } + + +def build_linux_validation_verdict_payload( + linux_validation_dispatch_report: dict[str, Any], + *, + source_path: Path, + project_root: Path, + linux_validation_verdict_timeout_seconds: int, + verdict_command_parts: list[str], + dry_run: bool, + verdict_command_result: dict[str, Any] | None, +) -> dict[str, Any]: + terminal_status = str(linux_validation_dispatch_report["terminal_verdict_status"]) + terminal_decision = str(linux_validation_dispatch_report["terminal_verdict_decision"]) + terminal_exit_code = int(linux_validation_dispatch_report["terminal_verdict_exit_code"]) + terminal_should_proceed = bool(linux_validation_dispatch_report["terminal_verdict_should_proceed"]) + terminal_requires_manual_action = bool( + linux_validation_dispatch_report["terminal_verdict_requires_manual_action"] + ) + terminal_channel = str(linux_validation_dispatch_report["terminal_verdict_channel"]) + + dispatch_status = str(linux_validation_dispatch_report["linux_validation_dispatch_status"]) + dispatch_decision = str(linux_validation_dispatch_report["linux_validation_dispatch_decision"]) + dispatch_exit_code = int(linux_validation_dispatch_report["linux_validation_dispatch_exit_code"]) + dispatch_should_dispatch = bool(linux_validation_dispatch_report["linux_validation_should_dispatch"]) + dispatch_requires_manual_action = bool( + linux_validation_dispatch_report["linux_validation_requires_manual_action"] + ) + dispatch_channel = str(linux_validation_dispatch_report["linux_validation_channel"]) + dispatch_attempted = bool(linux_validation_dispatch_report["linux_validation_dispatch_attempted"]) + dispatch_command_returncode = linux_validation_dispatch_report["command_returncode"] + + drift_status = str(linux_validation_dispatch_report["gate_manifest_drift_status"]) + drift_missing_runtime_tests = list( + linux_validation_dispatch_report["gate_manifest_drift_missing_runtime_tests"] + ) + drift_missing_manifest_entries = list( + linux_validation_dispatch_report["gate_manifest_drift_missing_manifest_entries"] + ) + drift_orphan_manifest_entries = list( + linux_validation_dispatch_report["gate_manifest_drift_orphan_manifest_entries"] + ) + + reason_codes = list(linux_validation_dispatch_report["reason_codes"]) + structural_issues = list(linux_validation_dispatch_report["structural_issues"]) + missing_artifacts = list(linux_validation_dispatch_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_dispatch_report["evidence_manifest"]) + + expected_terminal = { + "ready_for_linux_validation": ( + "proceed_linux_validation", + True, + False, + "release", + 0, + ), + "ready_with_follow_up_for_linux_validation": ( + "proceed_linux_validation_with_follow_up", + True, + False, + "follow_up", + None, + ), + "blocked": ("halt_linux_validation_blocker", False, True, "blocker", None), + "contract_failed": ("abort_linux_validation", False, True, "blocker", None), + } + ( + expected_terminal_decision, + expected_terminal_should_proceed, + expected_terminal_requires_manual_action, + expected_terminal_channel, + expected_terminal_exit_code, + ) = expected_terminal[terminal_status] + + if terminal_decision != expected_terminal_decision: + structural_issues.append("terminal_verdict_decision_mismatch") + if terminal_should_proceed != expected_terminal_should_proceed: + structural_issues.append("terminal_verdict_should_proceed_mismatch") + if terminal_requires_manual_action != expected_terminal_requires_manual_action: + structural_issues.append("terminal_verdict_requires_manual_action_mismatch") + if terminal_channel != expected_terminal_channel: + structural_issues.append("terminal_verdict_channel_mismatch") + if expected_terminal_exit_code == 0 and terminal_exit_code != 0: + structural_issues.append("terminal_verdict_exit_code_mismatch_ready") + if expected_terminal_exit_code is None and terminal_exit_code == 0: + structural_issues.append("terminal_verdict_exit_code_mismatch_non_ready") + + if drift_status != "passed": + structural_issues.append("gate_manifest_drift_failed") + if drift_missing_runtime_tests: + structural_issues.append("gate_manifest_drift_missing_runtime_tests") + if drift_missing_manifest_entries: + structural_issues.append("gate_manifest_drift_missing_manifest_entries") + if drift_orphan_manifest_entries: + structural_issues.append("gate_manifest_drift_orphan_manifest_entries") + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + structural_issues.append("linux_validation_dispatch_evidence_missing") + + expected_dispatch = { + "ready_dry_run": ( + "dispatch_linux_validation", + True, + False, + "release", + 0, + False, + None, + ), + "ready_with_follow_up_dry_run": ( + "dispatch_linux_validation_with_follow_up", + True, + False, + "follow_up", + None, + False, + None, + ), + "dispatched": ( + None, + True, + False, + None, + None, + True, + 0, + ), + "dispatch_failed": ( + None, + True, + False, + None, + None, + True, + None, + ), + "blocked": ( + "hold_linux_validation_blocker", + False, + True, + "blocker", + None, + False, + None, + ), + "contract_failed": ( + "abort_linux_validation_dispatch", + False, + True, + "blocker", + None, + False, + None, + ), + } + ( + expected_dispatch_decision, + expected_dispatch_should_dispatch, + expected_dispatch_requires_manual_action, + expected_dispatch_channel, + expected_dispatch_exit_code, + expected_dispatch_attempted, + expected_dispatch_command_returncode, + ) = expected_dispatch[dispatch_status] + + if expected_dispatch_decision is not None and dispatch_decision != expected_dispatch_decision: + structural_issues.append("linux_validation_dispatch_decision_mismatch") + + if dispatch_status in {"dispatched", "dispatch_failed"}: + if dispatch_channel == "release" and dispatch_decision != "dispatch_linux_validation": + structural_issues.append("linux_validation_dispatch_decision_mismatch_release") + if dispatch_channel == "follow_up" and dispatch_decision != "dispatch_linux_validation_with_follow_up": + structural_issues.append("linux_validation_dispatch_decision_mismatch_follow_up") + if dispatch_channel not in {"release", "follow_up"}: + structural_issues.append("linux_validation_dispatch_channel_mismatch_active") + + if dispatch_should_dispatch != expected_dispatch_should_dispatch: + structural_issues.append("linux_validation_should_dispatch_mismatch") + if dispatch_requires_manual_action != expected_dispatch_requires_manual_action: + structural_issues.append("linux_validation_requires_manual_action_mismatch") + + if expected_dispatch_channel is not None and dispatch_channel != expected_dispatch_channel: + structural_issues.append("linux_validation_dispatch_channel_mismatch") + + if dispatch_attempted != expected_dispatch_attempted: + structural_issues.append("linux_validation_dispatch_attempted_mismatch") + + if expected_dispatch_exit_code == 0 and dispatch_exit_code != 0: + structural_issues.append("linux_validation_dispatch_exit_code_mismatch_ready") + if expected_dispatch_exit_code is None and dispatch_exit_code == 0 and dispatch_status in { + "ready_with_follow_up_dry_run", + "blocked", + "contract_failed", + "dispatch_failed", + }: + structural_issues.append("linux_validation_dispatch_exit_code_mismatch_non_ready") + + if expected_dispatch_command_returncode == 0 and dispatch_command_returncode != 0: + structural_issues.append("linux_validation_dispatch_command_returncode_mismatch_success") + if dispatch_status in {"ready_dry_run", "ready_with_follow_up_dry_run", "blocked", "contract_failed"} and dispatch_command_returncode is not None: + structural_issues.append("linux_validation_dispatch_command_returncode_mismatch_non_executed") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + verdict_command = _format_shell_command(verdict_command_parts) if verdict_command_parts else "" + + verdict_status: str + verdict_decision: str + verdict_exit_code: int + linux_validation_passed: bool + linux_validation_requires_manual_action: bool + linux_validation_channel: str + + if structural_issues or missing_artifacts: + verdict_status = "contract_failed" + verdict_decision = "abort_linux_validation_verdict" + verdict_exit_code = max(1, terminal_exit_code, dispatch_exit_code) + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.extend(structural_issues) + elif dispatch_status == "contract_failed": + verdict_status = "contract_failed" + verdict_decision = "abort_linux_validation_verdict" + verdict_exit_code = max(1, dispatch_exit_code) + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.append("linux_validation_verdict_dispatch_contract_failed") + elif dispatch_status == "blocked": + verdict_status = "blocked" + verdict_decision = "hold_linux_validation_blocker" + verdict_exit_code = max(1, dispatch_exit_code) + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.append("linux_validation_verdict_blocked") + elif dispatch_status == "dispatch_failed": + verdict_status = "validation_failed" + verdict_decision = "escalate_linux_validation_failure" + verdict_exit_code = 1 + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.append("linux_validation_verdict_dispatch_failed") + elif dispatch_channel == "follow_up": + verdict_status = "validated_with_follow_up" + verdict_decision = "accept_linux_validation_with_follow_up" + verdict_exit_code = max(1, dispatch_exit_code) + linux_validation_passed = True + linux_validation_requires_manual_action = False + linux_validation_channel = "follow_up" + reason_codes.append("linux_validation_verdict_validated_with_follow_up") + else: + verdict_status = "validated" + verdict_decision = "accept_linux_validation" + verdict_exit_code = 0 + linux_validation_passed = True + linux_validation_requires_manual_action = False + linux_validation_channel = "release" + reason_codes.append("linux_validation_verdict_validated") + + verdict_attempted = False + verdict_command_returncode: int | None = None + verdict_command_stdout_tail = "" + verdict_command_stderr_tail = "" + verdict_error_type = "" + verdict_error_message = "" + + if ( + verdict_status in {"validated", "validated_with_follow_up"} + and verdict_command_parts + and not dry_run + ): + verdict_attempted = True + if verdict_command_result is None: + verdict_status = "validation_failed" + verdict_decision = "escalate_linux_validation_failure" + verdict_exit_code = 1 + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + reason_codes.append("linux_validation_verdict_result_missing") + else: + verdict_command_returncode = int(verdict_command_result["returncode"]) + verdict_command_stdout_tail = str(verdict_command_result.get("stdout_tail", "")) + verdict_command_stderr_tail = str(verdict_command_result.get("stderr_tail", "")) + verdict_error_type = str(verdict_command_result.get("error_type", "")) + verdict_error_message = str(verdict_command_result.get("error_message", "")) + if verdict_command_returncode != 0: + verdict_status = "validation_failed" + verdict_decision = "escalate_linux_validation_failure" + verdict_exit_code = 1 + linux_validation_passed = False + linux_validation_requires_manual_action = True + linux_validation_channel = "blocker" + if verdict_error_type == "command_not_found": + reason_codes.append("linux_validation_verdict_cli_unavailable") + elif verdict_error_type == "timeout": + reason_codes.append("linux_validation_verdict_timeout") + else: + reason_codes.append("linux_validation_verdict_command_failed") + elif verdict_status == "validated_with_follow_up": + verdict_exit_code = max(1, verdict_exit_code) + reason_codes.append("linux_validation_verdict_command_succeeded_with_follow_up") + else: + verdict_exit_code = 0 + reason_codes.append("linux_validation_verdict_command_succeeded") + + if verdict_status not in ALLOWED_LINUX_VALIDATION_VERDICT_STATUSES: + raise ValueError("internal: unsupported linux_validation_verdict_status computed") + if verdict_decision not in ALLOWED_LINUX_VALIDATION_VERDICT_DECISIONS: + raise ValueError("internal: unsupported linux_validation_verdict_decision computed") + if linux_validation_channel not in ALLOWED_LINUX_VALIDATION_VERDICT_CHANNELS: + raise ValueError("internal: unsupported linux_validation_verdict_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_dispatch_status={dispatch_status} " + f"linux_validation_dispatch_decision={dispatch_decision} " + f"linux_validation_verdict_status={verdict_status} " + f"linux_validation_verdict_decision={verdict_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_dispatch_report": str(source_path), + "project_root": str(project_root), + "source_terminal_verdict_report": str( + linux_validation_dispatch_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_dispatch_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_dispatch_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_dispatch_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": drift_status, + "gate_manifest_drift_missing_runtime_tests": drift_missing_runtime_tests, + "gate_manifest_drift_missing_manifest_entries": drift_missing_manifest_entries, + "gate_manifest_drift_orphan_manifest_entries": drift_orphan_manifest_entries, + "terminal_verdict_status": terminal_status, + "terminal_verdict_decision": terminal_decision, + "terminal_verdict_exit_code": terminal_exit_code, + "terminal_verdict_should_proceed": terminal_should_proceed, + "terminal_verdict_requires_manual_action": terminal_requires_manual_action, + "terminal_verdict_channel": terminal_channel, + "linux_validation_should_dispatch": dispatch_should_dispatch, + "linux_validation_requires_manual_action": dispatch_requires_manual_action, + "linux_validation_channel": dispatch_channel, + "linux_validation_dispatch_status": dispatch_status, + "linux_validation_dispatch_decision": dispatch_decision, + "linux_validation_dispatch_exit_code": dispatch_exit_code, + "linux_validation_dispatch_attempted": dispatch_attempted, + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_verdict_status": verdict_status, + "linux_validation_verdict_decision": verdict_decision, + "linux_validation_verdict_exit_code": verdict_exit_code, + "linux_validation_passed": linux_validation_passed, + "linux_validation_verdict_requires_manual_action": linux_validation_requires_manual_action, + "linux_validation_verdict_channel": linux_validation_channel, + "linux_validation_verdict_attempted": verdict_attempted, + "linux_validation_verdict_timeout_seconds": linux_validation_verdict_timeout_seconds, + "linux_validation_verdict_command": verdict_command, + "linux_validation_verdict_command_parts": verdict_command_parts, + "dry_run": dry_run, + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": verdict_command_stdout_tail, + "verdict_command_stderr_tail": verdict_command_stderr_tail, + "verdict_error_type": verdict_error_type, + "verdict_error_message": verdict_error_message, + "release_run_id": linux_validation_dispatch_report["release_run_id"], + "release_run_url": str(linux_validation_dispatch_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_dispatch_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_dispatch_report["linux_validation_dispatch_summary"] + ), + "linux_validation_verdict_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Verdict Report", + "", + ( + "- Linux Validation Verdict Status: " + f"**{str(payload['linux_validation_verdict_status']).upper()}**" + ), + ( + "- Linux Validation Verdict Decision: " + f"`{payload['linux_validation_verdict_decision']}`" + ), + ( + "- Linux Validation Verdict Exit Code: " + f"`{payload['linux_validation_verdict_exit_code']}`" + ), + f"- Linux Validation Passed: `{payload['linux_validation_passed']}`", + ( + "- Linux Validation Verdict Requires Manual Action: " + f"`{payload['linux_validation_verdict_requires_manual_action']}`" + ), + f"- Linux Validation Verdict Channel: `{payload['linux_validation_verdict_channel']}`", + f"- Dispatch Status: `{payload['linux_validation_dispatch_status']}`", + f"- Dispatch Decision: `{payload['linux_validation_dispatch_decision']}`", + f"- Dispatch Exit Code: `{payload['linux_validation_dispatch_exit_code']}`", + f"- Dispatch Attempted: `{payload['linux_validation_dispatch_attempted']}`", + f"- Terminal Verdict Status: `{payload['terminal_verdict_status']}`", + f"- Gate Manifest Drift Status: `{payload['gate_manifest_drift_status']}`", + f"- Verdict Command Attempted: `{payload['linux_validation_verdict_attempted']}`", + f"- Dry Run: `{payload['dry_run']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_verdict_summary']}", + "", + "## Verdict Command", + ] + + if payload["linux_validation_verdict_command"]: + lines.append(f"- `{payload['linux_validation_verdict_command']}`") + else: + lines.append("- none") + + lines.extend(["", "## Reason Codes"]) + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.extend(["", "## Verdict Command Output (tail)"]) + stdout_tail = str(payload["verdict_command_stdout_tail"]).strip() + stderr_tail = str(payload["verdict_command_stderr_tail"]).strip() + if stdout_tail: + lines.append("- stdout:") + lines.append("```text") + lines.append(stdout_tail) + lines.append("```") + else: + lines.append("- stdout: none") + if stderr_tail: + lines.append("- stderr:") + lines.append("```text") + lines.append(stderr_tail) + lines.append("```") + else: + lines.append("- stderr: none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + verdict_command_returncode = payload["verdict_command_returncode"] + dispatch_command_returncode = payload["linux_validation_dispatch_command_returncode"] + return { + "workflow_linux_validation_verdict_status": str(payload["linux_validation_verdict_status"]), + "workflow_linux_validation_verdict_decision": str(payload["linux_validation_verdict_decision"]), + "workflow_linux_validation_verdict_exit_code": str(payload["linux_validation_verdict_exit_code"]), + "workflow_linux_validation_verdict_passed": ( + "true" if payload["linux_validation_passed"] else "false" + ), + "workflow_linux_validation_verdict_requires_manual_action": ( + "true" if payload["linux_validation_verdict_requires_manual_action"] else "false" + ), + "workflow_linux_validation_verdict_channel": str(payload["linux_validation_verdict_channel"]), + "workflow_linux_validation_verdict_dispatch_status": str( + payload["linux_validation_dispatch_status"] + ), + "workflow_linux_validation_verdict_dispatch_decision": str( + payload["linux_validation_dispatch_decision"] + ), + "workflow_linux_validation_verdict_dispatch_exit_code": str( + payload["linux_validation_dispatch_exit_code"] + ), + "workflow_linux_validation_verdict_dispatch_command_returncode": ( + "" if dispatch_command_returncode is None else str(dispatch_command_returncode) + ), + "workflow_linux_validation_verdict_command_attempted": ( + "true" if payload["linux_validation_verdict_attempted"] else "false" + ), + "workflow_linux_validation_verdict_command_returncode": ( + "" if verdict_command_returncode is None else str(verdict_command_returncode) + ), + "workflow_linux_validation_verdict_terminal_status": str(payload["terminal_verdict_status"]), + "workflow_linux_validation_verdict_gate_manifest_drift_status": str( + payload["gate_manifest_drift_status"] + ), + "workflow_linux_validation_verdict_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_verdict_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_verdict_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_verdict_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_verdict_report_json": str(output_json), + "workflow_linux_validation_verdict_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Converge Linux validation verdict from P2-60 Linux validation dispatch " + "and publish Linux validation verdict contract" + ) + ) + parser.add_argument( + "--linux-validation-dispatch-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json", + help="P2-60 Linux validation dispatch report JSON path", + ) + parser.add_argument( + "--project-root", + default=".", + help="Project root used to execute optional Linux validation verdict command", + ) + parser.add_argument( + "--linux-validation-verdict-command", + default="", + help="Optional explicit Linux validation verdict command string", + ) + parser.add_argument( + "--linux-validation-verdict-timeout-seconds", + type=int, + default=900, + help="Linux validation verdict command timeout in seconds", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json", + help="Output Linux validation verdict JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md", + help="Output Linux validation verdict markdown path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation verdict JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + if args.linux_validation_verdict_timeout_seconds < 1: + print( + "[p2-linux-ci-workflow-linux-validation-verdict-gate] " + "--linux-validation-verdict-timeout-seconds must be >= 1", + file=sys.stderr, + ) + return 2 + + source_path = Path(args.linux_validation_dispatch_report) + project_root = Path(args.project_root) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + dispatch_report = load_linux_validation_dispatch_report(source_path) + verdict_command_parts = build_linux_validation_verdict_command_parts( + linux_validation_dispatch_report=dispatch_report, + linux_validation_verdict_command=args.linux_validation_verdict_command, + ) + except ValueError as exc: + print( + f"[p2-linux-ci-workflow-linux-validation-verdict-gate] {exc}", + file=sys.stderr, + ) + return 2 + + verdict_command_result: dict[str, Any] | None = None + if verdict_command_parts and not args.dry_run: + verdict_command_result = execute_linux_validation_verdict_command( + verdict_command_parts, + cwd=project_root, + timeout_seconds=args.linux_validation_verdict_timeout_seconds, + ) + + payload = build_linux_validation_verdict_payload( + dispatch_report, + source_path=source_path.resolve(), + project_root=project_root.resolve(), + linux_validation_verdict_timeout_seconds=args.linux_validation_verdict_timeout_seconds, + verdict_command_parts=verdict_command_parts, + dry_run=args.dry_run, + verdict_command_result=verdict_command_result, + ) + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation verdict json: {output_json_path}") + print(f"linux validation verdict markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation verdict summary: " + f"linux_validation_verdict_status={payload['linux_validation_verdict_status']} " + f"linux_validation_verdict_decision={payload['linux_validation_verdict_decision']} " + f"linux_validation_verdict_exit_code={payload['linux_validation_verdict_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_verdict_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_lv_verdict_publish_gate.py b/scripts/run_p2_lv_verdict_publish_gate.py new file mode 100644 index 0000000..9058c23 --- /dev/null +++ b/scripts/run_p2_lv_verdict_publish_gate.py @@ -0,0 +1,927 @@ +"""Phase 2 card P2-62 gate for Linux CI workflow Linux-validation verdict publish. + +This script consumes the P2-61 Linux-validation verdict artifact and converges +one terminal publish contract for Linux validation: +1) validate Linux validation verdict + evidence consistency, +2) normalize publish semantics for downstream consumers, +3) emit JSON/Markdown report + optional GitHub outputs. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ALLOWED_LINUX_VALIDATION_VERDICT_STATUSES: set[str] = { + "validated", + "validated_with_follow_up", + "validation_failed", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_VERDICT_DECISIONS: set[str] = { + "accept_linux_validation", + "accept_linux_validation_with_follow_up", + "escalate_linux_validation_failure", + "hold_linux_validation_blocker", + "abort_linux_validation_verdict", +} +ALLOWED_LINUX_VALIDATION_VERDICT_CHANNELS: set[str] = {"release", "follow_up", "blocker"} + +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_STATUSES: set[str] = { + "published", + "published_with_follow_up", + "blocked", + "contract_failed", +} +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_DECISIONS: set[str] = { + "announce_linux_validation_passed", + "announce_linux_validation_passed_with_follow_up", + "announce_linux_validation_blocker", + "abort_publish", +} +ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_CHANNELS: set[str] = { + "release", + "follow_up", + "blocker", +} + + +def _coerce_bool(value: Any, *, field: str, path: Path) -> bool: + if not isinstance(value, bool): + raise ValueError(f"{path}: field '{field}' must be bool") + return value + + +def _coerce_int(value: Any, *, field: str, path: Path) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{path}: field '{field}' must be int") + return value + + +def _coerce_optional_int(value: Any, *, field: str, path: Path) -> int | None: + if value is None: + return None + return _coerce_int(value, field=field, path=path) + + +def _coerce_str(value: Any, *, field: str, path: Path) -> str: + if not isinstance(value, str): + raise ValueError(f"{path}: field '{field}' must be string") + return value + + +def _coerce_str_list(value: Any, *, field: str, path: Path) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{path}: field '{field}' must be string list") + return list(value) + + +def _unique(items: list[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + +def _load_evidence_manifest(value: Any, *, field: str, path: Path) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError(f"{path}: field '{field}' must be object list") + + evidence_manifest: list[dict[str, Any]] = [] + for idx, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"{path}: {field}[{idx}] must be object") + if "source" not in entry or "path" not in entry or "exists" not in entry: + raise ValueError(f"{path}: {field}[{idx}] missing source/path/exists") + evidence_manifest.append( + { + "source": _coerce_str(entry["source"], field=f"{field}[{idx}].source", path=path), + "path": _coerce_str(entry["path"], field=f"{field}[{idx}].path", path=path), + "exists": _coerce_bool(entry["exists"], field=f"{field}[{idx}].exists", path=path), + } + ) + return evidence_manifest + + +def load_linux_validation_verdict_report(path: Path) -> dict[str, Any]: + if not path.is_file(): + raise ValueError(f"{path}: Linux validation verdict report not found") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}: invalid JSON ({exc})") from exc + if not isinstance(payload, dict): + raise ValueError(f"{path}: Linux validation verdict payload must be object") + + required_fields = ( + "source_linux_validation_dispatch_report", + "project_root", + "source_terminal_verdict_report", + "source_release_final_publish_archive_report", + "source_gate_manifest_drift_report", + "release_final_publish_archive_status", + "gate_manifest_drift_status", + "gate_manifest_drift_missing_runtime_tests", + "gate_manifest_drift_missing_manifest_entries", + "gate_manifest_drift_orphan_manifest_entries", + "terminal_verdict_status", + "terminal_verdict_decision", + "terminal_verdict_exit_code", + "terminal_verdict_should_proceed", + "terminal_verdict_requires_manual_action", + "terminal_verdict_channel", + "linux_validation_should_dispatch", + "linux_validation_requires_manual_action", + "linux_validation_channel", + "linux_validation_dispatch_status", + "linux_validation_dispatch_decision", + "linux_validation_dispatch_exit_code", + "linux_validation_dispatch_attempted", + "linux_validation_dispatch_command_returncode", + "linux_validation_verdict_status", + "linux_validation_verdict_decision", + "linux_validation_verdict_exit_code", + "linux_validation_passed", + "linux_validation_verdict_requires_manual_action", + "linux_validation_verdict_channel", + "linux_validation_verdict_attempted", + "linux_validation_verdict_timeout_seconds", + "linux_validation_verdict_command", + "linux_validation_verdict_command_parts", + "dry_run", + "verdict_command_returncode", + "verdict_command_stdout_tail", + "verdict_command_stderr_tail", + "verdict_error_type", + "verdict_error_message", + "release_run_id", + "release_run_url", + "follow_up_queue_url", + "linux_validation_dispatch_summary", + "linux_validation_verdict_summary", + "reason_codes", + "structural_issues", + "missing_artifacts", + "evidence_manifest", + ) + for field in required_fields: + if field not in payload: + raise ValueError(f"{path}: missing field '{field}'") + + verdict_status = _coerce_str( + payload["linux_validation_verdict_status"], + field="linux_validation_verdict_status", + path=path, + ) + if verdict_status not in ALLOWED_LINUX_VALIDATION_VERDICT_STATUSES: + raise ValueError( + f"{path}: field 'linux_validation_verdict_status' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_STATUSES)}" + ) + + verdict_decision = _coerce_str( + payload["linux_validation_verdict_decision"], + field="linux_validation_verdict_decision", + path=path, + ) + if verdict_decision not in ALLOWED_LINUX_VALIDATION_VERDICT_DECISIONS: + raise ValueError( + f"{path}: field 'linux_validation_verdict_decision' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_DECISIONS)}" + ) + + verdict_exit_code = _coerce_int( + payload["linux_validation_verdict_exit_code"], + field="linux_validation_verdict_exit_code", + path=path, + ) + if verdict_exit_code < 0: + raise ValueError(f"{path}: field 'linux_validation_verdict_exit_code' must be >= 0") + + verdict_channel = _coerce_str( + payload["linux_validation_verdict_channel"], + field="linux_validation_verdict_channel", + path=path, + ) + if verdict_channel not in ALLOWED_LINUX_VALIDATION_VERDICT_CHANNELS: + raise ValueError( + f"{path}: field 'linux_validation_verdict_channel' must be one of " + f"{sorted(ALLOWED_LINUX_VALIDATION_VERDICT_CHANNELS)}" + ) + + release_run_id_raw = payload["release_run_id"] + if release_run_id_raw is None: + release_run_id = None + else: + release_run_id = _coerce_int(release_run_id_raw, field="release_run_id", path=path) + if release_run_id < 1: + raise ValueError(f"{path}: field 'release_run_id' must be >= 1 when present") + + dispatch_command_returncode = _coerce_optional_int( + payload["linux_validation_dispatch_command_returncode"], + field="linux_validation_dispatch_command_returncode", + path=path, + ) + verdict_command_returncode = _coerce_optional_int( + payload["verdict_command_returncode"], + field="verdict_command_returncode", + path=path, + ) + + return { + "generated_at": payload.get("generated_at"), + "source_linux_validation_dispatch_report": _coerce_str( + payload["source_linux_validation_dispatch_report"], + field="source_linux_validation_dispatch_report", + path=path, + ), + "project_root": _coerce_str(payload["project_root"], field="project_root", path=path), + "source_terminal_verdict_report": _coerce_str( + payload["source_terminal_verdict_report"], + field="source_terminal_verdict_report", + path=path, + ), + "source_release_final_publish_archive_report": _coerce_str( + payload["source_release_final_publish_archive_report"], + field="source_release_final_publish_archive_report", + path=path, + ), + "source_gate_manifest_drift_report": _coerce_str( + payload["source_gate_manifest_drift_report"], + field="source_gate_manifest_drift_report", + path=path, + ), + "release_final_publish_archive_status": _coerce_str( + payload["release_final_publish_archive_status"], + field="release_final_publish_archive_status", + path=path, + ), + "gate_manifest_drift_status": _coerce_str( + payload["gate_manifest_drift_status"], + field="gate_manifest_drift_status", + path=path, + ), + "gate_manifest_drift_missing_runtime_tests": _coerce_str_list( + payload["gate_manifest_drift_missing_runtime_tests"], + field="gate_manifest_drift_missing_runtime_tests", + path=path, + ), + "gate_manifest_drift_missing_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_missing_manifest_entries"], + field="gate_manifest_drift_missing_manifest_entries", + path=path, + ), + "gate_manifest_drift_orphan_manifest_entries": _coerce_str_list( + payload["gate_manifest_drift_orphan_manifest_entries"], + field="gate_manifest_drift_orphan_manifest_entries", + path=path, + ), + "terminal_verdict_status": _coerce_str( + payload["terminal_verdict_status"], + field="terminal_verdict_status", + path=path, + ), + "terminal_verdict_decision": _coerce_str( + payload["terminal_verdict_decision"], + field="terminal_verdict_decision", + path=path, + ), + "terminal_verdict_exit_code": _coerce_int( + payload["terminal_verdict_exit_code"], + field="terminal_verdict_exit_code", + path=path, + ), + "terminal_verdict_should_proceed": _coerce_bool( + payload["terminal_verdict_should_proceed"], + field="terminal_verdict_should_proceed", + path=path, + ), + "terminal_verdict_requires_manual_action": _coerce_bool( + payload["terminal_verdict_requires_manual_action"], + field="terminal_verdict_requires_manual_action", + path=path, + ), + "terminal_verdict_channel": _coerce_str( + payload["terminal_verdict_channel"], + field="terminal_verdict_channel", + path=path, + ), + "linux_validation_should_dispatch": _coerce_bool( + payload["linux_validation_should_dispatch"], + field="linux_validation_should_dispatch", + path=path, + ), + "linux_validation_requires_manual_action": _coerce_bool( + payload["linux_validation_requires_manual_action"], + field="linux_validation_requires_manual_action", + path=path, + ), + "linux_validation_channel": _coerce_str( + payload["linux_validation_channel"], + field="linux_validation_channel", + path=path, + ), + "linux_validation_dispatch_status": _coerce_str( + payload["linux_validation_dispatch_status"], + field="linux_validation_dispatch_status", + path=path, + ), + "linux_validation_dispatch_decision": _coerce_str( + payload["linux_validation_dispatch_decision"], + field="linux_validation_dispatch_decision", + path=path, + ), + "linux_validation_dispatch_exit_code": _coerce_int( + payload["linux_validation_dispatch_exit_code"], + field="linux_validation_dispatch_exit_code", + path=path, + ), + "linux_validation_dispatch_attempted": _coerce_bool( + payload["linux_validation_dispatch_attempted"], + field="linux_validation_dispatch_attempted", + path=path, + ), + "linux_validation_dispatch_command_returncode": dispatch_command_returncode, + "linux_validation_verdict_status": verdict_status, + "linux_validation_verdict_decision": verdict_decision, + "linux_validation_verdict_exit_code": verdict_exit_code, + "linux_validation_passed": _coerce_bool( + payload["linux_validation_passed"], + field="linux_validation_passed", + path=path, + ), + "linux_validation_verdict_requires_manual_action": _coerce_bool( + payload["linux_validation_verdict_requires_manual_action"], + field="linux_validation_verdict_requires_manual_action", + path=path, + ), + "linux_validation_verdict_channel": verdict_channel, + "linux_validation_verdict_attempted": _coerce_bool( + payload["linux_validation_verdict_attempted"], + field="linux_validation_verdict_attempted", + path=path, + ), + "linux_validation_verdict_timeout_seconds": _coerce_int( + payload["linux_validation_verdict_timeout_seconds"], + field="linux_validation_verdict_timeout_seconds", + path=path, + ), + "linux_validation_verdict_command": _coerce_str( + payload["linux_validation_verdict_command"], + field="linux_validation_verdict_command", + path=path, + ), + "linux_validation_verdict_command_parts": _coerce_str_list( + payload["linux_validation_verdict_command_parts"], + field="linux_validation_verdict_command_parts", + path=path, + ), + "dry_run": _coerce_bool(payload["dry_run"], field="dry_run", path=path), + "verdict_command_returncode": verdict_command_returncode, + "verdict_command_stdout_tail": _coerce_str( + payload["verdict_command_stdout_tail"], + field="verdict_command_stdout_tail", + path=path, + ), + "verdict_command_stderr_tail": _coerce_str( + payload["verdict_command_stderr_tail"], + field="verdict_command_stderr_tail", + path=path, + ), + "verdict_error_type": _coerce_str( + payload["verdict_error_type"], + field="verdict_error_type", + path=path, + ), + "verdict_error_message": _coerce_str( + payload["verdict_error_message"], + field="verdict_error_message", + path=path, + ), + "release_run_id": release_run_id, + "release_run_url": _coerce_str(payload["release_run_url"], field="release_run_url", path=path), + "follow_up_queue_url": _coerce_str( + payload["follow_up_queue_url"], + field="follow_up_queue_url", + path=path, + ), + "linux_validation_dispatch_summary": _coerce_str( + payload["linux_validation_dispatch_summary"], + field="linux_validation_dispatch_summary", + path=path, + ), + "linux_validation_verdict_summary": _coerce_str( + payload["linux_validation_verdict_summary"], + field="linux_validation_verdict_summary", + path=path, + ), + "reason_codes": _coerce_str_list(payload["reason_codes"], field="reason_codes", path=path), + "structural_issues": _coerce_str_list( + payload["structural_issues"], + field="structural_issues", + path=path, + ), + "missing_artifacts": _coerce_str_list( + payload["missing_artifacts"], + field="missing_artifacts", + path=path, + ), + "evidence_manifest": _load_evidence_manifest( + payload["evidence_manifest"], + field="evidence_manifest", + path=path, + ), + } + + +def build_linux_validation_verdict_publish_payload( + linux_validation_verdict_report: dict[str, Any], *, source_path: Path +) -> dict[str, Any]: + verdict_status = str(linux_validation_verdict_report["linux_validation_verdict_status"]) + verdict_decision = str(linux_validation_verdict_report["linux_validation_verdict_decision"]) + verdict_exit_code = int(linux_validation_verdict_report["linux_validation_verdict_exit_code"]) + verdict_passed = bool(linux_validation_verdict_report["linux_validation_passed"]) + verdict_requires_manual_action = bool( + linux_validation_verdict_report["linux_validation_verdict_requires_manual_action"] + ) + verdict_channel = str(linux_validation_verdict_report["linux_validation_verdict_channel"]) + + reason_codes = list(linux_validation_verdict_report["reason_codes"]) + structural_issues = list(linux_validation_verdict_report["structural_issues"]) + missing_artifacts = list(linux_validation_verdict_report["missing_artifacts"]) + evidence_manifest = list(linux_validation_verdict_report["evidence_manifest"]) + + missing_evidence = [str(item["path"]) for item in evidence_manifest if not bool(item["exists"])] + if missing_evidence: + missing_artifacts.extend(missing_evidence) + reason_codes.append("linux_validation_verdict_publish_evidence_missing") + + expected_verdict = { + "validated": ( + "accept_linux_validation", + 0, + True, + False, + "release", + ), + "validated_with_follow_up": ( + "accept_linux_validation_with_follow_up", + None, + True, + False, + "follow_up", + ), + "validation_failed": ( + "escalate_linux_validation_failure", + None, + False, + True, + "blocker", + ), + "blocked": ( + "hold_linux_validation_blocker", + None, + False, + True, + "blocker", + ), + "contract_failed": ( + "abort_linux_validation_verdict", + None, + False, + True, + "blocker", + ), + } + ( + expected_decision, + expected_exit_code, + expected_passed, + expected_requires_manual_action, + expected_channel, + ) = expected_verdict[verdict_status] + + if verdict_decision != expected_decision: + structural_issues.append("linux_validation_verdict_decision_mismatch") + if verdict_passed != expected_passed: + structural_issues.append("linux_validation_verdict_passed_mismatch") + if verdict_requires_manual_action != expected_requires_manual_action: + structural_issues.append("linux_validation_verdict_requires_manual_action_mismatch") + if verdict_channel != expected_channel: + structural_issues.append("linux_validation_verdict_channel_mismatch") + if expected_exit_code == 0 and verdict_exit_code != 0: + structural_issues.append("linux_validation_verdict_exit_code_mismatch_validated") + if expected_exit_code is None and verdict_exit_code == 0 and verdict_status != "validated": + structural_issues.append("linux_validation_verdict_exit_code_mismatch_non_validated") + + structural_issues = _unique(structural_issues) + missing_artifacts = _unique(missing_artifacts) + + publish_status: str + publish_decision: str + publish_exit_code: int + publish_should_notify: bool + publish_requires_manual_action: bool + publish_channel: str + + if structural_issues or missing_artifacts: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.extend(structural_issues) + elif verdict_status == "validated": + publish_status = "published" + publish_decision = "announce_linux_validation_passed" + publish_exit_code = 0 + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "release" + reason_codes = ["linux_validation_verdict_publish_published"] + elif verdict_status == "validated_with_follow_up": + publish_status = "published_with_follow_up" + publish_decision = "announce_linux_validation_passed_with_follow_up" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = False + publish_channel = "follow_up" + reason_codes.append("linux_validation_verdict_publish_with_follow_up") + elif verdict_status == "validation_failed": + publish_status = "blocked" + publish_decision = "announce_linux_validation_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_verdict_publish_validation_failed") + elif verdict_status == "blocked": + publish_status = "blocked" + publish_decision = "announce_linux_validation_blocker" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_verdict_publish_blocked") + else: + publish_status = "contract_failed" + publish_decision = "abort_publish" + publish_exit_code = max(1, verdict_exit_code) + publish_should_notify = True + publish_requires_manual_action = True + publish_channel = "blocker" + reason_codes.append("linux_validation_verdict_publish_upstream_contract_failed") + + if publish_status not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_STATUSES: + raise ValueError("internal: unsupported linux_validation_verdict_publish_status computed") + if publish_decision not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_DECISIONS: + raise ValueError("internal: unsupported linux_validation_verdict_publish_decision computed") + if publish_channel not in ALLOWED_LINUX_VALIDATION_VERDICT_PUBLISH_CHANNELS: + raise ValueError("internal: unsupported linux_validation_verdict_publish_channel computed") + + reason_codes = _unique(reason_codes) + summary = ( + f"linux_validation_verdict_status={verdict_status} " + f"linux_validation_verdict_publish_status={publish_status} " + f"linux_validation_verdict_publish_decision={publish_decision}" + ) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source_linux_validation_verdict_report": str(source_path), + "source_linux_validation_dispatch_report": str( + linux_validation_verdict_report["source_linux_validation_dispatch_report"] + ), + "project_root": str(linux_validation_verdict_report["project_root"]), + "source_terminal_verdict_report": str( + linux_validation_verdict_report["source_terminal_verdict_report"] + ), + "source_release_final_publish_archive_report": str( + linux_validation_verdict_report["source_release_final_publish_archive_report"] + ), + "source_gate_manifest_drift_report": str( + linux_validation_verdict_report["source_gate_manifest_drift_report"] + ), + "release_final_publish_archive_status": str( + linux_validation_verdict_report["release_final_publish_archive_status"] + ), + "gate_manifest_drift_status": str( + linux_validation_verdict_report["gate_manifest_drift_status"] + ), + "gate_manifest_drift_missing_runtime_tests": list( + linux_validation_verdict_report["gate_manifest_drift_missing_runtime_tests"] + ), + "gate_manifest_drift_missing_manifest_entries": list( + linux_validation_verdict_report["gate_manifest_drift_missing_manifest_entries"] + ), + "gate_manifest_drift_orphan_manifest_entries": list( + linux_validation_verdict_report["gate_manifest_drift_orphan_manifest_entries"] + ), + "terminal_verdict_status": str(linux_validation_verdict_report["terminal_verdict_status"]), + "terminal_verdict_decision": str( + linux_validation_verdict_report["terminal_verdict_decision"] + ), + "terminal_verdict_exit_code": int( + linux_validation_verdict_report["terminal_verdict_exit_code"] + ), + "terminal_verdict_should_proceed": bool( + linux_validation_verdict_report["terminal_verdict_should_proceed"] + ), + "terminal_verdict_requires_manual_action": bool( + linux_validation_verdict_report["terminal_verdict_requires_manual_action"] + ), + "terminal_verdict_channel": str(linux_validation_verdict_report["terminal_verdict_channel"]), + "linux_validation_should_dispatch": bool( + linux_validation_verdict_report["linux_validation_should_dispatch"] + ), + "linux_validation_requires_manual_action": bool( + linux_validation_verdict_report["linux_validation_requires_manual_action"] + ), + "linux_validation_channel": str(linux_validation_verdict_report["linux_validation_channel"]), + "linux_validation_dispatch_status": str( + linux_validation_verdict_report["linux_validation_dispatch_status"] + ), + "linux_validation_dispatch_decision": str( + linux_validation_verdict_report["linux_validation_dispatch_decision"] + ), + "linux_validation_dispatch_exit_code": int( + linux_validation_verdict_report["linux_validation_dispatch_exit_code"] + ), + "linux_validation_dispatch_attempted": bool( + linux_validation_verdict_report["linux_validation_dispatch_attempted"] + ), + "linux_validation_dispatch_command_returncode": linux_validation_verdict_report[ + "linux_validation_dispatch_command_returncode" + ], + "linux_validation_verdict_status": verdict_status, + "linux_validation_verdict_decision": verdict_decision, + "linux_validation_verdict_exit_code": verdict_exit_code, + "linux_validation_passed": verdict_passed, + "linux_validation_verdict_requires_manual_action": verdict_requires_manual_action, + "linux_validation_verdict_channel": verdict_channel, + "linux_validation_verdict_attempted": bool( + linux_validation_verdict_report["linux_validation_verdict_attempted"] + ), + "linux_validation_verdict_timeout_seconds": int( + linux_validation_verdict_report["linux_validation_verdict_timeout_seconds"] + ), + "linux_validation_verdict_command": str( + linux_validation_verdict_report["linux_validation_verdict_command"] + ), + "linux_validation_verdict_command_parts": list( + linux_validation_verdict_report["linux_validation_verdict_command_parts"] + ), + "dry_run": bool(linux_validation_verdict_report["dry_run"]), + "verdict_command_returncode": linux_validation_verdict_report["verdict_command_returncode"], + "verdict_command_stdout_tail": str( + linux_validation_verdict_report["verdict_command_stdout_tail"] + ), + "verdict_command_stderr_tail": str( + linux_validation_verdict_report["verdict_command_stderr_tail"] + ), + "verdict_error_type": str(linux_validation_verdict_report["verdict_error_type"]), + "verdict_error_message": str(linux_validation_verdict_report["verdict_error_message"]), + "release_run_id": linux_validation_verdict_report["release_run_id"], + "release_run_url": str(linux_validation_verdict_report["release_run_url"]), + "follow_up_queue_url": str(linux_validation_verdict_report["follow_up_queue_url"]), + "linux_validation_dispatch_summary": str( + linux_validation_verdict_report["linux_validation_dispatch_summary"] + ), + "linux_validation_verdict_summary": str( + linux_validation_verdict_report["linux_validation_verdict_summary"] + ), + "linux_validation_verdict_publish_status": publish_status, + "linux_validation_verdict_publish_decision": publish_decision, + "linux_validation_verdict_publish_exit_code": publish_exit_code, + "linux_validation_verdict_publish_should_notify": publish_should_notify, + "linux_validation_verdict_publish_requires_manual_action": publish_requires_manual_action, + "linux_validation_verdict_publish_channel": publish_channel, + "linux_validation_verdict_publish_summary": summary, + "reason_codes": reason_codes, + "structural_issues": structural_issues, + "missing_artifacts": missing_artifacts, + "evidence_manifest": evidence_manifest, + } + + +def render_markdown_report(payload: dict[str, Any]) -> str: + lines = [ + "# Linux CI Workflow Linux Validation Verdict Publish Report", + "", + ( + "- Linux Validation Verdict Publish Status: " + f"**{str(payload['linux_validation_verdict_publish_status']).upper()}**" + ), + ( + "- Linux Validation Verdict Publish Decision: " + f"`{payload['linux_validation_verdict_publish_decision']}`" + ), + ( + "- Linux Validation Verdict Publish Exit Code: " + f"`{payload['linux_validation_verdict_publish_exit_code']}`" + ), + ( + "- Linux Validation Verdict Publish Should Notify: " + f"`{payload['linux_validation_verdict_publish_should_notify']}`" + ), + ( + "- Linux Validation Verdict Publish Requires Manual Action: " + f"`{payload['linux_validation_verdict_publish_requires_manual_action']}`" + ), + ( + "- Linux Validation Verdict Publish Channel: " + f"`{payload['linux_validation_verdict_publish_channel']}`" + ), + f"- Linux Validation Verdict Status: `{payload['linux_validation_verdict_status']}`", + f"- Follow-Up Queue URL: `{payload['follow_up_queue_url']}`", + f"- Release Run ID: `{payload['release_run_id']}`", + f"- Release Run URL: `{payload['release_run_url']}`", + f"- Generated At (UTC): `{payload['generated_at']}`", + "", + "## Summary", + "", + f"- {payload['linux_validation_verdict_publish_summary']}", + "", + "## Reason Codes", + ] + + reason_codes = payload["reason_codes"] + if reason_codes: + lines.extend(f"- `{item}`" for item in reason_codes) + else: + lines.append("- none") + + lines.extend(["", "## Structural Issues"]) + structural_issues = payload["structural_issues"] + if structural_issues: + lines.extend(f"- `{item}`" for item in structural_issues) + else: + lines.append("- none") + + lines.extend(["", "## Missing Artifacts"]) + missing_artifacts = payload["missing_artifacts"] + if missing_artifacts: + lines.extend(f"- `{item}`" for item in missing_artifacts) + else: + lines.append("- none") + + lines.append("") + return "\n".join(lines) + + +def build_github_output_values( + payload: dict[str, Any], *, output_json: Path, output_markdown: Path +) -> dict[str, str]: + release_run_id = payload["release_run_id"] + return { + "workflow_linux_validation_verdict_publish_status": str( + payload["linux_validation_verdict_publish_status"] + ), + "workflow_linux_validation_verdict_publish_decision": str( + payload["linux_validation_verdict_publish_decision"] + ), + "workflow_linux_validation_verdict_publish_exit_code": str( + payload["linux_validation_verdict_publish_exit_code"] + ), + "workflow_linux_validation_verdict_publish_should_notify": ( + "true" if payload["linux_validation_verdict_publish_should_notify"] else "false" + ), + "workflow_linux_validation_verdict_publish_requires_manual_action": ( + "true" + if payload["linux_validation_verdict_publish_requires_manual_action"] + else "false" + ), + "workflow_linux_validation_verdict_publish_channel": str( + payload["linux_validation_verdict_publish_channel"] + ), + "workflow_linux_validation_verdict_publish_follow_up_queue_url": str( + payload["follow_up_queue_url"] + ), + "workflow_linux_validation_verdict_publish_run_id": ( + "" if release_run_id is None else str(release_run_id) + ), + "workflow_linux_validation_verdict_publish_run_url": str(payload["release_run_url"]), + "workflow_linux_validation_verdict_publish_reason_codes": json.dumps( + payload["reason_codes"], + ensure_ascii=False, + separators=(",", ":"), + ), + "workflow_linux_validation_verdict_publish_report_json": str(output_json), + "workflow_linux_validation_verdict_publish_report_markdown": str(output_markdown), + } + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Publish Linux CI workflow Linux validation verdict publish contract " + "from P2-61 Linux validation verdict report" + ) + ) + parser.add_argument( + "--linux-validation-verdict-report", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json", + help="P2-61 Linux validation verdict report JSON path", + ) + parser.add_argument( + "--output-json", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json", + help="Output Linux validation verdict publish JSON path", + ) + parser.add_argument( + "--output-markdown", + default=".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md", + help="Output Linux validation verdict publish markdown report path", + ) + parser.add_argument( + "--github-output", + default=None, + help="Optional GitHub Actions output file path", + ) + parser.add_argument( + "--print-report", + action="store_true", + help="Print Linux validation verdict publish JSON payload", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and print summary without writing output files", + ) + args = parser.parse_args() + + verdict_report_path = Path(args.linux_validation_verdict_report) + output_json_path = Path(args.output_json) + output_markdown_path = Path(args.output_markdown) + + try: + verdict_report = load_linux_validation_verdict_report(verdict_report_path) + payload = build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_report_path.resolve(), + ) + except ValueError as exc: + print( + "[p2-linux-ci-workflow-linux-validation-verdict-publish-gate] " + f"{exc}", + file=sys.stderr, + ) + return 2 + + if not args.dry_run: + output_json_path.parent.mkdir(parents=True, exist_ok=True) + output_json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + output_markdown_path.parent.mkdir(parents=True, exist_ok=True) + output_markdown_path.write_text(render_markdown_report(payload), encoding="utf-8") + print(f"linux validation verdict publish json: {output_json_path}") + print(f"linux validation verdict publish markdown: {output_markdown_path}") + + if args.github_output: + write_github_output( + Path(args.github_output), + build_github_output_values( + payload, + output_json=output_json_path, + output_markdown=output_markdown_path, + ), + ) + print(f"github output: {args.github_output}") + + print( + "linux validation verdict publish summary: " + f"linux_validation_verdict_publish_status=" + f"{payload['linux_validation_verdict_publish_status']} " + f"linux_validation_verdict_publish_decision=" + f"{payload['linux_validation_verdict_publish_decision']} " + f"linux_validation_verdict_publish_exit_code=" + f"{payload['linux_validation_verdict_publish_exit_code']}" + ) + if args.print_report or args.dry_run: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + return int(payload["linux_validation_verdict_publish_exit_code"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_multi_agent_supervisor_gate.py b/scripts/run_p2_multi_agent_supervisor_gate.py new file mode 100644 index 0000000..c4bdf46 --- /dev/null +++ b/scripts/run_p2_multi_agent_supervisor_gate.py @@ -0,0 +1,30 @@ +"""Phase 2 card P2-01 gate runner for multi-agent supervisor orchestration. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_multi_agent_supervisor_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_tasks_manager_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_tools_registry_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-01 multi-agent supervisor gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_p2_org_policy_audit_gate.py b/scripts/run_p2_org_policy_audit_gate.py new file mode 100644 index 0000000..7f4b631 --- /dev/null +++ b/scripts/run_p2_org_policy_audit_gate.py @@ -0,0 +1,30 @@ +"""Phase 2 card P2-05 gate runner for org policy and audit controls. + +This script only defines Linux-stage test command list and does not run tests. +""" + +from __future__ import annotations + +from pathlib import Path + + +def build_gate_commands(project_root: Path) -> list[str]: + return [ + "python -m pytest -q -c pytest.ini tests/test_org_policy_audit_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_server_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_hook_permission_audit_runtime.py", + "python -m pytest -q -c pytest.ini tests/test_main_runtime.py", + ] + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + commands = build_gate_commands(root) + print("P2-05 org policy and audit gate commands (Linux execution stage):") + for idx, command in enumerate(commands, start=1): + print(f"{idx}. {command}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_phase0_gate.py b/scripts/run_phase0_gate.py new file mode 100644 index 0000000..bcd56b1 --- /dev/null +++ b/scripts/run_phase0_gate.py @@ -0,0 +1,126 @@ +"""Phase 0 runtime regression gate entrypoint. + +Runs the curated Phase 0 runtime regression test suite with one command. +This script is designed for local pre-merge checks and CI job reuse. +""" + +from __future__ import annotations + +import argparse +import shlex +import subprocess +import sys +from pathlib import Path +from typing import Sequence + + +PHASE0_GATE_TEST_FILES: tuple[str, ...] = ( + "tests/test_runtime_bootstrap.py", + "tests/test_main_runtime.py", + "tests/test_resume_runtime.py", + "tests/test_commands_auth_runtime.py", + "tests/test_features_runtime.py", + "tests/test_review_command_runtime.py", + "tests/test_session_history_memory_runtime.py", + "tests/test_context_builder_runtime.py", + "tests/test_hooks_runtime.py", + "tests/test_doctor_runtime.py", + "tests/test_plugins_runtime.py", + "tests/test_phase0_regression_contract.py", +) + + +def build_phase0_pytest_command( + *, + python_executable: str | None = None, + extra_pytest_args: Sequence[str] | None = None, +) -> list[str]: + """Build subprocess command for Phase 0 gate.""" + python_bin = python_executable or sys.executable + cmd: list[str] = [ + python_bin, + "-m", + "pytest", + "-c", + "pytest.ini", + *PHASE0_GATE_TEST_FILES, + ] + if extra_pytest_args: + cmd.extend(extra_pytest_args) + return cmd + + +def _validate_gate_file_manifest() -> list[str]: + """Return missing test files from manifest.""" + missing: list[str] = [] + for test_file in PHASE0_GATE_TEST_FILES: + if not Path(test_file).is_file(): + missing.append(test_file) + return missing + + +def _format_shell_command(parts: Sequence[str]) -> str: + return " ".join(shlex.quote(part) for part in parts) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run Phase 0 regression gate tests") + parser.add_argument( + "--python-executable", + default=sys.executable, + help="Python executable to use (default: current interpreter)", + ) + parser.add_argument( + "--list-tests", + action="store_true", + help="List curated Phase 0 test files and exit", + ) + parser.add_argument( + "--print-command", + action="store_true", + help="Print the underlying pytest command and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate manifest and show command without executing tests", + ) + parser.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Extra args forwarded to pytest after '--'", + ) + args = parser.parse_args() + + if args.list_tests: + for test_file in PHASE0_GATE_TEST_FILES: + print(test_file) + return 0 + + missing = _validate_gate_file_manifest() + if missing: + for path in missing: + print(f"[phase0-gate] missing test file: {path}", file=sys.stderr) + return 2 + + forwarded_args = list(args.pytest_args) + if forwarded_args and forwarded_args[0] == "--": + forwarded_args = forwarded_args[1:] + + command = build_phase0_pytest_command( + python_executable=args.python_executable, + extra_pytest_args=forwarded_args, + ) + + if args.print_command or args.dry_run: + print(_format_shell_command(command)) + if args.print_command and not args.dry_run: + return 0 + if args.dry_run: + return 0 + + return subprocess.call(command) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/setup.py b/setup.py index 7a3156d..c0624f8 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,10 @@ -from setuptools import setup, find_packages +"""Legacy setuptools entrypoint. -with open("README.md", "r", encoding="utf-8") as f: - long_description = f.read() +Primary project metadata lives in ``pyproject.toml``. +This file is kept as a thin compatibility shim for tooling that still +expects ``setup.py`` to exist. +""" -with open("requirements.txt", "r", encoding="utf-8") as f: - requirements = [ - line.strip() - for line in f - if line.strip() and not line.startswith("#") - ] +from setuptools import setup -setup( - name="claude-code-python", - version="1.0.0", - author="Claude Code Python Project", - author_email="example@example.com", - description="AI Programming Assistant CLI - Python implementation", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/your-repo/claude-code-python", - packages=find_packages(exclude=["tests", "examples"]), - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - python_requires=">=3.10", - install_requires=requirements, - entry_points={ - "console_scripts": [ - "claude=claude_code.main:main", - ], - "gui_scripts": [ - "claude-gui=claude_code.gui:main", - ], - }, - include_package_data=True, - zip_safe=False, -) +setup() diff --git a/tests/.tmp_context/case_3b4ad4ccb2f5493199410def62a7d1b4/root/CLAUDE.md b/tests/.tmp_context/case_3b4ad4ccb2f5493199410def62a7d1b4/root/CLAUDE.md new file mode 100644 index 0000000..28c4e5a --- /dev/null +++ b/tests/.tmp_context/case_3b4ad4ccb2f5493199410def62a7d1b4/root/CLAUDE.md @@ -0,0 +1 @@ +root-level instructions \ No newline at end of file diff --git a/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.json b/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.json new file mode 100644 index 0000000..744385f --- /dev/null +++ b/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.json @@ -0,0 +1 @@ +{"id": "local-demo", "name": "Local Demo", "version": "0.1.0", "description": "runtime test plugin", "entry": "plugin.py"} \ No newline at end of file diff --git a/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.py b/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.py new file mode 100644 index 0000000..5d489c6 --- /dev/null +++ b/tests/.tmp_plugins/plugin_5a6cc573889e4c2e9635d04a26774795/plugin.py @@ -0,0 +1,10 @@ +class DemoPlugin: + def get_tools(self): + return ['tool-a'] + def get_commands(self): + return ['cmd-a'] + def get_hooks(self): + return {'event.a': [lambda payload: payload]} + +def get_plugin(): + return DemoPlugin() \ No newline at end of file diff --git a/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.json b/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.json new file mode 100644 index 0000000..744385f --- /dev/null +++ b/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.json @@ -0,0 +1 @@ +{"id": "local-demo", "name": "Local Demo", "version": "0.1.0", "description": "runtime test plugin", "entry": "plugin.py"} \ No newline at end of file diff --git a/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.py b/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.py new file mode 100644 index 0000000..5d489c6 --- /dev/null +++ b/tests/.tmp_plugins/plugin_e8a81aed45a44f149f4950e2ce5da7e1/plugin.py @@ -0,0 +1,10 @@ +class DemoPlugin: + def get_tools(self): + return ['tool-a'] + def get_commands(self): + return ['cmd-a'] + def get_hooks(self): + return {'event.a': [lambda payload: payload]} + +def get_plugin(): + return DemoPlugin() \ No newline at end of file diff --git a/tests/test_active_memory_runtime.py b/tests/test_active_memory_runtime.py new file mode 100644 index 0000000..e9fff62 --- /dev/null +++ b/tests/test_active_memory_runtime.py @@ -0,0 +1,119 @@ +"""Runtime tests for P1-04 active memory integration.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +import claude_code.main as main_mod +from claude_code.engine.query import QueryConfig, QueryEngine +from claude_code.services.event_journal import EventJournal +from claude_code.services.memory_service import MemoryEntry, SessionMemory + + +class _SingleTurnApiClient: + """Deterministic API client for query-engine runtime tests.""" + + async def create_message_streaming(self, messages, options): + yield SimpleNamespace( + type="message", + content={"content": [{"type": "text", "text": "ack"}]}, + usage=None, + error=None, + ) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "ack"}]) + + +@pytest.mark.asyncio +async def test_query_engine_records_memory_hit_event(tmp_path): + memory = SessionMemory(storage_dir=tmp_path / "memory") + await memory.set_scoped( + "project", + "architecture", + "sqlite runtime backend", + working_directory=tmp_path, + ) + journal = EventJournal(tmp_path / "events.jsonl") + engine = QueryEngine(api_client=_SingleTurnApiClient(), config=QueryConfig()) + engine.memory = memory + engine.event_journal = journal + engine.config.working_directory = str(tmp_path) + engine.config.memory_scope = "project" + + async for _ in engine.query("please explain sqlite architecture"): + pass + + events = journal.query_events(event_types=["memory.hit"], limit=20) + assert events + payload = events[0].payload + assert payload.get("scope") == "project" + assert "architecture" in payload.get("matched_keys", []) + + +def test_build_system_prompt_includes_scoped_memory_snapshot(monkeypatch, tmp_path): + memory = SessionMemory(storage_dir=tmp_path / "memory") + memory._memory.clear() # type: ignore[attr-defined] # isolate test from previous state + key = SessionMemory._scoped_key("project", "roadmap", working_directory=tmp_path) + memory._memory[key] = MemoryEntry(key=key, value="P1-04 active memory") # type: ignore[attr-defined] + + monkeypatch.setattr(main_mod, "get_memory", lambda: memory) + monkeypatch.setenv("CLAUDE_MEMORY_SCOPE", "project") + + prompt = main_mod.build_system_prompt(str(tmp_path)) + + assert "Active memory snapshot:" in prompt + assert "scope: project" in prompt + assert "roadmap: P1-04 active memory" in prompt + + +def test_create_runtime_propagates_memory_scope_from_env(monkeypatch, tmp_path): + captured = {} + + class _FakeSession: + def __init__(self): + self.id = "session-xyz" + self.metadata = type( + "_Metadata", + (), + {"working_directory": "D:/initial", "model": "claude-initial"}, + )() + + class _FakeSessionManager: + @classmethod + def from_store(cls, store): + return cls() + + def ensure_current_session(self): + return _FakeSession() + + class _FakeQueryEngine: + def __init__(self, api_client, config, tool_registry): + captured["config"] = config + self.api_client = api_client + self.config = config + self.tool_registry = tool_registry + + monkeypatch.setenv("CLAUDE_MEMORY_SCOPE", "user") + monkeypatch.setattr(main_mod, "get_config", lambda: main_mod.Config(model="claude-test-model")) + monkeypatch.setattr(main_mod, "setup_api_client", lambda app_config=None: object()) + monkeypatch.setattr(main_mod, "create_default_registry", lambda: object()) + monkeypatch.setattr(main_mod, "QueryEngine", _FakeQueryEngine) + monkeypatch.setattr(main_mod, "SessionManager", _FakeSessionManager) + monkeypatch.setattr(main_mod, "SQLiteSessionStore", lambda db_path=None: object()) + monkeypatch.setattr(main_mod, "HistoryManager", lambda storage_path=None: object()) + monkeypatch.setattr(main_mod, "SQLiteEventJournal", lambda db_path=None: object()) + monkeypatch.setattr( + main_mod, + "create_task_manager_with_event_journal", + lambda config, event_journal: object(), + ) + monkeypatch.setattr(main_mod, "HooksManager", lambda config_path=None: object()) + monkeypatch.setattr(main_mod, "get_memory", lambda: SessionMemory(storage_dir=tmp_path / "memory")) + monkeypatch.setattr(main_mod, "_create_application", lambda app_config: object()) + + main_mod.create_runtime(model="claude-custom-model", working_dir="D:/runtime") + + assert captured["config"].memory_scope == "user" diff --git a/tests/test_agent_background_context_runtime.py b/tests/test_agent_background_context_runtime.py new file mode 100644 index 0000000..f79283b --- /dev/null +++ b/tests/test_agent_background_context_runtime.py @@ -0,0 +1,155 @@ +"""Runtime tests for AgentTool background/sync context inheritance consistency.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from claude_code.tasks.manager import TaskManager +from claude_code.tasks.queue import InMemoryTaskQueue +from claude_code.tools.agent import AgentTool +from claude_code.tools.base import ToolContext + + +class _FakeStreamEvent: + def __init__(self, content: list[dict]) -> None: + self.type = "message" + self.content = {"content": content} + self.usage = None + self.error = None + + +class _FakeApiClient: + async def create_message_streaming(self, messages, options): + yield _FakeStreamEvent([{"type": "text", "text": "agent-ok"}]) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "agent-ok"}]) + + +@pytest.mark.asyncio +async def test_agent_tool_sync_path_inherits_context_and_registry(monkeypatch): + tool = AgentTool() + fake_registry = object() + + captured: dict[str, object] = {} + + class _SpyQueryEngine: + def __init__(self, api_client, config, tool_registry): + captured["config"] = config + captured["tool_registry"] = tool_registry + + async def query(self, prompt: str): + yield {"type": "text", "content": "sync-ok"} + + monkeypatch.setattr("claude_code.main.setup_api_client", lambda cfg=None: _FakeApiClient()) + monkeypatch.setattr("claude_code.engine.query.QueryEngine", _SpyQueryEngine) + + context = ToolContext( + working_directory="D:/workspace/main", + environment={}, + permission_mode="plan", + always_allow=["read"], + always_deny=["write"], + model="qwen2.5:14b", + session_id="session-sync", + conversation_id="conv-sync", + memory_scope="local", + api_provider="ollama", + task_manager=TaskManager(task_queue=InMemoryTaskQueue()), + tool_registry=fake_registry, + ) + + result = await tool.execute( + { + "description": "sync context check", + "prompt": "verify sync context", + "subagent_type": "general-purpose", + "run_in_background": False, + "cwd": "D:/workspace/sync-agent", + }, + context, + ) + + assert result.is_error is False + assert "sync-ok" in str(result.content) + cfg = captured["config"] + assert getattr(cfg, "working_directory") == "D:/workspace/sync-agent" + assert getattr(cfg, "model") == "qwen2.5:14b" + assert getattr(cfg, "permission_mode") == "plan" + assert getattr(cfg, "memory_scope") == "project" + assert captured["tool_registry"] is fake_registry + + +@pytest.mark.asyncio +async def test_agent_tool_background_path_inherits_context_and_task_metadata(monkeypatch): + tool = AgentTool() + manager = TaskManager(task_queue=InMemoryTaskQueue()) + fake_registry = object() + + captured: dict[str, object] = {} + + class _SpyQueryEngine: + def __init__(self, api_client, config, tool_registry): + captured["bg_config"] = config + captured["bg_tool_registry"] = tool_registry + + async def query(self, prompt: str): + yield {"type": "text", "content": "background-ok"} + + monkeypatch.setattr("claude_code.main.setup_api_client", lambda cfg=None: _FakeApiClient()) + monkeypatch.setattr("claude_code.engine.query.QueryEngine", _SpyQueryEngine) + + context = ToolContext( + working_directory="D:/workspace/main", + environment={}, + permission_mode="plan", + always_allow=["read"], + always_deny=["write"], + model="qwen2.5:14b", + session_id="session-bg", + conversation_id="conv-bg", + memory_scope="local", + api_provider="ollama", + task_manager=manager, + tool_registry=fake_registry, + ) + + result = await tool.execute( + { + "description": "background context check", + "prompt": "verify background context", + "subagent_type": "general-purpose", + "run_in_background": True, + "cwd": "D:/workspace/bg-agent", + }, + context, + ) + + assert result.is_error is False + assert "Started background agent:" in str(result.content) + + tasks = manager.get_all_tasks() + assert len(tasks) == 1 + task = tasks[0] + assert task.metadata["session_id"] == "session-bg" + assert task.metadata["conversation_id"] == "conv-bg" + assert task.metadata["working_directory"] == "D:/workspace/bg-agent" + assert task.metadata["permission_mode"] == "plan" + assert task.metadata["memory_scope"] == "project" + assert task.metadata["provider"] == "ollama" + assert task.metadata["model"] == "qwen2.5:14b" + assert task.metadata["source_tool"] == "agent" + + assert getattr(captured["bg_config"], "working_directory") == "D:/workspace/bg-agent" + assert getattr(captured["bg_config"], "model") == "qwen2.5:14b" + assert getattr(captured["bg_config"], "session_id") == "session-bg" + assert getattr(captured["bg_config"], "permission_mode") == "plan" + assert getattr(captured["bg_config"], "memory_scope") == "project" + assert captured["bg_tool_registry"] is fake_registry + + await manager.wait_for_task(task.id, timeout=2.0) + assert task.status.value == "completed" + assert task.result is not None + assert "background-ok" in task.result.stdout diff --git a/tests/test_artifact_bus_runtime.py b/tests/test_artifact_bus_runtime.py new file mode 100644 index 0000000..1eaa9f7 --- /dev/null +++ b/tests/test_artifact_bus_runtime.py @@ -0,0 +1,76 @@ +"""Runtime tests for P2-02 artifact bus schema and merge semantics.""" + +from __future__ import annotations + +import pytest + +from claude_code.services.artifact_bus import ( + ArtifactBus, + ArtifactConflictError, + ArtifactSchemaError, +) + + +def test_artifact_bus_schema_validation_rejects_invalid_type(): + bus = ArtifactBus() + with pytest.raises(ArtifactSchemaError, match="type must be one of"): + bus.publish( + node_id="n1", + payload={ + "artifacts": [ + {"type": "unknown", "key": "x", "content": "v"}, + ] + }, + ) + + +def test_artifact_bus_producer_consumer_dependency_view(): + bus = ArtifactBus() + bus.publish( + node_id="producer", + payload={ + "artifacts": [ + {"type": "report", "key": "summary", "content": "done"}, + {"type": "note", "key": "ops", "content": "remember"}, + ] + }, + ) + + view = bus.build_dependency_view(["producer"]) + assert "producer" in view + bundle = view["producer"] + assert "artifacts" in bundle + assert "merged" in bundle + assert bundle["merged"]["report:summary"] == "done" + assert bundle["merged"]["note:ops"] == "remember" + + +def test_artifact_bus_append_merge_strategy_collects_conflicts(): + bus = ArtifactBus() + bus.publish( + node_id="n1", + payload={"artifacts": [{"type": "finding", "key": "lint", "content": "E1"}]}, + ) + bus.publish( + node_id="n2", + payload={ + "artifacts": [ + {"type": "finding", "key": "lint", "content": "E2", "merge": "append"} + ] + }, + ) + snapshot = bus.snapshot() + assert snapshot["merged"]["finding:lint"] == ["E1", "E2"] + + +def test_artifact_bus_conflict_fail_strategy_raises(): + bus = ArtifactBus() + bus.publish( + node_id="a", + payload={"artifacts": [{"type": "patch", "key": "fileA", "content": "v1"}]}, + ) + with pytest.raises(ArtifactConflictError, match="artifact conflict"): + bus.publish( + node_id="b", + payload={"artifacts": [{"type": "patch", "key": "fileA", "content": "v2"}]}, + ) diff --git a/tests/test_cli_daemon_thin_client_runtime.py b/tests/test_cli_daemon_thin_client_runtime.py new file mode 100644 index 0000000..fde094e --- /dev/null +++ b/tests/test_cli_daemon_thin_client_runtime.py @@ -0,0 +1,43 @@ +"""Runtime tests for P1-06 CLI daemon thin-client migration helpers.""" + +from __future__ import annotations + +from argparse import Namespace + +import claude_code.main as main_mod + + +def test_resolve_daemon_client_mode_defaults_to_host_port_when_enabled(): + args = Namespace( + daemon_client=True, + daemon_url=None, + daemon_host="127.0.0.8", + daemon_port=9001, + daemon_timeout=12.5, + daemon_required=False, + ) + + mode = main_mod._resolve_daemon_client_mode(args) + + assert mode.enabled is True + assert mode.base_url == "http://127.0.0.8:9001" + assert mode.timeout_seconds == 12.5 + assert mode.required is False + + +def test_resolve_daemon_client_mode_prefers_explicit_url(): + args = Namespace( + daemon_client=False, + daemon_url="http://127.0.0.77:9900", + daemon_host="127.0.0.1", + daemon_port=8787, + daemon_timeout=30.0, + daemon_required=True, + ) + + mode = main_mod._resolve_daemon_client_mode(args) + + assert mode.enabled is True + assert mode.base_url == "http://127.0.0.77:9900" + assert mode.required is True + diff --git a/tests/test_commands_auth_runtime.py b/tests/test_commands_auth_runtime.py new file mode 100644 index 0000000..22579c5 --- /dev/null +++ b/tests/test_commands_auth_runtime.py @@ -0,0 +1,141 @@ +"""Runtime tests for auth commands and config/env key resolution.""" + +from __future__ import annotations + +import io +import os +from types import SimpleNamespace + +import pytest +from rich.console import Console + +import claude_code.commands.auth as auth_mod +from claude_code.commands.base import CommandContext + + +def _context(tmp_path) -> CommandContext: + return CommandContext( + working_directory=str(tmp_path), + console=Console(file=io.StringIO(), force_terminal=False, width=120), + engine=None, + session=None, + config=None, + ) + + +def _clear_auth_env(monkeypatch: pytest.MonkeyPatch) -> None: + for key in ( + "CLAUDE_API_PROVIDER", + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "AZURE_OPENAI_API_KEY", + ): + monkeypatch.delenv(key, raising=False) + + +@pytest.mark.asyncio +async def test_auth_status_prefers_env_key_over_config_key(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="anthropic", api_key="sk-ant-config-0000") + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", lambda: None) + monkeypatch.setenv("CLAUDE_API_PROVIDER", "anthropic") + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-env-9999") + + result = await auth_mod.AuthStatusCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Status: Logged in" in str(result.content) + assert "Source: environment variable" in str(result.content) + assert "sk-ant-env-9999" not in str(result.content) + assert "...9999" in str(result.content) + + +@pytest.mark.asyncio +async def test_auth_status_uses_config_key_for_matching_openai_family_provider(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="openai", api_key="sk-openai-config-1234") + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", lambda: None) + monkeypatch.setenv("CLAUDE_API_PROVIDER", "vllm") + + result = await auth_mod.AuthStatusCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Provider: vllm" in str(result.content) + assert "Source: config file" in str(result.content) + assert "sk-openai-config-1234" not in str(result.content) + assert "...1234" in str(result.content) + + +@pytest.mark.asyncio +async def test_auth_status_ignores_mismatched_config_key_provider(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="anthropic", api_key="sk-ant-config-2222") + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", lambda: None) + monkeypatch.setenv("CLAUDE_API_PROVIDER", "openai") + + result = await auth_mod.AuthStatusCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Status: Not logged in" in str(result.content) + assert "Expected Key: OPENAI_API_KEY" in str(result.content) + + +@pytest.mark.asyncio +async def test_login_command_reports_provider_specific_env_var(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="openai", api_key=None) + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", lambda: None) + monkeypatch.setenv("CLAUDE_API_PROVIDER", "openai") + + result = await auth_mod.LoginCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Active provider: openai" in str(result.content) + assert "OPENAI_API_KEY" in str(result.content) + + +@pytest.mark.asyncio +async def test_logout_clears_runtime_env_and_persisted_config_key(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="anthropic", api_key="sk-ant-config-4444") + save_called = {"value": False} + + def _save() -> None: + save_called["value"] = True + + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", _save) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-env-3333") + monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-env-3333") + + result = await auth_mod.LogoutCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Logged out successfully." in str(result.content) + assert "env:ANTHROPIC_API_KEY" in str(result.content) + assert "env:OPENAI_API_KEY" in str(result.content) + assert "config:api_key" in str(result.content) + assert config.api_key is None + assert save_called["value"] is True + assert os.getenv("ANTHROPIC_API_KEY") is None + assert os.getenv("OPENAI_API_KEY") is None + + +@pytest.mark.asyncio +async def test_auth_status_for_credential_based_provider(monkeypatch, tmp_path): + _clear_auth_env(monkeypatch) + config = SimpleNamespace(api_provider="bedrock", api_key=None) + monkeypatch.setattr(auth_mod, "get_config", lambda: config) + monkeypatch.setattr(auth_mod, "save_config", lambda: None) + monkeypatch.setenv("CLAUDE_API_PROVIDER", "bedrock") + + result = await auth_mod.AuthStatusCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Status: Credential-based" in str(result.content) + assert "Provider: bedrock" in str(result.content) + assert "API Key: not required" in str(result.content) diff --git a/tests/test_context_builder_runtime.py b/tests/test_context_builder_runtime.py new file mode 100644 index 0000000..40fe7c9 --- /dev/null +++ b/tests/test_context_builder_runtime.py @@ -0,0 +1,128 @@ +"""Runtime tests for ContextBuilder/ClaudeMdLoader behavior.""" + +from __future__ import annotations + +import os +import shutil +import stat +import uuid +from pathlib import Path + +import pytest + +from claude_code.engine.context import ClaudeMdLoader, ContextBuilder + + +def _cleanup_tree(path: Path) -> None: + """Best-effort recursive cleanup for Windows test temp directories.""" + + def _onerror(func, value, exc_info): + try: + os.chmod(value, stat.S_IWRITE) + func(value) + except Exception: + pass + + if path.exists(): + shutil.rmtree(path, onerror=_onerror) + + +def test_claude_md_loader_includes_parent_claude_md(): + base = Path("tests") / ".tmp_context" / f"case_{uuid.uuid4().hex}" + try: + root = base / "root" + nested = root / "sub" / "deeper" + nested.mkdir(parents=True, exist_ok=True) + (root / "CLAUDE.md").write_text("root-level instructions", encoding="utf-8") + loader = ClaudeMdLoader(nested) + files = loader.find_claude_md_files() + + assert files + content = loader.load_content() + assert content is not None + assert "root-level instructions" in content + finally: + _cleanup_tree(base) + + +def test_claude_md_loader_orders_files_from_root_to_current(): + base = Path("tests") / ".tmp_context" / f"order_{uuid.uuid4().hex}" + try: + root = base / "root" + sub = root / "sub" + nested = sub / "deep" + nested.mkdir(parents=True, exist_ok=True) + (root / "CLAUDE.md").write_text("root", encoding="utf-8") + (sub / "CLAUDE.md").write_text("sub", encoding="utf-8") + (nested / "CLAUDE.md").write_text("nested", encoding="utf-8") + + loader = ClaudeMdLoader(nested) + files = loader.find_claude_md_files() + + assert files == [root / "CLAUDE.md", sub / "CLAUDE.md", nested / "CLAUDE.md"] + + content = loader.load_content() + assert content is not None + assert content.index("root") < content.index("sub") < content.index("nested") + finally: + _cleanup_tree(base) + + +def test_claude_md_loader_skips_single_unreadable_file(monkeypatch: pytest.MonkeyPatch): + base = Path("tests") / ".tmp_context" / f"unreadable_{uuid.uuid4().hex}" + try: + root = base / "root" + nested = root / "nested" + nested.mkdir(parents=True, exist_ok=True) + root_md = root / "CLAUDE.md" + nested_md = nested / "CLAUDE.md" + root_md.write_text("root text", encoding="utf-8") + nested_md.write_text("nested text", encoding="utf-8") + + original_read_text = Path.read_text + + def _patched_read_text(self: Path, *args, **kwargs): + if self == root_md: + raise OSError("permission denied") + return original_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _patched_read_text) + + loader = ClaudeMdLoader(nested) + content = loader.load_content() + + assert content is not None + assert "nested text" in content + assert "root text" not in content + finally: + _cleanup_tree(base) + + +def test_context_builder_user_context_keeps_session_history_memory_boundaries(): + base = Path("tests") / ".tmp_context" / f"context_{uuid.uuid4().hex}" + try: + base.mkdir(parents=True, exist_ok=True) + context = ContextBuilder(base).get_user_context() + + assert "currentDate" in context + assert "claudeMd" not in context + assert "session" not in context + assert "history" not in context + assert "memory" not in context + finally: + _cleanup_tree(base) + + +def test_context_builder_includes_claude_md_without_injecting_history_or_memory(): + base = Path("tests") / ".tmp_context" / f"context_md_{uuid.uuid4().hex}" + try: + base.mkdir(parents=True, exist_ok=True) + (base / "CLAUDE.md").write_text("project instructions", encoding="utf-8") + context = ContextBuilder(base).get_user_context() + + assert "claudeMd" in context + assert "project instructions" in str(context["claudeMd"]) + assert "history" not in context + assert "memory" not in context + finally: + _cleanup_tree(base) diff --git a/tests/test_custom_agents_loader_runtime.py b/tests/test_custom_agents_loader_runtime.py new file mode 100644 index 0000000..6d9f7e2 --- /dev/null +++ b/tests/test_custom_agents_loader_runtime.py @@ -0,0 +1,202 @@ +"""Runtime tests for custom agents loader and AgentTool integration.""" + +from __future__ import annotations + +import pytest +from claude_code.agents.loader import load_agents_from_directory +from claude_code.tools.agent import AgentTool +from claude_code.tools.base import ToolContext + + +def test_load_agents_from_directory_parses_and_overrides_nested_agents(tmp_path): + root = tmp_path / "repo" + root.mkdir() + (root / ".git").mkdir() + + root_agents = root / ".claude" / "agents" + root_agents.mkdir(parents=True) + nested_agents = root / "sub" / ".claude" / "agents" + nested_agents.mkdir(parents=True) + + (root_agents / "shared.md").write_text( + "---\n" + "name: shared\n" + "description: root shared\n" + "model: haiku\n" + "tools:\n" + " - Read\n" + "memory: user\n" + "---\n" + "root prompt\n", + encoding="utf-8", + ) + (nested_agents / "shared.md").write_text( + "---\n" + "name: shared\n" + "description: nested shared\n" + "model: sonnet\n" + "tools:\n" + " - Bash\n" + "disallowedTools:\n" + " - Edit\n" + "memory: project\n" + "---\n" + "nested prompt\n", + encoding="utf-8", + ) + (nested_agents / "local-only.md").write_text( + "---\n" + "name: local-only\n" + "description: nested only\n" + "permissionMode: plan\n" + "maxTurns: 2\n" + "omitClaudeMd: true\n" + "---\n" + "local prompt\n", + encoding="utf-8", + ) + (nested_agents / "invalid.md").write_text( + "no frontmatter content", + encoding="utf-8", + ) + + loaded = load_agents_from_directory(root / "sub") + + assert "shared" in loaded + assert "local-only" in loaded + assert "invalid" not in loaded + assert loaded["shared"].description == "nested shared" + assert loaded["shared"].prompt == "nested prompt" + assert loaded["shared"].model == "sonnet" + assert loaded["shared"].tools == ["Bash"] + assert loaded["shared"].disallowed_tools == ["Edit"] + assert loaded["shared"].memory == "project" + assert loaded["local-only"].permission_mode == "plan" + assert loaded["local-only"].max_turns == 2 + assert loaded["local-only"].omit_claude_md is True + + +def test_load_agents_from_directory_handles_invalid_yaml_and_missing_required_fields(tmp_path): + root = tmp_path / "repo" + root.mkdir() + (root / ".git").mkdir() + agents_dir = root / ".claude" / "agents" + agents_dir.mkdir(parents=True) + + (agents_dir / "broken-yaml.md").write_text( + "---\n" + "name: broken\n" + "description: [unclosed\n" + "---\n" + "prompt\n", + encoding="utf-8", + ) + (agents_dir / "missing-description.md").write_text( + "---\n" + "name: missing\n" + "---\n" + "prompt\n", + encoding="utf-8", + ) + + loaded = load_agents_from_directory(root) + assert loaded == {} + + +@pytest.mark.asyncio +async def test_agent_tool_prefers_custom_agent_over_builtin_with_same_name(tmp_path, monkeypatch): + root = tmp_path / "repo" + root.mkdir() + (root / ".git").mkdir() + agents_dir = root / ".claude" / "agents" + agents_dir.mkdir(parents=True) + (agents_dir / "general-purpose.md").write_text( + "---\n" + "name: general-purpose\n" + "description: override default\n" + "model: haiku\n" + "memory: local\n" + "---\n" + "custom override prompt\n", + encoding="utf-8", + ) + + tool = AgentTool() + context = ToolContext( + working_directory=str(root), + environment={}, + ) + + captured: dict[str, str] = {} + + async def _fake_run_sync_agent(*, agent_def, **kwargs): # type: ignore[no-untyped-def] + captured["agent_prompt"] = agent_def.prompt + captured["agent_memory"] = str(agent_def.memory) + captured["agent_model"] = agent_def.model + from claude_code.tools.base import ToolResult + + return ToolResult(content="ok", is_error=False) + + monkeypatch.setattr(tool, "_run_sync_agent", _fake_run_sync_agent) + + result = await tool.execute( + { + "description": "run custom", + "prompt": "test custom", + "subagent_type": "general-purpose", + "run_in_background": False, + }, + context, + ) + assert result.is_error is False + assert captured["agent_prompt"] == "custom override prompt" + assert captured["agent_memory"] == "local" + assert captured["agent_model"] == "haiku" + + +@pytest.mark.asyncio +async def test_agent_tool_unknown_type_includes_custom_and_builtin_names(tmp_path): + root = tmp_path / "repo" + root.mkdir() + (root / ".git").mkdir() + agents_dir = root / ".claude" / "agents" + agents_dir.mkdir(parents=True) + (agents_dir / "my-custom.md").write_text( + "---\n" + "name: my-custom\n" + "description: custom agent\n" + "---\n" + "custom prompt\n", + encoding="utf-8", + ) + + tool = AgentTool() + context = ToolContext( + working_directory=str(root), + environment={}, + ) + + result = await tool.execute( + { + "description": "run unknown", + "prompt": "test unknown", + "subagent_type": "does-not-exist", + "run_in_background": False, + }, + context, + ) + + assert result.is_error is True + message = str(result.content) + assert "Unknown agent type: does-not-exist" in message + assert "my-custom" in message + assert "general-purpose" in message + + +def test_agent_tool_schema_allows_custom_subagent_type_names(): + tool = AgentTool() + schema = tool.input_schema + subagent_schema = schema["properties"]["subagent_type"] + + assert subagent_schema["type"] == "string" + assert "enum" not in subagent_schema diff --git a/tests/test_doctor_runtime.py b/tests/test_doctor_runtime.py new file mode 100644 index 0000000..103b94c --- /dev/null +++ b/tests/test_doctor_runtime.py @@ -0,0 +1,133 @@ +"""Runtime tests for doctor interpreter diagnostics on Windows bootstrap paths.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +import claude_code.main as main_mod + + +def test_interpreter_path_classification_detects_windowsapps_stub(): + stub_path = r"C:\Users\demo\AppData\Local\Microsoft\WindowsApps\python.exe" + assert main_mod._classify_interpreter_path(stub_path, platform_name="win32") == "windowsapps_stub" + assert main_mod._is_windowsapps_stub(stub_path, platform_name="win32") is True + + +def test_collect_interpreter_diagnostics_windowsapps_stub_regression(): + diagnostics = main_mod._collect_interpreter_diagnostics( + platform_name="win32", + executable_path=r"C:\Program Files\Python311\python.exe", + python_on_path=r"C:\Users\demo\AppData\Local\Microsoft\WindowsApps\python.exe", + ) + + assert diagnostics.python_on_path_source == "windowsapps_stub" + assert diagnostics.has_windowsapps_stub_risk is True + assert diagnostics.warning is not None + assert "WindowsApps python.exe stub detected" in diagnostics.warning + assert "py -3 -m claude_code.main" in diagnostics.recommended_launcher + + +def test_collect_interpreter_diagnostics_virtualenv_has_no_stub_risk(): + diagnostics = main_mod._collect_interpreter_diagnostics( + platform_name="win32", + executable_path=r"D:\repo\.venv\Scripts\python.exe", + python_on_path=r"D:\repo\.venv\Scripts\python.exe", + ) + + assert diagnostics.executable_source == "virtualenv" + assert diagnostics.python_on_path_source == "virtualenv" + assert diagnostics.has_windowsapps_stub_risk is False + assert diagnostics.warning is None + + +class _FakeTable: + last_instance = None + + def __init__(self, *args, **kwargs): + self.rows: list[tuple[str, str, str]] = [] + _FakeTable.last_instance = self + + def add_column(self, *args, **kwargs): + return None + + def add_row(self, *args): + self.rows.append(tuple(str(item) for item in args)) + + +class _FakeConsole: + last_instance = None + + def __init__(self, *args, **kwargs): + self.printed: list[object] = [] + _FakeConsole.last_instance = self + + def print(self, obj): + self.printed.append(obj) + + +@pytest.mark.asyncio +async def test_run_doctor_outputs_warn_branch_for_stub_risk(monkeypatch): + import rich.console as rich_console + import rich.table as rich_table + + monkeypatch.setattr(rich_console, "Console", _FakeConsole) + monkeypatch.setattr(rich_table, "Table", _FakeTable) + monkeypatch.setattr( + main_mod, + "_collect_interpreter_diagnostics", + lambda: main_mod.InterpreterDiagnostics( + executable=r"C:\Python311\python.exe", + python_on_path=r"C:\Users\demo\AppData\Local\Microsoft\WindowsApps\python.exe", + executable_source="system", + python_on_path_source="windowsapps_stub", + has_windowsapps_stub_risk=True, + warning="WindowsApps python.exe stub detected in python on PATH.", + recommended_launcher="Use `py -3 -m claude_code.main`.", + ), + ) + monkeypatch.setattr(main_mod, "ContextBuilder", lambda cwd: SimpleNamespace(get_system_context=lambda: {"gitStatus": "ok"})) + monkeypatch.setattr(main_mod, "create_runtime", lambda working_dir=None: SimpleNamespace(query_engine=object())) + monkeypatch.setattr(main_mod.os, "getcwd", lambda: "D:/workspace") + monkeypatch.setattr(main_mod.sys, "platform", "win32") + + await main_mod.run_doctor() + + rows = _FakeTable.last_instance.rows + assert any(row[0] == "Interpreter" and row[1] == "WARN" for row in rows) + assert any(row[0] == "Interpreter Risk" and row[1] == "WARN" for row in rows) + assert any(row[0] == "Windows Launcher" and row[1] == "INFO" for row in rows) + + +@pytest.mark.asyncio +async def test_run_doctor_outputs_ok_branch_without_stub_risk(monkeypatch): + import rich.console as rich_console + import rich.table as rich_table + + monkeypatch.setattr(rich_console, "Console", _FakeConsole) + monkeypatch.setattr(rich_table, "Table", _FakeTable) + monkeypatch.setattr( + main_mod, + "_collect_interpreter_diagnostics", + lambda: main_mod.InterpreterDiagnostics( + executable="/usr/bin/python3", + python_on_path="/usr/bin/python3", + executable_source="system", + python_on_path_source="system", + has_windowsapps_stub_risk=False, + warning=None, + recommended_launcher="Use `python -m claude_code.main`.", + ), + ) + monkeypatch.setattr(main_mod, "ContextBuilder", lambda cwd: SimpleNamespace(get_system_context=lambda: {"gitStatus": "ok"})) + monkeypatch.setattr(main_mod, "create_runtime", lambda working_dir=None: SimpleNamespace(query_engine=object())) + monkeypatch.setattr(main_mod.os, "getcwd", lambda: "/workspace") + monkeypatch.setattr(main_mod.sys, "platform", "linux") + + await main_mod.run_doctor() + + rows = _FakeTable.last_instance.rows + assert any(row[0] == "Interpreter" and row[1] == "OK" for row in rows) + assert not any(row[0] == "Interpreter Risk" for row in rows) + assert not any(row[0] == "Windows Launcher" for row in rows) diff --git a/tests/test_event_journal_runtime.py b/tests/test_event_journal_runtime.py new file mode 100644 index 0000000..45b0b72 --- /dev/null +++ b/tests/test_event_journal_runtime.py @@ -0,0 +1,224 @@ +"""Runtime tests for P1-02 event journal contracts.""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from claude_code.engine.query import QueryConfig, QueryEngine, ToolCallResult +from claude_code.services.event_journal import ( + EVENT_JOURNAL_SCHEMA_KEY, + EVENT_JOURNAL_SCHEMA_VERSION, + EventJournal, + EventJournalEntry, + SQLiteEventJournal, +) +from claude_code.tasks.manager import TaskEvent, TaskManager +from claude_code.tasks.queue import InMemoryTaskQueue +from claude_code.tasks.types import TaskResult +from claude_code.tools.base import Tool, ToolContext, ToolResult +from claude_code.tools.registry import ToolRegistry + + +class _FakeStreamEvent: + def __init__(self, event_type: str, content: dict | None = None) -> None: + self.type = event_type + self.content = content + self.usage = None + self.error = None + + +class _TwoTurnToolApiClient: + def __init__(self, tool_name: str) -> None: + self._tool_name = tool_name + self._count = 0 + + async def create_message_streaming(self, messages, options): + self._count += 1 + if self._count == 1: + yield _FakeStreamEvent( + "message", + { + "content": [ + { + "type": "tool_use", + "id": "tool-1", + "name": self._tool_name, + "input": {"text": "hello"}, + } + ] + }, + ) + else: + yield _FakeStreamEvent("message", {"content": [{"type": "text", "text": "done"}]}) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "done"}]) + + +class _EchoTool(Tool): + @property + def name(self) -> str: + return "echo_tool" + + @property + def description(self) -> str: + return "echo text" + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=input_data.get("text", ""), is_error=False, tool_use_id="") + + +def test_event_journal_entry_schema_versioning_and_validation(): + entry = EventJournalEntry.from_dict( + { + "event_id": "evt-1", + "version": EVENT_JOURNAL_SCHEMA_VERSION, + "event_type": "query.started", + "sequence": 1, + "timestamp": 1700000000.0, + "payload": {"x": 1}, + "metadata": {"m": 2}, + } + ) + assert entry.version == EVENT_JOURNAL_SCHEMA_VERSION + assert entry.event_type == "query.started" + + with pytest.raises(ValueError): + EventJournalEntry.from_dict( + { + "event_id": "evt-2", + "version": 0, + "event_type": "query.started", + "sequence": 2, + "timestamp": 1700000001.0, + "payload": {}, + "metadata": {}, + } + ) + + +def test_event_journal_writer_reader_and_partial_corruption_recovery(tmp_path: Path): + journal = EventJournal(tmp_path / "event_journal.jsonl") + first = journal.append_event(event_type="query.started", payload={"input": "hello"}, session_id="s1") + second = journal.append_event(event_type="tool.completed", payload={"tool": "echo"}, session_id="s1") + assert second.sequence == first.sequence + 1 + + # Simulate corrupted trailing line. + with open(journal.storage_path, "a", encoding="utf-8") as handle: + handle.write("{invalid-json") + handle.write("\n") + + events = journal.query_events(session_id="s1") + assert len(events) == 2 + diagnostics = journal.get_diagnostics() + assert diagnostics["parse_errors"] >= 1 + + +@pytest.mark.asyncio +async def test_query_tool_task_event_chain_records_into_journal(tmp_path: Path): + journal = EventJournal(tmp_path / "event_journal.jsonl") + registry = ToolRegistry(lazy=False) + registry.register(_EchoTool()) + engine = QueryEngine( + api_client=_TwoTurnToolApiClient("echo_tool"), + config=QueryConfig(tools=[_EchoTool()], session_id="session-chain", working_directory=str(tmp_path)), + tool_registry=registry, + ) + engine.event_journal = journal + engine.conversation_id = "conversation-chain" + + tool_results: list[ToolCallResult] = [] + async for item in engine.query("run"): + if isinstance(item, ToolCallResult): + tool_results.append(item) + assert tool_results + + manager = TaskManager(task_queue=InMemoryTaskQueue(), event_journal=journal) + task = await manager.create_bash_task("echo hi") + + async def _ok_executor(_task): + return TaskResult(code=0, stdout="ok") + + await manager.start_task(task.id, executor=_ok_executor) + await manager.wait_for_task(task.id, timeout=2.0) + + events = journal.replay_events(session_id="session-chain") + event_types = [entry.event_type for entry in events] + assert "query.started" in event_types + assert "message.user" in event_types + assert "tool.started" in event_types + assert "tool.completed" in event_types + + task_events = journal.query_events(event_types=["task.started", "task.completed"]) + task_event_types = [entry.event_type for entry in task_events] + assert "task.started" in task_event_types + assert "task.completed" in task_event_types + + +def test_task_event_serialization_contract(): + event = TaskEvent(task_id="task-1", event_type="started", data={"attempt": 1}) + assert event.task_id == "task-1" + assert event.event_type == "started" + assert event.data["attempt"] == 1 + + +def test_sqlite_event_journal_schema_and_query_replay(tmp_path: Path): + journal = SQLiteEventJournal(tmp_path / "runtime_state.db") + first = journal.append_event( + event_type="query.started", + payload={"input": "hello"}, + session_id="s-sqlite", + ) + second = journal.append_event( + event_type="tool.completed", + payload={"tool": "echo"}, + session_id="s-sqlite", + ) + assert second.sequence == first.sequence + 1 + + queried = journal.query_events( + session_id="s-sqlite", + event_types=["tool.completed"], + ) + assert len(queried) == 1 + assert queried[0].event_type == "tool.completed" + + replayed = journal.replay_events(session_id="s-sqlite") + assert [item.sequence for item in replayed] == [first.sequence, second.sequence] + + diagnostics = journal.get_diagnostics() + assert diagnostics["backend"] == "sqlite" + assert diagnostics["events_count"] >= 2 + + +def test_sqlite_event_journal_schema_version_guard(tmp_path: Path): + db_path = tmp_path / "runtime_state.db" + journal = SQLiteEventJournal(db_path) + journal.append_event(event_type="query.started", payload={"v": 1}) + + import sqlite3 + + with sqlite3.connect(db_path, timeout=5.0) as conn: + conn.execute( + "UPDATE journal_metadata SET value = ? WHERE key = ?", + (str(EVENT_JOURNAL_SCHEMA_VERSION + 1), EVENT_JOURNAL_SCHEMA_KEY), + ) + + with pytest.raises(RuntimeError, match="newer than this binary"): + SQLiteEventJournal(db_path) diff --git a/tests/test_features_runtime.py b/tests/test_features_runtime.py new file mode 100644 index 0000000..49b1668 --- /dev/null +++ b/tests/test_features_runtime.py @@ -0,0 +1,91 @@ +"""Runtime tests for feature registry state and precedence semantics.""" + +from __future__ import annotations + +import pytest + +import claude_code.features as features_mod +from claude_code.features import Feature, FeatureRegistry + + +@pytest.fixture(autouse=True) +def _reset_global_registry() -> None: + """Ensure global feature registry state does not leak across tests.""" + features_mod._global_registry = None + yield + features_mod._global_registry = None + + +def _clear_feature_env(monkeypatch: pytest.MonkeyPatch) -> None: + for feature in Feature: + monkeypatch.delenv(f"FEATURE_{feature.value.upper()}", raising=False) + monkeypatch.delenv("FEATURE_EXPERIMENT_X", raising=False) + + +def test_enable_disable_does_not_mutate_frozen_dataclass(monkeypatch): + _clear_feature_env(monkeypatch) + registry = FeatureRegistry() + + registry.enable(Feature.DAEMON) + assert registry.is_enabled(Feature.DAEMON) is True + + registry.disable(Feature.DAEMON) + assert registry.is_enabled(Feature.DAEMON) is False + + +def test_env_fallback_is_dynamic_when_no_runtime_override(monkeypatch): + _clear_feature_env(monkeypatch) + monkeypatch.setenv("FEATURE_DAEMON", "1") + registry = FeatureRegistry() + assert registry.is_enabled(Feature.DAEMON) is True + + monkeypatch.setenv("FEATURE_DAEMON", "0") + assert registry.is_enabled(Feature.DAEMON) is False + + +def test_runtime_override_takes_precedence_over_env(monkeypatch): + _clear_feature_env(monkeypatch) + monkeypatch.setenv("FEATURE_REMOTE", "1") + registry = FeatureRegistry() + assert registry.is_enabled(Feature.REMOTE) is True + + registry.disable(Feature.REMOTE) + assert registry.is_enabled(Feature.REMOTE) is False + + +def test_get_all_configs_reports_runtime_env_default_sources(monkeypatch): + _clear_feature_env(monkeypatch) + registry = FeatureRegistry() + + default_snapshot = registry.get_all_configs()["buddy"] + assert default_snapshot["source"] == "default" + assert default_snapshot["enabled"] is False + + monkeypatch.setenv("FEATURE_BUDDY", "1") + env_snapshot = registry.get_all_configs()["buddy"] + assert env_snapshot["source"] == "env" + assert env_snapshot["enabled"] is True + + registry.disable(Feature.BUDDY) + runtime_snapshot = registry.get_all_configs()["buddy"] + assert runtime_snapshot["source"] == "runtime" + assert runtime_snapshot["enabled"] is False + + +def test_global_registry_state_is_consistent_across_get_calls(monkeypatch): + _clear_feature_env(monkeypatch) + first = features_mod.get_feature_registry() + second = features_mod.get_feature_registry() + + assert first is second + first.enable(Feature.MCP_OAUTH) + assert second.is_enabled(Feature.MCP_OAUTH) is True + + +def test_unknown_feature_helper_uses_boolean_env_parsing(monkeypatch): + _clear_feature_env(monkeypatch) + monkeypatch.setenv("FEATURE_EXPERIMENT_X", "true") + assert features_mod.feature("experiment_x") is True + + monkeypatch.setenv("FEATURE_EXPERIMENT_X", "off") + assert features_mod.feature("experiment_x") is False diff --git a/tests/test_github_ci_workflow_runtime.py b/tests/test_github_ci_workflow_runtime.py new file mode 100644 index 0000000..82beae0 --- /dev/null +++ b/tests/test_github_ci_workflow_runtime.py @@ -0,0 +1,193 @@ +"""Runtime tests for P2-04 GitHub/CI workflow state machine.""" + +from __future__ import annotations + +import subprocess + +import pytest + +from claude_code.services.github_ci_workflow import ( + GitHubCIWorkflowError, + GitHubCIWorkflowService, +) + + +def _cp(command: list[str], *, returncode: int = 0, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess(command, returncode, stdout=stdout, stderr=stderr) + + +def test_workflow_state_machine_success_with_fake_repo_and_pr_chain(): + working_directory = "/tmp/fake-repo" + branch_name = "codex/p2-04-123" + ci_command = "python -m pytest -q" + + command_map: dict[tuple[str, ...], subprocess.CompletedProcess[str]] = { + ("git", "rev-parse", "--is-inside-work-tree"): _cp( + ["git", "rev-parse", "--is-inside-work-tree"], + stdout="true\n", + ), + ("git", "status", "--porcelain"): _cp(["git", "status", "--porcelain"], stdout=""), + ("git", "checkout", "-B", branch_name): _cp(["git", "checkout", "-B", branch_name]), + ("git", "add", "-A"): _cp(["git", "add", "-A"]), + ("git", "diff", "--cached", "--name-only"): _cp( + ["git", "diff", "--cached", "--name-only"], + stdout="claude_code/main.py\n", + ), + ("git", "commit", "-m", "feat: workflow"): _cp(["git", "commit", "-m", "feat: workflow"]), + ("python", "-m", "pytest", "-q"): _cp(["python", "-m", "pytest", "-q"], stdout="ok\n"), + ("git", "diff", "--name-only", "main...HEAD"): _cp( + ["git", "diff", "--name-only", "main...HEAD"], + stdout="claude_code/main.py\n", + ), + ("git", "push", "-u", "origin", branch_name): _cp( + ["git", "push", "-u", "origin", branch_name], + stdout="pushed\n", + ), + ( + "gh", + "pr", + "create", + "--base", + "main", + "--head", + branch_name, + "--title", + "Workflow PR", + "--body", + "PR body", + ): _cp( + [ + "gh", + "pr", + "create", + "--base", + "main", + "--head", + branch_name, + "--title", + "Workflow PR", + "--body", + "PR body", + ], + stdout="https://github.com/example/repo/pull/1\n", + ), + } + + calls: list[tuple[list[str], str]] = [] + + def _runner(command: list[str], cwd: str) -> subprocess.CompletedProcess[str]: + calls.append((list(command), cwd)) + key = tuple(command) + return command_map.get(key, _cp(command)) + + service = GitHubCIWorkflowService(command_runner=_runner) + payload = service.run_workflow( + working_directory=working_directory, + issue_reference="#123", + plan_summary="deliver workflow", + branch_name=branch_name, + base_branch="main", + commit_message="feat: workflow", + pr_title="Workflow PR", + pr_body="PR body", + ci_commands=[ci_command], + headless_ci=True, + push_remote=True, + create_pull_request=True, + ) + + assert payload["status"] == "completed" + assert payload["branch_name"] == branch_name + assert payload["pull_request_url"] == "https://github.com/example/repo/pull/1" + step_status = {item["name"]: item["status"] for item in payload["steps"]} + assert step_status == { + "issue": "completed", + "plan": "completed", + "code": "completed", + "test": "completed", + "review": "completed", + "pr": "completed", + } + assert all(cwd == working_directory for _, cwd in calls) + assert any(command[:3] == ["gh", "pr", "create"] for command, _ in calls) + + +def test_workflow_fails_on_dirty_repo_when_not_allowed(): + def _runner(command: list[str], cwd: str) -> subprocess.CompletedProcess[str]: + if command[:3] == ["git", "status", "--porcelain"]: + return _cp(command, stdout=" M claude_code/main.py\n") + return _cp(command, stdout="true\n") + + service = GitHubCIWorkflowService(command_runner=_runner) + with pytest.raises(GitHubCIWorkflowError) as exc_info: + service.run_workflow( + working_directory="D:/repo", + issue_reference="#dirty", + ci_commands=[], + create_pull_request=False, + push_remote=False, + ) + + exc = exc_info.value + assert exc.code == "repo_dirty" + assert exc.status_code == 409 + assert isinstance(exc.details, dict) + workflow = exc.details["workflow"] + assert workflow["status"] == "failed" + steps = {item["name"]: item["status"] for item in workflow["steps"]} + assert steps["code"] == "failed" + assert steps["test"] == "skipped" + + +def test_workflow_failure_classifies_permission_and_network_errors(): + branch_name = "codex/p2-04-403" + + def _permission_runner(command: list[str], cwd: str) -> subprocess.CompletedProcess[str]: + if command[:3] == ["git", "status", "--porcelain"]: + return _cp(command, stdout="") + if command[:3] == ["gh", "pr", "create"]: + return _cp(command, returncode=1, stderr="HTTP 403 Forbidden") + if command[:3] == ["git", "diff", "--cached"]: + return _cp(command, stdout="claude_code/main.py\n") + return _cp(command, stdout="true\n") + + permission_service = GitHubCIWorkflowService(command_runner=_permission_runner) + with pytest.raises(GitHubCIWorkflowError) as permission_exc: + permission_service.run_workflow( + working_directory="D:/repo", + issue_reference="#403", + branch_name=branch_name, + commit_message="feat: pr", + pr_title="permission", + pr_body="permission", + create_pull_request=True, + push_remote=False, + ) + assert permission_exc.value.code == "permission_denied" + assert permission_exc.value.status_code == 403 + assert permission_exc.value.step == "pr" + + def _network_runner(command: list[str], cwd: str) -> subprocess.CompletedProcess[str]: + if command[:3] == ["git", "status", "--porcelain"]: + return _cp(command, stdout="") + if command[:4] == ["git", "push", "-u", "origin"]: + return _cp(command, returncode=128, stderr="Could not resolve host: github.com") + if command[:3] == ["git", "diff", "--cached"]: + return _cp(command, stdout="claude_code/main.py\n") + return _cp(command, stdout="true\n") + + network_service = GitHubCIWorkflowService(command_runner=_network_runner) + with pytest.raises(GitHubCIWorkflowError) as network_exc: + network_service.run_workflow( + working_directory="D:/repo", + issue_reference="#net", + branch_name="codex/p2-04-net", + commit_message="feat: net", + pr_title="network", + pr_body="network", + push_remote=True, + create_pull_request=True, + ) + assert network_exc.value.code == "network_failure" + assert network_exc.value.status_code == 503 + assert network_exc.value.step == "pr" diff --git a/tests/test_hook_permission_audit_runtime.py b/tests/test_hook_permission_audit_runtime.py new file mode 100644 index 0000000..dba2279 --- /dev/null +++ b/tests/test_hook_permission_audit_runtime.py @@ -0,0 +1,237 @@ +"""Runtime tests for P1-05 hook/permission/audit convergence.""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from claude_code.engine.query import QueryConfig, QueryEngine, ToolCallResult +from claude_code.services.event_journal import EventJournal +from claude_code.services.hooks_manager import HookEvent, HookResult +from claude_code.tools.base import Tool, ToolContext, ToolResult +from claude_code.tools.registry import ToolRegistry + + +class _FakeStreamEvent: + def __init__(self, event_type: str, content: dict | None = None) -> None: + self.type = event_type + self.content = content + self.usage = None + self.error = None + + +class _TwoTurnToolApiClient: + def __init__(self, tool_name: str) -> None: + self._tool_name = tool_name + self._count = 0 + + async def create_message_streaming(self, messages, options): + self._count += 1 + if self._count == 1: + yield _FakeStreamEvent( + "message", + { + "content": [ + { + "type": "tool_use", + "id": "tool-1", + "name": self._tool_name, + "input": {"text": "audit-me"}, + } + ] + }, + ) + else: + yield _FakeStreamEvent("message", {"content": [{"type": "text", "text": "done"}]}) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "done"}]) + + +class _EchoTool(Tool): + @property + def name(self) -> str: + return "echo_tool" + + @property + def description(self) -> str: + return "echo text" + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=input_data.get("text", ""), is_error=False, tool_use_id="") + + +class _WriteProbeTool(Tool): + def __init__(self) -> None: + self.executed = False + + @property + def name(self) -> str: + return "write" + + @property + def description(self) -> str: + return "write probe" + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + self.executed = True + return ToolResult(content="write-executed", is_error=False, tool_use_id="") + + +class _SpyHooksManager: + def __init__(self) -> None: + self.calls: list[tuple[HookEvent, dict]] = [] + + async def trigger(self, event: HookEvent, context: dict) -> list[HookResult]: + self.calls.append((event, context)) + return [ + HookResult( + hook_name=f"{event.value}-hook", + success=True, + output=f"{event.value}-ok", + duration_ms=1.0, + ) + ] + + +@pytest.mark.asyncio +async def test_permission_and_hook_audit_events_on_success_path(tmp_path: Path): + registry = ToolRegistry(lazy=False) + tool = _EchoTool() + registry.register(tool) + + journal = EventJournal(tmp_path / "event_journal.jsonl") + hooks = _SpyHooksManager() + engine = QueryEngine( + api_client=_TwoTurnToolApiClient("echo_tool"), + config=QueryConfig( + tools=[tool], + session_id="session-success", + working_directory=str(tmp_path), + permission_mode="default", + ), + tool_registry=registry, + ) + engine.event_journal = journal + engine.hooks_manager = hooks + + tool_results: list[ToolCallResult] = [] + async for item in engine.query("run success path"): + if isinstance(item, ToolCallResult): + tool_results.append(item) + assert tool_results + assert tool_results[0].result.is_error is False + + permission_events = journal.query_events( + session_id="session-success", + event_types=["permission.requested", "permission.decided"], + limit=20, + ) + assert [event.event_type for event in permission_events] == [ + "permission.requested", + "permission.decided", + ] + decision_payload = permission_events[1].payload + assert decision_payload.get("allowed") is True + assert decision_payload.get("decision_reason") in {"allow_name", "default_allow"} + + hook_events = journal.query_events( + session_id="session-success", + event_types=["hook.execution"], + limit=20, + ) + hook_kinds = [entry.payload.get("hook_event") for entry in hook_events] + assert HookEvent.BEFORE_TOOL.value in hook_kinds + assert HookEvent.AFTER_TOOL.value in hook_kinds + assert HookEvent.ON_ERROR.value not in hook_kinds + first_results = hook_events[0].payload.get("results", []) + assert first_results + assert first_results[0]["hook_name"].endswith("-hook") + assert first_results[0]["success"] is True + + +@pytest.mark.asyncio +async def test_permission_denied_path_emits_decision_and_error_hook_audit(tmp_path: Path): + registry = ToolRegistry(lazy=False) + tool = _WriteProbeTool() + registry.register(tool) + + journal = EventJournal(tmp_path / "event_journal.jsonl") + hooks = _SpyHooksManager() + engine = QueryEngine( + api_client=_TwoTurnToolApiClient("write"), + config=QueryConfig( + tools=[tool], + session_id="session-denied", + working_directory=str(tmp_path), + permission_mode="plan", + ), + tool_registry=registry, + ) + engine.event_journal = journal + engine.hooks_manager = hooks + + tool_results: list[ToolCallResult] = [] + async for item in engine.query("attempt denied write"): + if isinstance(item, ToolCallResult): + tool_results.append(item) + assert tool_results + assert tool_results[0].result.is_error is True + assert "Permission denied" in str(tool_results[0].result.content) + assert tool.executed is False + + decided_events = journal.query_events( + session_id="session-denied", + event_types=["permission.decided"], + limit=20, + ) + assert len(decided_events) == 1 + decided_payload = decided_events[0].payload + assert decided_payload.get("allowed") is False + assert decided_payload.get("decision_reason") == "default_deny" + + hook_events = journal.query_events( + session_id="session-denied", + event_types=["hook.execution"], + limit=20, + ) + hook_kinds = [entry.payload.get("hook_event") for entry in hook_events] + assert HookEvent.BEFORE_TOOL.value in hook_kinds + assert HookEvent.AFTER_TOOL.value in hook_kinds + assert HookEvent.ON_ERROR.value in hook_kinds + + failed_events = journal.query_events( + session_id="session-denied", + event_types=["tool.failed"], + limit=20, + ) + assert failed_events + assert failed_events[0].payload.get("is_error") is True diff --git a/tests/test_hooks_runtime.py b/tests/test_hooks_runtime.py new file mode 100644 index 0000000..21a0bc6 --- /dev/null +++ b/tests/test_hooks_runtime.py @@ -0,0 +1,294 @@ +"""Runtime tests for hooks manager, hook command, and runtime hook integration.""" + +from __future__ import annotations + +import asyncio +import io +from pathlib import Path +from types import SimpleNamespace + +import pytest +from rich.console import Console + +import claude_code.services.hooks_manager as hooks_mod +from claude_code.commands.base import CommandContext, CommandResult, CommandType, SimpleCommand +from claude_code.commands.hooks import HooksCommand +from claude_code.engine.query import QueryConfig, QueryEngine, ToolCallResult, ToolUse +from claude_code.repl import REPL, REPLConfig +from claude_code.services.hooks_manager import HookEvent, HooksManager +from claude_code.tools.base import Tool, ToolContext, ToolResult +from claude_code.tools.registry import ToolRegistry + + +def _context(tmp_path: Path, engine: object | None = None) -> CommandContext: + return CommandContext( + working_directory=str(tmp_path), + console=Console(file=io.StringIO(), force_terminal=False, width=120), + engine=engine, + session=None, + config=None, + ) + + +class _SpyHooksManager: + def __init__(self) -> None: + self.events: list[tuple[HookEvent, dict]] = [] + + async def trigger(self, event: HookEvent, context: dict) -> list: + self.events.append((event, context)) + return [] + + +class _FakeStreamEvent: + def __init__(self, event_type: str, content: dict | None = None) -> None: + self.type = event_type + self.content = content + self.usage = None + self.error = None + + +class _ToolCallingAPIClient: + def __init__(self) -> None: + self.calls = 0 + + async def create_message_streaming(self, messages, options): + self.calls += 1 + if self.calls == 1: + yield _FakeStreamEvent( + "message", + { + "content": [ + { + "type": "tool_use", + "id": "tool-1", + "name": "echo_tool", + "input": {"text": "hook-me"}, + } + ] + }, + ) + else: + yield _FakeStreamEvent( + "message", + {"content": [{"type": "text", "text": "done"}]}, + ) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "done"}]) + + +class _EchoTool(Tool): + @property + def name(self) -> str: + return "echo_tool" + + @property + def description(self) -> str: + return "echo text" + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=input_data.get("text", "")) + + +def test_hooks_manager_load_save_parse_round_trip(tmp_path): + hooks_path = tmp_path / ".claude" / "hooks.json" + manager = HooksManager(config_path=hooks_path) + manager.add_hook("pre-hook", HookEvent.BEFORE_TOOL, "echo pre", timeout_seconds=7) + manager.add_hook( + "post-disabled", + HookEvent.AFTER_TOOL, + "echo post", + enabled=False, + timeout_seconds=9, + ) + + reloaded = HooksManager(config_path=hooks_path) + hooks = reloaded.list_hooks(include_disabled=True) + by_name = {hook.name: hook for hook in hooks} + + assert set(by_name.keys()) == {"pre-hook", "post-disabled"} + assert by_name["pre-hook"].event == HookEvent.BEFORE_TOOL + assert by_name["pre-hook"].timeout_seconds == 7 + assert by_name["post-disabled"].enabled is False + assert reloaded.get_hooks(HookEvent.AFTER_TOOL) == [] + + +@pytest.mark.asyncio +async def test_hooks_command_roundtrip_with_runtime_manager(tmp_path): + manager = HooksManager(config_path=tmp_path / ".claude" / "hooks.json") + command = HooksCommand() + engine = SimpleNamespace(hooks_manager=manager) + context = _context(tmp_path, engine=engine) + + add_result = await command.execute("add before_tool echo pre", context) + assert add_result.success + assert add_result.content == "Added hook: hook-1" + + list_result = await command.execute("list", context) + assert list_result.success + assert "hook-1" in str(list_result.content) + assert "before_tool" in str(list_result.content) + + remove_result = await command.execute("remove hook-1", context) + assert remove_result.success + assert remove_result.content == "Removed hook: hook-1" + + empty_result = await command.execute("list", context) + assert empty_result.success + assert empty_result.content == "No hooks configured" + + +@pytest.mark.asyncio +async def test_query_engine_tool_chain_triggers_before_and_after_hooks(): + registry = ToolRegistry() + echo_tool = _EchoTool() + registry.register(echo_tool) + + engine = QueryEngine( + api_client=_ToolCallingAPIClient(), + config=QueryConfig(tools=[echo_tool]), + tool_registry=registry, + ) + spy_hooks = _SpyHooksManager() + engine.hooks_manager = spy_hooks + + tool_results: list[ToolCallResult] = [] + async for event in engine.query("run hookable tool"): + if isinstance(event, ToolCallResult): + tool_results.append(event) + + assert tool_results + events = [event for event, _ in spy_hooks.events] + assert HookEvent.BEFORE_TOOL in events + assert HookEvent.AFTER_TOOL in events + before_contexts = [ctx for event, ctx in spy_hooks.events if event == HookEvent.BEFORE_TOOL] + assert before_contexts + assert before_contexts[0].get("tool_name") == "echo_tool" + assert before_contexts[0].get("tool_use_id") == "tool-1" + + +@pytest.mark.asyncio +async def test_query_engine_error_path_triggers_on_error_hook(): + engine = QueryEngine(api_client=SimpleNamespace(), config=QueryConfig()) + spy_hooks = _SpyHooksManager() + engine.hooks_manager = spy_hooks + + result = await engine._execute_tool(ToolUse(id="tool-x", name="missing_tool", input={})) + + assert result.result.is_error is True + events = [event for event, _ in spy_hooks.events] + assert HookEvent.BEFORE_TOOL in events + assert HookEvent.AFTER_TOOL in events + assert HookEvent.ON_ERROR in events + + +@pytest.mark.asyncio +async def test_repl_command_hooks_trigger_before_after_and_on_error(tmp_path): + spy_hooks = _SpyHooksManager() + engine = SimpleNamespace( + hooks_manager=spy_hooks, + config=SimpleNamespace(session_id="session-1", model="claude-test-model"), + ) + repl = REPL( + engine=engine, + config=REPLConfig(working_directory=str(tmp_path), welcome_message=False), + ) + + async def _ok_handler(args: str, context: CommandContext) -> CommandResult: + return CommandResult(success=True, content=f"ok:{args}") + + async def _fail_handler(args: str, context: CommandContext) -> CommandResult: + return CommandResult(success=False, error="intentional-failure") + + async def _boom_handler(args: str, context: CommandContext) -> CommandResult: + raise RuntimeError("command exploded") + + repl._commands = { + "ok": SimpleCommand( + name="ok", + description="ok command", + handler=_ok_handler, + command_type=CommandType.LOCAL, + ), + "fail": SimpleCommand( + name="fail", + description="fail command", + handler=_fail_handler, + command_type=CommandType.LOCAL, + ), + "boom": SimpleCommand( + name="boom", + description="boom command", + handler=_boom_handler, + command_type=CommandType.LOCAL, + ), + } + + assert await repl.run_command("/ok payload") is True + assert await repl.run_command("/fail") is True + assert await repl.run_command("/boom") is True + + events = [event for event, _ in spy_hooks.events] + assert events.count(HookEvent.BEFORE_COMMAND) >= 3 + assert events.count(HookEvent.AFTER_COMMAND) >= 2 + assert events.count(HookEvent.ON_ERROR) >= 2 + error_contexts = [ctx for event, ctx in spy_hooks.events if event == HookEvent.ON_ERROR] + assert any(ctx.get("command_name") == "fail" for ctx in error_contexts) + assert any(ctx.get("command_name") == "boom" for ctx in error_contexts) + + +@pytest.mark.asyncio +async def test_hooks_manager_timeout_failure_and_disabled_paths(monkeypatch, tmp_path): + manager = HooksManager(config_path=tmp_path / ".claude" / "hooks.json") + manager.add_hook("timeout-hook", HookEvent.BEFORE_TOOL, "timeout-cmd", timeout_seconds=1) + manager.add_hook("failure-hook", HookEvent.BEFORE_TOOL, "failure-cmd") + manager.add_hook("disabled-hook", HookEvent.BEFORE_TOOL, "disabled-cmd", enabled=False) + + calls: list[str] = [] + wait_for_calls = {"count": 0} + + class _FakeProcess: + def __init__(self, command: str): + self.command = command + self.returncode = 0 if "timeout-cmd" in command else 1 + + async def communicate(self): + if "failure-cmd" in self.command: + return b"", b"hook command failed" + return b"ok", b"" + + async def _fake_create_subprocess_shell(command, **kwargs): + calls.append(command) + return _FakeProcess(command) + + async def _fake_wait_for(awaitable, timeout): + wait_for_calls["count"] += 1 + if wait_for_calls["count"] == 1: + close = getattr(awaitable, "close", None) + if callable(close): + close() + raise asyncio.TimeoutError() + return await awaitable + + monkeypatch.setattr(hooks_mod.asyncio, "create_subprocess_shell", _fake_create_subprocess_shell) + monkeypatch.setattr(hooks_mod.asyncio, "wait_for", _fake_wait_for) + + results = await manager.trigger(HookEvent.BEFORE_TOOL, {"tool_name": "read"}) + + assert len(results) == 2 + assert any("Timeout after 1s" in (item.error or "") for item in results) + assert any("hook command failed" in (item.error or "") for item in results) + assert all("disabled-cmd" not in command for command in calls) diff --git a/tests/test_ide_integration_runtime.py b/tests/test_ide_integration_runtime.py new file mode 100644 index 0000000..5924dbe --- /dev/null +++ b/tests/test_ide_integration_runtime.py @@ -0,0 +1,227 @@ +"""Runtime tests for P2-03 IDE integration path (VS Code first).""" + +from __future__ import annotations + +import time +from contextlib import contextmanager +from pathlib import Path +from types import SimpleNamespace + +from claude_code.server.control_plane import ControlPlaneClient +from claude_code.server.ide_adapter import VSCodeClientAdapter +from claude_code.engine.session import SessionManager +from claude_code.services.event_journal import EventJournal +from claude_code.server.control_plane import ControlPlaneDaemon, DaemonServerConfig +from claude_code.tasks.manager import TaskManager +from claude_code.tasks.queue import InMemoryTaskQueue +from claude_code.tools.base import Tool, ToolContext, ToolResult +from claude_code.tools.registry import ToolRegistry + + +class _EchoTool(Tool): + @property + def name(self) -> str: + return "echo_tool" + + @property + def description(self) -> str: + return "Echo input text." + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=str(input_data.get("text", "")), is_error=False, tool_use_id="echo") + + def validate_input(self, input_data: dict) -> tuple[bool, str | None]: + if "text" not in input_data: + return False, "missing text" + return True, None + + +class _FakeQueryEngine: + def __init__(self, session_manager: SessionManager) -> None: + self._session_manager = session_manager + self.received_queries: list[str] = [] + self._messages: list[object] = [] + self.config = SimpleNamespace( + model="claude-test-model", + permission_mode="default", + always_allow=[], + always_deny=[], + session_id=None, + working_directory=None, + ) + + async def query(self, user_input: str): + self.received_queries.append(user_input) + text = f"echo:{user_input}" + yield {"type": "text", "content": text} + assistant_message = SimpleNamespace( + role="assistant", + content=[{"type": "text", "text": text}], + ) + self._messages.append(assistant_message) + yield assistant_message + yield {"type": "stop_reason", "reason": "end_turn"} + + async def resume_session(self, session_id: str | None = None) -> bool: + if session_id: + session = self._session_manager.load_session(session_id) + if session is None: + return False + self.config.session_id = session.id + return True + sessions = self._session_manager.list_sessions() + if not sessions: + return False + self.config.session_id = sessions[0].id + return True + + def get_messages(self) -> list[object]: + return list(self._messages) + + +def _build_runtime(tmp_path: Path) -> tuple[SimpleNamespace, str]: + session_dir = tmp_path / "sessions" + session_manager = SessionManager(storage_dir=session_dir) + session = session_manager.create_session() + session.add_user_message("bootstrap message") + + tool_registry = ToolRegistry(lazy=False) + tool_registry.register(_EchoTool()) + + query_engine = _FakeQueryEngine(session_manager) + query_engine.config.session_id = session.id + query_engine.config.working_directory = str(tmp_path) + event_journal = EventJournal(tmp_path / ".claude" / "event_journal.jsonl") + query_engine.event_journal = event_journal + + runtime = SimpleNamespace( + working_directory=str(tmp_path), + query_engine=query_engine, + session_manager=session_manager, + task_manager=TaskManager(task_queue=InMemoryTaskQueue()), + tool_registry=tool_registry, + event_journal=event_journal, + ) + return runtime, session.id + + +@contextmanager +def _running_daemon(runtime: SimpleNamespace): + daemon = ControlPlaneDaemon( + runtime, + DaemonServerConfig( + host="127.0.0.1", + port=0, + request_timeout_seconds=3.0, + ), + ) + daemon.start() + client = ControlPlaneClient(daemon.base_url, timeout_seconds=0.2) + last_error: Exception | None = None + for _ in range(40): + try: + client.health() + last_error = None + break + except Exception as exc: # pragma: no cover - startup race mitigation only + last_error = exc + time.sleep(0.05) + if last_error is not None: + daemon.stop() + raise last_error + try: + yield daemon + finally: + daemon.stop() + + +def _prepare_git_repo(tmp_path: Path) -> None: + import subprocess + + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True, text=True) + subprocess.run( + ["git", "config", "user.email", "ide-test@example.com"], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + subprocess.run( + ["git", "config", "user.name", "ide-test"], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + + +def test_vscode_client_protocol_adapter_normalizes_workspace_snapshot(tmp_path: Path): + class _FakeDaemonClient: + def get_ide_workspace(self, **kwargs): + assert kwargs["include_diff"] is True + return { + "workspace": { + "working_directory": str(tmp_path), + "diff": {"content": "diff --git a/a.py b/a.py", "truncated": False, "line_count": 1}, + "changed_files": [{"path": "a.py", "status": " M"}], + "sessions": [{"id": "session-1"}], + "tasks": [{"id": "task-1"}], + "findings": [{"file_path": "a.py", "line": 1, "severity": "low", "issue": "x"}], + } + } + + adapter = VSCodeClientAdapter(_FakeDaemonClient()) # type: ignore[arg-type] + snapshot = adapter.fetch_workspace_snapshot() + + assert snapshot.working_directory == str(tmp_path) + assert snapshot.diff["content"].startswith("diff --git") + assert snapshot.changed_files[0]["path"] == "a.py" + assert snapshot.sessions[0]["id"] == "session-1" + assert snapshot.tasks[0]["id"] == "task-1" + assert snapshot.findings[0]["file_path"] == "a.py" + + +def test_ide_to_daemon_core_chain_returns_diff_task_session_and_findings(tmp_path: Path): + _prepare_git_repo(tmp_path) + baseline = tmp_path / "module.py" + baseline.write_text("def run():\n return 0\n", encoding="utf-8") + + import subprocess + + subprocess.run(["git", "add", "module.py"], cwd=tmp_path, check=True, capture_output=True, text=True) + subprocess.run( + ["git", "commit", "-m", "baseline"], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + + baseline.write_text("def run():\n print('debug')\n return 0\n", encoding="utf-8") + + runtime, _ = _build_runtime(tmp_path) + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + payload = client.get_ide_workspace(diff_max_lines=200, findings_limit=50) + workspace = payload["workspace"] + + assert workspace["working_directory"] == str(tmp_path) + assert isinstance(workspace["sessions"], list) + assert isinstance(workspace["tasks"], list) + assert isinstance(workspace["changed_files"], list) + assert isinstance(workspace["findings"], list) + assert "module.py" in workspace["diff"]["content"] + assert any(item["path"] == "module.py" for item in workspace["changed_files"]) diff --git a/tests/test_linux_unified_gate_runtime.py b/tests/test_linux_unified_gate_runtime.py new file mode 100644 index 0000000..3dbf4c4 --- /dev/null +++ b/tests/test_linux_unified_gate_runtime.py @@ -0,0 +1,34 @@ +"""Contract tests for Linux unified gate command manifest.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_linux_unified_gate_module(): + script_path = Path("scripts") / "run_linux_unified_gate.py" + spec = importlib.util.spec_from_file_location("linux_unified_gate_script", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_linux_unified_gate_manifest_references_existing_test_files(): + gate = _load_linux_unified_gate_module() + assert gate.LINUX_UNIFIED_TEST_FILES + for test_file in gate.LINUX_UNIFIED_TEST_FILES: + assert Path(test_file).is_file() + + +def test_linux_unified_gate_command_list_matches_manifest_order(): + gate = _load_linux_unified_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == len(gate.LINUX_UNIFIED_TEST_FILES) + expected_prefix = "python -m pytest -q -c pytest.ini " + for idx, test_file in enumerate(gate.LINUX_UNIFIED_TEST_FILES): + assert commands[idx] == f"{expected_prefix}{test_file}" + diff --git a/tests/test_main_runtime.py b/tests/test_main_runtime.py index 82dc060..807e0c4 100644 --- a/tests/test_main_runtime.py +++ b/tests/test_main_runtime.py @@ -5,6 +5,7 @@ import pytest import claude_code.main as main_mod +from claude_code.permissions import PermissionMode class _AssistantMessage: @@ -21,7 +22,11 @@ async def query(self, query: str): @pytest.mark.asyncio async def test_run_single_query_prints_stream_and_final_text(monkeypatch, capsys): - monkeypatch.setattr(main_mod, "create_engine", lambda model=None: _FakeEngine()) + monkeypatch.setattr( + main_mod, + "create_engine", + lambda model=None, working_dir=None: _FakeEngine(), + ) code = await main_mod.run_single_query("hello") @@ -58,3 +63,383 @@ def __init__(self, config): assert captured["config"].provider.value == "openai" assert captured["config"].api_key == "dummy" assert captured["config"].base_url == "http://localhost:11434/v1" + + +def test_setup_api_client_accepts_config_provider_without_env(monkeypatch): + monkeypatch.delenv("CLAUDE_API_PROVIDER", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + + captured = {} + + class _FakeApiClient: + def __init__(self, config): + captured["config"] = config + + monkeypatch.setattr(main_mod, "APIClient", _FakeApiClient) + config = main_mod.Config( + api_provider="ollama", + openai_base_url="http://localhost:11434/v1", + ) + + client = main_mod.setup_api_client(config) + + assert isinstance(client, _FakeApiClient) + assert captured["config"].provider.value == "openai" + assert captured["config"].api_key == "dummy" + + +def test_setup_api_client_prefers_env_api_key_over_config_for_anthropic(monkeypatch): + monkeypatch.delenv("CLAUDE_API_PROVIDER", raising=False) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-env-1234") + + captured = {} + + class _FakeApiClient: + def __init__(self, config): + captured["config"] = config + + monkeypatch.setattr(main_mod, "APIClient", _FakeApiClient) + config = main_mod.Config(api_provider="anthropic", api_key="sk-ant-config-0000") + + client = main_mod.setup_api_client(config) + + assert isinstance(client, _FakeApiClient) + assert captured["config"].provider.value == "anthropic" + assert captured["config"].api_key == "sk-ant-env-1234" + + +def test_setup_api_client_uses_openai_config_key_when_env_missing(monkeypatch): + monkeypatch.delenv("CLAUDE_API_PROVIDER", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + + captured = {} + + class _FakeApiClient: + def __init__(self, config): + captured["config"] = config + + monkeypatch.setattr(main_mod, "APIClient", _FakeApiClient) + config = main_mod.Config( + api_provider="openai", + api_key="sk-openai-config-5678", + openai_base_url="https://api.example.invalid/v1", + ) + + client = main_mod.setup_api_client(config) + + assert isinstance(client, _FakeApiClient) + assert captured["config"].provider.value == "openai" + assert captured["config"].api_key == "sk-openai-config-5678" + assert captured["config"].base_url == "https://api.example.invalid/v1" + + +def test_create_engine_uses_model_from_config_when_arg_missing(monkeypatch): + captured = {} + + class _FakeQueryEngine: + def __init__(self, api_client, config, tool_registry): + captured["config"] = config + + cfg = main_mod.Config( + model="claude-haiku-20240307", + permission_mode=PermissionMode.DEFAULT, + ) + + monkeypatch.setattr(main_mod, "get_config", lambda: cfg) + monkeypatch.setattr(main_mod, "setup_api_client", lambda app_config=None: object()) + monkeypatch.setattr(main_mod, "create_default_registry", lambda: object()) + monkeypatch.setattr(main_mod, "QueryEngine", _FakeQueryEngine) + + main_mod.create_engine() + + assert captured["config"].model == "claude-haiku-20240307" + + +def test_create_runtime_attaches_runtime_services(monkeypatch): + class _FakeSession: + def __init__(self): + self.id = "session-123" + self.metadata = type( + "_Metadata", + (), + {"working_directory": "D:/initial", "model": "claude-initial"}, + )() + + class _FakeSessionManager: + @classmethod + def from_store(cls, store): + return cls() + + def ensure_current_session(self): + return _FakeSession() + + class _FakeQueryEngine: + def __init__(self, api_client, config, tool_registry): + self.api_client = api_client + self.config = config + self.tool_registry = tool_registry + + fake_api_client = object() + fake_registry = object() + fake_history = object() + fake_task_manager = object() + fake_hooks = object() + fake_memory = object() + fake_event_journal = object() + fake_session_store = object() + fake_app = object() + + monkeypatch.setattr(main_mod, "get_config", lambda: main_mod.Config(model="claude-test-model")) + monkeypatch.setattr(main_mod, "setup_api_client", lambda app_config=None: fake_api_client) + monkeypatch.setattr(main_mod, "create_default_registry", lambda: fake_registry) + monkeypatch.setattr(main_mod, "QueryEngine", _FakeQueryEngine) + monkeypatch.setattr(main_mod, "SessionManager", _FakeSessionManager) + monkeypatch.setattr(main_mod, "SQLiteSessionStore", lambda db_path=None: fake_session_store) + monkeypatch.setattr(main_mod, "HistoryManager", lambda storage_path=None: fake_history) + monkeypatch.setattr(main_mod, "SQLiteEventJournal", lambda db_path=None: fake_event_journal) + monkeypatch.setattr( + main_mod, + "create_task_manager_with_event_journal", + lambda config, event_journal: fake_task_manager, + ) + monkeypatch.setattr(main_mod, "HooksManager", lambda config_path=None: fake_hooks) + monkeypatch.setattr(main_mod, "get_memory", lambda: fake_memory) + monkeypatch.setattr(main_mod, "_create_application", lambda app_config: fake_app) + + runtime = main_mod.create_runtime(model="claude-custom-model", working_dir="D:/runtime") + + assert runtime.query_engine.config.model == "claude-custom-model" + assert runtime.query_engine.config.working_directory == "D:/runtime" + assert runtime.query_engine.config.session_id == "session-123" + assert runtime.query_engine.session_manager is runtime.session_manager + assert runtime.query_engine.history_manager is runtime.history_manager + assert runtime.query_engine.task_manager is runtime.task_manager + assert runtime.query_engine.hooks_manager is runtime.hooks_manager + assert runtime.query_engine.memory is runtime.memory + assert runtime.query_engine.event_journal is runtime.event_journal + + +def test_create_engine_delegates_to_runtime_context(monkeypatch): + fake_engine = object() + fake_runtime = main_mod.RuntimeContext( + application=object(), + app_config=main_mod.Config(), + api_client=object(), + tool_registry=object(), + query_engine=fake_engine, + session_manager=object(), + history_manager=object(), + task_manager=object(), + hooks_manager=object(), + memory=object(), + event_journal=object(), + working_directory="D:/cwd", + ) + monkeypatch.setattr(main_mod, "create_runtime", lambda model=None, working_dir=None: fake_runtime) + + engine = main_mod.create_engine(model="claude-model", working_dir="D:/cwd") + + assert engine is fake_engine + + +def test_main_mcp_serve_uses_runtime_registry(monkeypatch): + import claude_code.mcp.server as mcp_server_mod + + captured = {} + + async def _fake_run_mcp_server(working_directory: str, server_name: str, tool_registry=None): + captured["working_directory"] = working_directory + captured["server_name"] = server_name + captured["tool_registry"] = tool_registry + + fake_registry = object() + fake_runtime = main_mod.RuntimeContext( + application=object(), + app_config=main_mod.Config(), + api_client=object(), + tool_registry=fake_registry, + query_engine=object(), + session_manager=object(), + history_manager=object(), + task_manager=object(), + hooks_manager=object(), + memory=object(), + event_journal=object(), + working_directory="D:/cwd", + ) + + monkeypatch.setattr(main_mod, "create_runtime", lambda model=None, working_dir=None: fake_runtime) + monkeypatch.setattr(mcp_server_mod, "run_mcp_server", _fake_run_mcp_server) + monkeypatch.setattr( + main_mod, + "setup_default_commands", + lambda: (_ for _ in ()).throw(AssertionError("should not reach command setup in --mcp-serve mode")), + ) + monkeypatch.setattr(main_mod.sys, "argv", ["claude-code", "--mcp-serve", "--mcp-name", "runtime-test"]) + + main_mod.main() + + assert captured["server_name"] == "runtime-test" + assert captured["tool_registry"] is fake_registry + + +def test_main_daemon_serve_uses_runtime_context(monkeypatch): + import claude_code.server.control_plane as control_plane_mod + + captured = {} + fake_runtime = main_mod.RuntimeContext( + application=object(), + app_config=main_mod.Config(), + api_client=object(), + tool_registry=object(), + query_engine=object(), + session_manager=object(), + history_manager=object(), + task_manager=object(), + hooks_manager=object(), + memory=object(), + event_journal=object(), + working_directory="D:/cwd", + ) + + def _fake_run_control_plane_daemon(runtime, *, host, port, request_timeout_seconds): + captured["runtime"] = runtime + captured["host"] = host + captured["port"] = port + captured["request_timeout_seconds"] = request_timeout_seconds + + monkeypatch.setattr(main_mod, "create_runtime", lambda model=None, working_dir=None: fake_runtime) + monkeypatch.setattr(control_plane_mod, "run_control_plane_daemon", _fake_run_control_plane_daemon) + monkeypatch.setattr( + main_mod, + "setup_default_commands", + lambda: (_ for _ in ()).throw(AssertionError("should not reach command setup in --daemon-serve mode")), + ) + monkeypatch.setattr( + main_mod.sys, + "argv", + [ + "claude-code", + "--daemon-serve", + "--daemon-host", + "127.0.0.2", + "--daemon-port", + "9011", + "--daemon-timeout", + "8.5", + ], + ) + + main_mod.main() + + assert captured["runtime"] is fake_runtime + assert captured["host"] == "127.0.0.2" + assert captured["port"] == 9011 + assert captured["request_timeout_seconds"] == 8.5 + + +def test_config_accepts_local_openai_compatible_providers(): + assert main_mod.Config(api_provider="ollama").api_provider == "ollama" + assert main_mod.Config(api_provider="vllm").api_provider == "vllm" + assert main_mod.Config(api_provider="deepseek").api_provider == "deepseek" + + +def test_main_single_query_uses_daemon_thin_client(monkeypatch): + class _FakeClient: + def __init__(self, base_url: str, *, timeout_seconds: float = 10.0) -> None: + self.base_url = base_url + self.timeout_seconds = timeout_seconds + + def query(self, query: str): + return {"output": f"daemon:{query}"} + + import claude_code.server.control_plane as control_plane_mod + + monkeypatch.setattr(control_plane_mod, "ControlPlaneClient", _FakeClient) + monkeypatch.setattr(main_mod.sys, "argv", ["claude-code", "--daemon-client", "hello", "thin"]) + + with pytest.raises(SystemExit) as exc_info: + main_mod.main() + + assert exc_info.value.code == 0 + + +def test_main_single_query_falls_back_to_local_when_daemon_client_fails(monkeypatch): + class _BrokenClient: + def __init__(self, base_url: str, *, timeout_seconds: float = 10.0) -> None: + pass + + def query(self, query: str): + raise RuntimeError("daemon down") + + import claude_code.server.control_plane as control_plane_mod + + fallback = {"called": False} + + async def _fake_run_single_query(query: str, model=None, verbose=False, system=None): + fallback["called"] = True + return 0 + + monkeypatch.setattr(control_plane_mod, "ControlPlaneClient", _BrokenClient) + monkeypatch.setattr(main_mod, "run_single_query", _fake_run_single_query) + monkeypatch.setattr(main_mod.sys, "argv", ["claude-code", "--daemon-client", "fallback-case"]) + + with pytest.raises(SystemExit) as exc_info: + main_mod.main() + + assert exc_info.value.code == 0 + assert fallback["called"] is True + + +def test_main_single_query_daemon_required_fails_without_local_fallback(monkeypatch): + class _BrokenClient: + def __init__(self, base_url: str, *, timeout_seconds: float = 10.0) -> None: + pass + + def query(self, query: str): + raise RuntimeError("daemon down") + + import claude_code.server.control_plane as control_plane_mod + + fallback = {"called": False} + + async def _fake_run_single_query(query: str, model=None, verbose=False, system=None): + fallback["called"] = True + return 0 + + monkeypatch.setattr(control_plane_mod, "ControlPlaneClient", _BrokenClient) + monkeypatch.setattr(main_mod, "run_single_query", _fake_run_single_query) + monkeypatch.setattr( + main_mod.sys, + "argv", + ["claude-code", "--daemon-client", "--daemon-required", "hard-fail"], + ) + + with pytest.raises(SystemExit) as exc_info: + main_mod.main() + + assert exc_info.value.code == 1 + assert fallback["called"] is False + + +def test_main_pipe_mode_uses_daemon_thin_client(monkeypatch): + class _FakeClient: + def __init__(self, base_url: str, *, timeout_seconds: float = 10.0) -> None: + self.base_url = base_url + self.timeout_seconds = timeout_seconds + + def query(self, query: str): + return {"output": "daemon-pipe-output"} + + import claude_code.server.control_plane as control_plane_mod + + monkeypatch.setattr(control_plane_mod, "ControlPlaneClient", _FakeClient) + monkeypatch.setattr(main_mod.sys, "argv", ["claude-code", "--pipe", "--daemon-client"]) + monkeypatch.setattr(main_mod.sys, "stdin", type("_Stdin", (), {"read": staticmethod(lambda: "pipe input")})()) + + with pytest.raises(SystemExit) as exc_info: + main_mod.main() + + assert exc_info.value.code == 0 diff --git a/tests/test_memory_scope_runtime.py b/tests/test_memory_scope_runtime.py new file mode 100644 index 0000000..b43a435 --- /dev/null +++ b/tests/test_memory_scope_runtime.py @@ -0,0 +1,46 @@ +"""Runtime tests for memory scope isolation contracts.""" + +from __future__ import annotations + +import pytest + +from claude_code.services.memory_service import MemoryEntry, SessionMemory + + +@pytest.mark.asyncio +async def test_session_memory_scope_isolation_between_user_project_local(tmp_path): + memory = SessionMemory(storage_dir=tmp_path / "memory") + + await memory.set_scoped("user", "shared", "user-value", working_directory=tmp_path / "a") + await memory.set_scoped("project", "shared", "project-a", working_directory=tmp_path / "a") + await memory.set_scoped("project", "shared", "project-b", working_directory=tmp_path / "b") + await memory.set_scoped("local", "shared", "local-a", working_directory=tmp_path / "a") + + assert await memory.get_scoped("user", "shared", working_directory=tmp_path / "a") == "user-value" + assert await memory.get_scoped("user", "shared", working_directory=tmp_path / "b") == "user-value" + assert await memory.get_scoped("project", "shared", working_directory=tmp_path / "a") == "project-a" + assert await memory.get_scoped("project", "shared", working_directory=tmp_path / "b") == "project-b" + assert await memory.get_scoped("local", "shared", working_directory=tmp_path / "a") == "local-a" + assert await memory.get_scoped("local", "shared", working_directory=tmp_path / "b") is None + + +def test_snapshot_scoped_uses_scope_boundaries(tmp_path): + memory = SessionMemory(storage_dir=tmp_path / "memory") + project_a = tmp_path / "project-a" + project_b = tmp_path / "project-b" + + key_a = SessionMemory._scoped_key("project", "k1", working_directory=project_a) + key_b = SessionMemory._scoped_key("project", "k2", working_directory=project_b) + key_user = SessionMemory._scoped_key("user", "k3", working_directory=project_a) + + memory._memory[key_a] = MemoryEntry(key=key_a, value="A") # type: ignore[attr-defined] + memory._memory[key_b] = MemoryEntry(key=key_b, value="B") # type: ignore[attr-defined] + memory._memory[key_user] = MemoryEntry(key=key_user, value="U") # type: ignore[attr-defined] + + snap_a = memory.snapshot_scoped("project", working_directory=project_a) + snap_b = memory.snapshot_scoped("project", working_directory=project_b) + snap_user = memory.snapshot_scoped("user", working_directory=project_a) + + assert snap_a == {"k1": "A"} + assert snap_b == {"k2": "B"} + assert snap_user == {"k3": "U"} diff --git a/tests/test_multi_agent_supervisor_runtime.py b/tests/test_multi_agent_supervisor_runtime.py new file mode 100644 index 0000000..8f3952c --- /dev/null +++ b/tests/test_multi_agent_supervisor_runtime.py @@ -0,0 +1,214 @@ +"""Runtime tests for P2-01 multi-agent supervisor DAG orchestration.""" + +from __future__ import annotations + +import asyncio +import json +import time + +import pytest + +from claude_code.services.multi_agent_supervisor import ( + AgentNodeSpec, + MultiAgentSupervisor, + NodeExecutionStatus, + SupervisorBudget, + SupervisorConfigError, +) +from claude_code.tools.agent import AgentTool +from claude_code.tools.base import ToolContext, ToolResult + + +@pytest.mark.asyncio +async def test_supervisor_rejects_cyclic_graph(): + async def _executor(node, prompt, deps): + return "ok" + + supervisor = MultiAgentSupervisor(executor=_executor) + nodes = [ + AgentNodeSpec(node_id="a", description="a", prompt="a", dependencies=("b",)), + AgentNodeSpec(node_id="b", description="b", prompt="b", dependencies=("a",)), + ] + + with pytest.raises(SupervisorConfigError, match="contains a cycle"): + await supervisor.run(workflow_id="wf-cycle", nodes=nodes) + + +@pytest.mark.asyncio +async def test_supervisor_budget_and_timeout_state_machine(): + async def _executor(node, prompt, deps): + if node.node_id == "slow": + await asyncio.sleep(0.1) + return f"node:{node.node_id}" + + nodes = [ + AgentNodeSpec(node_id="slow", description="slow", prompt="slow", timeout_seconds=0.01), + AgentNodeSpec(node_id="next", description="next", prompt="next", dependencies=("slow",)), + ] + supervisor = MultiAgentSupervisor( + executor=_executor, + budget=SupervisorBudget(max_parallelism=1, max_failures=0), + ) + result = await supervisor.run(workflow_id="wf-timeout", nodes=nodes) + + by_id = {item.node_id: item for item in result.results} + assert by_id["slow"].status == NodeExecutionStatus.TIMEOUT + assert by_id["next"].status == NodeExecutionStatus.SKIPPED + assert result.success is False + + +@pytest.mark.asyncio +async def test_supervisor_parallel_scheduling_for_independent_nodes(): + timeline: list[tuple[str, str, float]] = [] + + async def _executor(node, prompt, deps): + timeline.append((node.node_id, "start", time.perf_counter())) + await asyncio.sleep(0.05) + timeline.append((node.node_id, "end", time.perf_counter())) + return f"done:{node.node_id}" + + nodes = [ + AgentNodeSpec(node_id="a", description="a", prompt="a"), + AgentNodeSpec(node_id="b", description="b", prompt="b"), + ] + supervisor = MultiAgentSupervisor( + executor=_executor, + budget=SupervisorBudget(max_parallelism=2), + ) + result = await supervisor.run(workflow_id="wf-parallel", nodes=nodes) + assert result.success is True + + starts = {node_id: ts for node_id, phase, ts in timeline if phase == "start"} + assert "a" in starts and "b" in starts + assert abs(starts["a"] - starts["b"]) < 0.04 + + +@pytest.mark.asyncio +async def test_supervisor_retry_recovery_for_failed_node(): + attempts: dict[str, int] = {} + + async def _executor(node, prompt, deps): + attempts[node.node_id] = attempts.get(node.node_id, 0) + 1 + if node.node_id == "fragile" and attempts[node.node_id] == 1: + raise RuntimeError("transient") + return f"ok:{node.node_id}" + + nodes = [ + AgentNodeSpec( + node_id="fragile", + description="fragile", + prompt="fragile", + max_retries=1, + ), + ] + supervisor = MultiAgentSupervisor(executor=_executor) + result = await supervisor.run(workflow_id="wf-retry", nodes=nodes) + item = result.results[0] + assert item.status == NodeExecutionStatus.COMPLETED + assert item.attempts == 2 + assert result.success is True + + +class _FakeAgentTool(AgentTool): + async def _run_sync_agent( + self, + agent_name: str, + description: str, + prompt: str, + agent_def, + model: str, + isolation, + cwd: str, + context: ToolContext, + on_progress, + ) -> ToolResult: + return ToolResult(content=f"{agent_name}|{description}|{prompt}") + + +@pytest.mark.asyncio +async def test_agent_tool_orchestrate_returns_supervisor_summary(): + tool = _FakeAgentTool() + context = ToolContext(working_directory=".", environment={}) + + result = await tool.execute( + { + "description": "workflow smoke", + "prompt": "root task", + "orchestrate": True, + "workflow": { + "id": "wf-agent", + "nodes": [ + {"id": "n1", "description": "first", "prompt": "do first"}, + { + "id": "n2", + "description": "second", + "prompt": "do second", + "depends_on": ["n1"], + "ownership": ["claude_code/tools/agent/__init__.py"], + }, + ], + "budget": {"max_parallelism": 2}, + }, + }, + context, + ) + + assert result.is_error is False + content = str(result.content) + assert "[supervisor:wf-agent] workflow smoke" in content + assert "- n1 [completed]" in content + assert "- n2 [completed]" in content + assert "artifacts:" in content + payload = content.split("artifacts:\n", 1)[1] + artifacts = json.loads(payload) + assert "n1" in artifacts and "n2" in artifacts + assert "__bus__" in artifacts + + +@pytest.mark.asyncio +async def test_supervisor_artifact_flow_and_conflict_merge_failure(): + async def _executor(node, prompt, deps): + if node.node_id == "a": + return { + "artifacts": [ + {"type": "report", "key": "k", "content": "v1"}, + ] + } + if node.node_id == "b": + # Default merge strategy is fail; same key should conflict. + return { + "artifacts": [ + {"type": "report", "key": "k", "content": "v2"}, + ] + } + return {"artifacts": [{"type": "note", "key": "ok", "content": "fallback"}]} + + supervisor = MultiAgentSupervisor(executor=_executor, budget=SupervisorBudget(max_parallelism=1)) + nodes = [ + AgentNodeSpec(node_id="a", description="a", prompt="a"), + AgentNodeSpec(node_id="b", description="b", prompt="b", dependencies=("a",)), + ] + result = await supervisor.run(workflow_id="wf-artifacts", nodes=nodes) + by_id = {item.node_id: item for item in result.results} + assert by_id["a"].status == NodeExecutionStatus.COMPLETED + assert by_id["b"].status == NodeExecutionStatus.FAILED + assert by_id["b"].error is not None and "artifact bus error" in by_id["b"].error + assert "__bus__" in result.artifacts_by_node + + +@pytest.mark.asyncio +async def test_agent_tool_orchestrate_config_error_is_reported(): + tool = _FakeAgentTool() + context = ToolContext(working_directory=".", environment={}) + + result = await tool.execute( + { + "description": "broken workflow", + "prompt": "root task", + "orchestrate": True, + "workflow": {"nodes": [{"id": "x", "description": "x"}]}, + }, + context, + ) + assert result.is_error is True + assert "Workflow config error" in str(result.content) diff --git a/tests/test_org_policy_audit_runtime.py b/tests/test_org_policy_audit_runtime.py new file mode 100644 index 0000000..a83da62 --- /dev/null +++ b/tests/test_org_policy_audit_runtime.py @@ -0,0 +1,200 @@ +"""Runtime tests for P2-05 org policy + approval + audit workflow.""" + +from __future__ import annotations + +import pytest + +from claude_code.services.org_policy_audit import ( + OrgPolicyAuditError, + OrgPolicyAuditService, +) + + +def test_policy_matcher_and_redaction_rules(): + service = OrgPolicyAuditService() + service.configure_policies( + policies=[ + { + "id": "deny-bash-prod", + "effect": "deny", + "tool_pattern": "bash", + "operation_pattern": "prod:*", + "priority": 10, + "reason": "production shell is blocked", + }, + { + "id": "allow-read", + "effect": "allow", + "tool_pattern": "read", + "operation_pattern": "*", + "priority": 20, + }, + ] + ) + + denied = service.evaluate( + tool_name="bash", + operation="prod:deploy", + payload={"token": "sk-very-secret-value"}, + actor="ops-bot", + ) + assert denied["allowed"] is False + assert denied["decision"] == "denied" + assert denied["rule"]["id"] == "deny-bash-prod" + assert denied["payload_redacted"]["token"] == "***REDACTED***" + + allowed = service.evaluate( + tool_name="read", + operation="repo:file", + payload={"path": "README.md"}, + actor="dev-bot", + ) + assert allowed["allowed"] is True + assert allowed["decision"] == "allowed" + assert allowed["rule"]["id"] == "allow-read" + + +def test_approval_state_transitions(): + service = OrgPolicyAuditService() + service.configure_policies( + policies=[ + { + "id": "approve-write", + "effect": "require_approval", + "tool_pattern": "write", + "operation_pattern": "*", + "priority": 10, + } + ] + ) + + first = service.evaluate( + tool_name="write", + operation="repo:edit", + payload={"content": "hello", "api_key": "abc"}, + actor="dev-bot", + ) + assert first["allowed"] is False + assert first["decision"] == "approval_pending" + approval_id = first["approval"]["approval_id"] + + approvals = service.list_approvals(status="pending", limit=10) + assert len(approvals) == 1 + assert approvals[0]["approval_id"] == approval_id + + decided = service.decide_approval( + approval_id=approval_id, + decision="approve", + decided_by="sec-admin", + reason="risk accepted", + ) + assert decided["status"] == "approved" + assert decided["decided_by"] == "sec-admin" + + allowed_after = service.evaluate( + tool_name="write", + operation="repo:edit", + payload={"content": "hello", "api_key": "abc"}, + actor="dev-bot", + ) + assert allowed_after["allowed"] is True + assert allowed_after["decision"] == "allowed_by_approval" + + with pytest.raises(OrgPolicyAuditError) as exc_info: + service.decide_approval( + approval_id=approval_id, + decision="reject", + decided_by="sec-admin", + ) + assert exc_info.value.code == "invalid_approval_transition" + + +def test_audit_chain_and_report_summary(): + service = OrgPolicyAuditService() + service.configure_policies( + policies=[ + { + "id": "approve-agent", + "effect": "require_approval", + "tool_pattern": "agent", + "operation_pattern": "*", + "priority": 5, + }, + { + "id": "deny-rm", + "effect": "deny", + "tool_pattern": "bash", + "operation_pattern": "danger:*", + "priority": 10, + }, + ] + ) + + pending = service.evaluate( + tool_name="agent", + operation="spawn", + payload={"prompt": "run", "password": "p@ss"}, + ) + approval_id = pending["approval"]["approval_id"] + + service.decide_approval( + approval_id=approval_id, + decision="reject", + decided_by="sec-admin", + reason="missing ticket", + ) + denied_by_approval = service.evaluate( + tool_name="agent", + operation="spawn", + payload={"prompt": "run", "password": "p@ss"}, + ) + assert denied_by_approval["decision"] == "denied_by_approval" + + denied_direct = service.evaluate( + tool_name="bash", + operation="danger:rm-rf", + payload={"command": "rm -rf /"}, + ) + assert denied_direct["decision"] == "denied" + + report = service.build_report(limit=200) + assert report["summary"]["rule_count"] == 2 + assert report["summary"]["approval_total"] >= 1 + assert report["summary"]["audit_total"] >= 4 + assert report["decision_counts"].get("denied", 0) >= 1 + assert report["decision_counts"].get("denied_by_approval", 0) >= 1 + + events = service.list_audit_events(event_type="org_policy.evaluated", limit=100) + assert events + assert all(event["event_type"] == "org_policy.evaluated" for event in events) + + +def test_policy_conflict_is_rejected(): + service = OrgPolicyAuditService() + service.configure_policies( + policies=[ + { + "id": "allow-write-a", + "effect": "allow", + "tool_pattern": "write", + "operation_pattern": "*", + "priority": 10, + }, + { + "id": "deny-write-b", + "effect": "deny", + "tool_pattern": "write", + "operation_pattern": "*", + "priority": 10, + }, + ] + ) + + with pytest.raises(OrgPolicyAuditError) as exc_info: + service.evaluate( + tool_name="write", + operation="repo:edit", + payload={"text": "x"}, + ) + assert exc_info.value.code == "policy_conflict" + assert exc_info.value.status_code == 409 diff --git a/tests/test_p1_active_memory_gate_runtime.py b/tests/test_p1_active_memory_gate_runtime.py new file mode 100644 index 0000000..f4ea74a --- /dev/null +++ b/tests/test_p1_active_memory_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P1-04 active memory gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_active_memory_gate_module(): + script_path = Path("scripts") / "run_p1_active_memory_gate.py" + spec = importlib.util.spec_from_file_location("p1_active_memory_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_active_memory_gate_contract(): + gate = _load_p1_active_memory_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 6 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_memory_scope_runtime.py" + assert commands[1] == "python -m pytest -q -c pytest.ini tests/test_active_memory_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p1_cli_thin_client_gate_runtime.py b/tests/test_p1_cli_thin_client_gate_runtime.py new file mode 100644 index 0000000..07e7610 --- /dev/null +++ b/tests/test_p1_cli_thin_client_gate_runtime.py @@ -0,0 +1,31 @@ +"""Contract tests for P1-06 CLI thin-client gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_cli_thin_client_gate_module(): + script_path = Path("scripts") / "run_p1_cli_thin_client_gate.py" + spec = importlib.util.spec_from_file_location("p1_cli_thin_client_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_cli_thin_client_gate_contract(): + gate = _load_p1_cli_thin_client_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == ( + "python -m pytest -q -c pytest.ini tests/test_cli_daemon_thin_client_runtime.py" + ) + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p1_control_plane_gate_runtime.py b/tests/test_p1_control_plane_gate_runtime.py new file mode 100644 index 0000000..d428f88 --- /dev/null +++ b/tests/test_p1_control_plane_gate_runtime.py @@ -0,0 +1,36 @@ +"""Contract tests for P1-01 control-plane gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_control_plane_gate_module(): + script_path = Path("scripts") / "run_p1_control_plane_gate.py" + spec = importlib.util.spec_from_file_location("p1_control_plane_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_control_plane_gate_manifest_references_existing_files(): + gate = _load_p1_control_plane_gate_module() + assert gate.P1_CONTROL_PLANE_TEST_FILES + for test_file in gate.P1_CONTROL_PLANE_TEST_FILES: + assert Path(test_file).is_file() + + +def test_p1_control_plane_gate_builds_pytest_command_contract(): + gate = _load_p1_control_plane_gate_module() + command = gate.build_p1_control_plane_pytest_command( + python_executable="python-custom", + extra_pytest_args=["-k", "daemon", "--maxfail", "1"], + ) + + assert command[:5] == ["python-custom", "-m", "pytest", "-c", "pytest.ini"] + for test_file in gate.P1_CONTROL_PLANE_TEST_FILES: + assert test_file in command + assert command[-4:] == ["-k", "daemon", "--maxfail", "1"] diff --git a/tests/test_p1_event_journal_gate_runtime.py b/tests/test_p1_event_journal_gate_runtime.py new file mode 100644 index 0000000..562e6c6 --- /dev/null +++ b/tests/test_p1_event_journal_gate_runtime.py @@ -0,0 +1,36 @@ +"""Contract tests for P1-02 event-journal gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_event_journal_gate_module(): + script_path = Path("scripts") / "run_p1_event_journal_gate.py" + spec = importlib.util.spec_from_file_location("p1_event_journal_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_event_journal_gate_manifest_references_existing_files(): + gate = _load_p1_event_journal_gate_module() + assert gate.P1_EVENT_JOURNAL_TEST_FILES + for test_file in gate.P1_EVENT_JOURNAL_TEST_FILES: + assert Path(test_file).is_file() + + +def test_p1_event_journal_gate_builds_pytest_command_contract(): + gate = _load_p1_event_journal_gate_module() + command = gate.build_p1_event_journal_pytest_command( + python_executable="python3", + extra_pytest_args=["-k", "journal"], + ) + + assert command[:5] == ["python3", "-m", "pytest", "-c", "pytest.ini"] + for test_file in gate.P1_EVENT_JOURNAL_TEST_FILES: + assert test_file in command + assert command[-2:] == ["-k", "journal"] diff --git a/tests/test_p1_hook_permission_audit_gate_runtime.py b/tests/test_p1_hook_permission_audit_gate_runtime.py new file mode 100644 index 0000000..513a2d4 --- /dev/null +++ b/tests/test_p1_hook_permission_audit_gate_runtime.py @@ -0,0 +1,32 @@ +"""Contract tests for P1-05 hook/permission/audit gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_hook_permission_audit_gate_module(): + script_path = Path("scripts") / "run_p1_hook_permission_audit_gate.py" + spec = importlib.util.spec_from_file_location( + "p1_hook_permission_audit_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_hook_permission_audit_gate_contract(): + gate = _load_p1_hook_permission_audit_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 7 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_permissions_unified.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_server_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p1_sqlite_state_backend_gate_runtime.py b/tests/test_p1_sqlite_state_backend_gate_runtime.py new file mode 100644 index 0000000..d6bc1ca --- /dev/null +++ b/tests/test_p1_sqlite_state_backend_gate_runtime.py @@ -0,0 +1,31 @@ +"""Contract tests for P1-03 SQLite state backend gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p1_sqlite_state_backend_gate_module(): + script_path = Path("scripts") / "run_p1_sqlite_state_backend_gate.py" + spec = importlib.util.spec_from_file_location("p1_sqlite_state_backend_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p1_sqlite_state_backend_gate_contract(): + gate = _load_p1_sqlite_state_backend_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 8 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_tasks_backend_contract.py" + assert commands[-1] == ( + "python -m pytest -q -c pytest.ini tests/test_session_history_memory_runtime.py" + ) + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_agent_runtime_parity_gate_runtime.py b/tests/test_p2_agent_runtime_parity_gate_runtime.py new file mode 100644 index 0000000..ebaba83 --- /dev/null +++ b/tests/test_p2_agent_runtime_parity_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-07 agent runtime parity gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_agent_runtime_parity_gate_module(): + script_path = Path("scripts") / "run_p2_agent_runtime_parity_gate.py" + spec = importlib.util.spec_from_file_location("p2_agent_runtime_parity_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_agent_runtime_parity_gate_contract(): + gate = _load_p2_agent_runtime_parity_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 5 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_agent_background_context_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_artifact_bus_gate_runtime.py b/tests/test_p2_artifact_bus_gate_runtime.py new file mode 100644 index 0000000..0cbcd2d --- /dev/null +++ b/tests/test_p2_artifact_bus_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-02 artifact bus gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_artifact_bus_gate_module(): + script_path = Path("scripts") / "run_p2_artifact_bus_gate.py" + spec = importlib.util.spec_from_file_location("p2_artifact_bus_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_artifact_bus_gate_contract(): + gate = _load_p2_artifact_bus_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_artifact_bus_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_query_engine_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_custom_agents_loader_gate_runtime.py b/tests/test_p2_custom_agents_loader_gate_runtime.py new file mode 100644 index 0000000..93bfe5f --- /dev/null +++ b/tests/test_p2_custom_agents_loader_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-08 custom agents loader gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_custom_agents_loader_gate_module(): + script_path = Path("scripts") / "run_p2_custom_agents_loader_gate.py" + spec = importlib.util.spec_from_file_location("p2_custom_agents_loader_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_custom_agents_loader_gate_contract(): + gate = _load_p2_custom_agents_loader_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 3 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_custom_agents_loader_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_github_ci_workflow_gate_runtime.py b/tests/test_p2_github_ci_workflow_gate_runtime.py new file mode 100644 index 0000000..dd8acde --- /dev/null +++ b/tests/test_p2_github_ci_workflow_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-04 GitHub/CI workflow gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_github_ci_workflow_gate_module(): + script_path = Path("scripts") / "run_p2_github_ci_workflow_gate.py" + spec = importlib.util.spec_from_file_location("p2_github_ci_workflow_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_github_ci_workflow_gate_contract(): + gate = _load_p2_github_ci_workflow_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_github_ci_workflow_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_ide_integration_gate_runtime.py b/tests/test_p2_ide_integration_gate_runtime.py new file mode 100644 index 0000000..203e9b0 --- /dev/null +++ b/tests/test_p2_ide_integration_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-03 IDE integration gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_ide_integration_gate_module(): + script_path = Path("scripts") / "run_p2_ide_integration_gate.py" + spec = importlib.util.spec_from_file_location("p2_ide_integration_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_ide_integration_gate_contract(): + gate = _load_p2_ide_integration_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_ide_integration_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_jetbrains_ide_integration_gate_runtime.py b/tests/test_p2_jetbrains_ide_integration_gate_runtime.py new file mode 100644 index 0000000..c7f12ea --- /dev/null +++ b/tests/test_p2_jetbrains_ide_integration_gate_runtime.py @@ -0,0 +1,31 @@ +"""Contract tests for P2-10 JetBrains IDE integration gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_jetbrains_ide_integration_gate_module(): + script_path = Path("scripts") / "run_p2_jetbrains_ide_integration_gate.py" + spec = importlib.util.spec_from_file_location("p2_jetbrains_ide_integration_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_jetbrains_ide_integration_gate_contract(): + gate = _load_p2_jetbrains_ide_integration_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == ( + "python -m pytest -q -c pytest.ini tests/test_p2_jetbrains_ide_integration_runtime.py" + ) + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_jetbrains_ide_integration_runtime.py b/tests/test_p2_jetbrains_ide_integration_runtime.py new file mode 100644 index 0000000..a17f940 --- /dev/null +++ b/tests/test_p2_jetbrains_ide_integration_runtime.py @@ -0,0 +1,57 @@ +"""Runtime tests for P2-10 JetBrains IDE integration adapter.""" + +from __future__ import annotations + +from pathlib import Path + +from claude_code.server.ide_adapter import JetBrainsClientAdapter + + +def test_jetbrains_client_adapter_normalizes_workspace_snapshot(tmp_path: Path): + class _FakeDaemonClient: + def get_ide_workspace(self, **kwargs): + assert kwargs["include_diff"] is True + return { + "workspace": { + "working_directory": str(tmp_path), + "diff": { + "content": "diff --git a/a.py b/a.py", + "truncated": False, + "line_count": 1, + }, + "changed_files": [{"path": "a.py", "status": " M"}], + "sessions": [{"id": "session-1"}], + "tasks": [{"id": "task-1"}], + "findings": [ + { + "file_path": "a.py", + "line": "12", + "severity": "high", + "issue": "unsafe call", + "recommendation": "guard input", + }, + { + "file_path": "b.py", + "line": 8, + "severity": "low", + "issue": "style drift", + "recommendation": "align naming", + }, + ], + } + } + + adapter = JetBrainsClientAdapter(_FakeDaemonClient()) # type: ignore[arg-type] + snapshot = adapter.fetch_workspace_snapshot() + + assert snapshot.project_root == str(tmp_path) + assert snapshot.vcs["diff"]["content"].startswith("diff --git") + assert snapshot.vcs["changed_files"][0]["path"] == "a.py" + assert snapshot.sessions[0]["id"] == "session-1" + assert snapshot.tasks[0]["id"] == "task-1" + + assert snapshot.inspections[0]["file_path"] == "a.py" + assert snapshot.inspections[0]["line"] == 12 + assert snapshot.inspections[0]["highlight"] == "error" + assert snapshot.inspections[1]["highlight"] == "weak_warning" + diff --git a/tests/test_p2_linux_ci_matrix_gate_runtime.py b/tests/test_p2_linux_ci_matrix_gate_runtime.py new file mode 100644 index 0000000..d5b088b --- /dev/null +++ b/tests/test_p2_linux_ci_matrix_gate_runtime.py @@ -0,0 +1,142 @@ +"""Contract tests for P2-16 Linux CI matrix gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_matrix_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_matrix_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_ci_matrix_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_shard_plan(): + return { + "manifest_total_tests": 5, + "shard_total": 3, + "shards": [ + { + "shard_index": 1, + "shard_total": 3, + "total_tests": 2, + "test_files": ["tests/a.py", "tests/d.py"], + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 1", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "1", + ], + }, + { + "shard_index": 2, + "shard_total": 3, + "total_tests": 2, + "test_files": ["tests/b.py", "tests/e.py"], + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "2", + ], + }, + { + "shard_index": 3, + "shard_total": 3, + "total_tests": 1, + "test_files": ["tests/c.py"], + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-3", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-3/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 3", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "3", + ], + }, + ], + } + + +def test_build_ci_matrix_payload_preserves_shard_order_and_fields(): + gate = _load_ci_matrix_gate_module() + payload = gate.build_ci_matrix_payload( + _sample_shard_plan(), + source_path=Path("reports/shard_plan.json"), + skip_empty_shards=False, + ) + + include = payload["matrix"]["include"] + assert payload["selected_shards"] == 3 + assert [item["shard_index"] for item in include] == [1, 2, 3] + assert include[0]["command_parts"][-2:] == ["--shard-index", "1"] + assert payload["summary_paths"][2].endswith("shard-3/summary.json") + + +def test_build_ci_matrix_payload_skip_empty_shards_contract(): + gate = _load_ci_matrix_gate_module() + shard_plan = _sample_shard_plan() + shard_plan["manifest_total_tests"] = 4 + shard_plan["shards"][2]["total_tests"] = 0 + shard_plan["shards"][2]["test_files"] = [] + + payload = gate.build_ci_matrix_payload( + shard_plan, + source_path=Path("reports/shard_plan.json"), + skip_empty_shards=True, + ) + + include = payload["matrix"]["include"] + assert payload["selected_shards"] == 2 + assert [item["shard_index"] for item in include] == [1, 2] + assert all(item["total_tests"] > 0 for item in include) + + +def test_build_github_output_values_contract(): + gate = _load_ci_matrix_gate_module() + payload = gate.build_ci_matrix_payload( + _sample_shard_plan(), + source_path=Path("reports/shard_plan.json"), + skip_empty_shards=False, + ) + + outputs = gate.build_github_output_values(payload) + assert "matrix" in outputs + assert "summary_paths" in outputs + assert outputs["selected_shards"] == "3" + assert outputs["shard_total"] == "3" + + +def test_load_shard_plan_rejects_total_mismatch(tmp_path: Path): + gate = _load_ci_matrix_gate_module() + bad_plan = _sample_shard_plan() + bad_plan["manifest_total_tests"] = 6 + path = tmp_path / "bad_shard_plan.json" + path.write_text(json.dumps(bad_plan), encoding="utf-8") + + try: + gate.load_shard_plan(path) + raised = False + except ValueError: + raised = True + + assert raised diff --git a/tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py b/tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py new file mode 100644 index 0000000..bf16eb8 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_command_guard_gate_runtime.py @@ -0,0 +1,254 @@ +"""Contract tests for P2-20 Linux CI workflow command guard gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_command_guard_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_command_guard_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_command_guard_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_ci_workflow_plan() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_ci_matrix": "/tmp/ci_matrix.json", + "shard_total": 2, + "manifest_total_tests": 4, + "selected_shards": 2, + "skipped_empty_shards": False, + "fan_out_matrix": { + "include": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 --shard-index 1 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-1", + "--shard-total", + "2", + "--shard-index", + "1", + "--continue-on-failure", + ], + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-2 --shard-total 2 --shard-index 2 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-2", + "--shard-total", + "2", + "--shard-index", + "2", + "--continue-on-failure", + ], + }, + ] + }, + "fan_out_jobs": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 --shard-index 1 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-1", + "--shard-total", + "2", + "--shard-index", + "1", + "--continue-on-failure", + ], + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-2 --shard-total 2 --shard-index 2 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-2", + "--shard-total", + "2", + "--shard-index", + "2", + "--continue-on-failure", + ], + }, + ], + "fan_in": { + "summary_paths": [ + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ], + "merged_summary_path": ".claude/reports/linux_unified_gate/merged_summary.json", + "aggregation_command": "python scripts/run_p2_linux_shard_aggregation_gate.py --output .claude/reports/linux_unified_gate/merged_summary.json --summary .claude/reports/linux_unified_gate/shards/shard-1/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "aggregation_command_parts": [ + "python", + "scripts/run_p2_linux_shard_aggregation_gate.py", + "--output", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ], + "publish_command": "python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md", + "publish_command_parts": [ + "python", + "scripts/run_p2_linux_report_publish_gate.py", + "--merged-summary", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--output-json", + ".claude/reports/linux_unified_gate/final_report.json", + "--output-markdown", + ".claude/reports/linux_unified_gate/final_report.md", + ], + "final_report_json": ".claude/reports/linux_unified_gate/final_report.json", + "final_report_markdown": ".claude/reports/linux_unified_gate/final_report.md", + }, + } + + +def test_guard_ci_workflow_plan_commands_passes_on_valid_payload(): + gate = _load_ci_workflow_command_guard_gate_module() + plan = _sample_ci_workflow_plan() + + normalized_plan, report = gate.guard_ci_workflow_plan_commands(plan) + + assert report["overall_status"] == "passed" + assert report["failed_commands"] == 0 + assert report["issues"] == [] + assert normalized_plan["fan_in"]["aggregation_command"] == plan["fan_in"]["aggregation_command"] + + +def test_guard_detects_command_parts_mismatch_and_returns_normalized_command(): + gate = _load_ci_workflow_command_guard_gate_module() + plan = _sample_ci_workflow_plan() + + plan["fan_out_matrix"]["include"][0]["command"] = ( + "python scripts/run_p2_linux_unified_execution_gate.py --report-dir " + ".claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 " + "--shard-index 1 --continue-on-failure --dry-run" + ) + + normalized_plan, report = gate.guard_ci_workflow_plan_commands(plan) + + assert report["overall_status"] == "failed" + issue_codes = [item["code"] for item in report["issues"]] + assert "command_parts_mismatch" in issue_codes + assert normalized_plan["fan_out_matrix"]["include"][0]["command"] == ( + "python scripts/run_p2_linux_unified_execution_gate.py --report-dir " + ".claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 " + "--shard-index 1 --continue-on-failure" + ) + + +def test_guard_detects_aggregation_summary_paths_mismatch(): + gate = _load_ci_workflow_command_guard_gate_module() + plan = _sample_ci_workflow_plan() + + plan["fan_in"]["aggregation_command_parts"] = [ + "python", + "scripts/run_p2_linux_shard_aggregation_gate.py", + "--output", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ] + plan["fan_in"]["aggregation_command"] = ( + "python scripts/run_p2_linux_shard_aggregation_gate.py --output " + ".claude/reports/linux_unified_gate/merged_summary.json --summary " + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json --summary " + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json" + ) + + _, report = gate.guard_ci_workflow_plan_commands(plan) + + assert report["overall_status"] == "failed" + issue_codes = [item["code"] for item in report["issues"]] + assert "summary_paths_mismatch" in issue_codes + + +def test_guard_detects_publish_report_path_mismatch(): + gate = _load_ci_workflow_command_guard_gate_module() + plan = _sample_ci_workflow_plan() + + plan["fan_in"]["publish_command_parts"] = [ + "python", + "scripts/run_p2_linux_report_publish_gate.py", + "--merged-summary", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--output-json", + ".claude/reports/linux_unified_gate/final_report-alt.json", + "--output-markdown", + ".claude/reports/linux_unified_gate/final_report.md", + ] + plan["fan_in"]["publish_command"] = ( + "python scripts/run_p2_linux_report_publish_gate.py --merged-summary " + ".claude/reports/linux_unified_gate/merged_summary.json --output-json " + ".claude/reports/linux_unified_gate/final_report-alt.json --output-markdown " + ".claude/reports/linux_unified_gate/final_report.md" + ) + + _, report = gate.guard_ci_workflow_plan_commands(plan) + + assert report["overall_status"] == "failed" + issue_codes = [item["code"] for item in report["issues"]] + assert "publish_output_json_mismatch" in issue_codes + + +def test_load_ci_workflow_plan_rejects_missing_command_parts_in_fan_in(tmp_path: Path): + gate = _load_ci_workflow_command_guard_gate_module() + payload = _sample_ci_workflow_plan() + payload["fan_in"].pop("aggregation_command_parts") + path = tmp_path / "ci_workflow_plan_bad_command_parts.json" + path.write_text(json.dumps(payload), encoding="utf-8") + + try: + gate.load_ci_workflow_plan(path) + raised = False + except ValueError: + raised = True + + assert raised diff --git a/tests/test_p2_linux_ci_workflow_decision_gate_runtime.py b/tests/test_p2_linux_ci_workflow_decision_gate_runtime.py new file mode 100644 index 0000000..ef361de --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_decision_gate_runtime.py @@ -0,0 +1,148 @@ +"""Contract tests for P2-23 Linux CI workflow execution decision gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_decision_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_decision_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_decision_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_governance_publish_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_governance_report": "/tmp/ci_workflow_governance.json", + "reported_status": "passed", + "computed_status": "passed", + "overall_status": "passed", + "should_execute_workflow": True, + "failed_checks": [], + "failed_check_count": 0, + "structural_issues": [], + } + + +def test_build_execution_decision_payload_approves_when_all_contracts_aligned(tmp_path: Path): + gate = _load_ci_workflow_decision_gate_module() + payload = _sample_governance_publish_payload() + report_path = tmp_path / "ci_workflow_governance_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + workflow_plan_path = tmp_path / "ci_workflow_plan.json" + workflow_plan_path.write_text("{}", encoding="utf-8") + workflow_yaml_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + workflow_yaml_path.parent.mkdir(parents=True, exist_ok=True) + workflow_yaml_path.write_text("name: Linux Unified Gate\n", encoding="utf-8") + + report = gate.load_governance_publish_report(report_path) + decision_payload = gate.build_execution_decision_payload( + report, + source_path=report_path.resolve(), + workflow_plan_path=workflow_plan_path, + workflow_path=workflow_yaml_path, + on_block_policy="fail", + ) + + assert decision_payload["decision"] == "execute" + assert decision_payload["decision_status"] == "approved" + assert decision_payload["should_execute_workflow"] is True + assert decision_payload["exit_code"] == 0 + assert decision_payload["reason_codes"] == ["governance_passed"] + + +def test_build_execution_decision_payload_blocks_when_failed_checks_present(tmp_path: Path): + gate = _load_ci_workflow_decision_gate_module() + payload = _sample_governance_publish_payload() + payload["overall_status"] = "failed" + payload["should_execute_workflow"] = False + payload["failed_checks"] = ["workflow_sync_drift", "command_guard_failed"] + payload["failed_check_count"] = 2 + report_path = tmp_path / "ci_workflow_governance_publish_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_governance_publish_report(report_path) + decision_payload = gate.build_execution_decision_payload( + report, + source_path=report_path.resolve(), + workflow_plan_path=tmp_path / "ci_workflow_plan.json", + workflow_path=tmp_path / ".github" / "workflows" / "linux_unified_gate.yml", + on_block_policy="fail", + ) + + assert decision_payload["decision"] == "blocked" + assert decision_payload["should_execute_workflow"] is False + assert decision_payload["exit_code"] == 1 + assert "workflow_sync_drift" in decision_payload["reason_codes"] + assert "command_guard_failed" in decision_payload["reason_codes"] + + +def test_build_execution_decision_payload_returns_zero_when_blocked_with_skip_policy(tmp_path: Path): + gate = _load_ci_workflow_decision_gate_module() + payload = _sample_governance_publish_payload() + payload["overall_status"] = "failed" + payload["should_execute_workflow"] = False + payload["failed_checks"] = ["lineage_mismatch"] + payload["failed_check_count"] = 1 + report_path = tmp_path / "ci_workflow_governance_publish_skip.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_governance_publish_report(report_path) + decision_payload = gate.build_execution_decision_payload( + report, + source_path=report_path.resolve(), + workflow_plan_path=tmp_path / "ci_workflow_plan.json", + workflow_path=tmp_path / ".github" / "workflows" / "linux_unified_gate.yml", + on_block_policy="skip", + ) + + assert decision_payload["decision"] == "blocked" + assert decision_payload["should_execute_workflow"] is False + assert decision_payload["exit_code"] == 0 + assert decision_payload["on_block_policy"] == "skip" + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_decision_gate_module() + payload = _sample_governance_publish_payload() + report_path = tmp_path / "ci_workflow_governance_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + workflow_plan_path = tmp_path / "ci_workflow_plan.json" + workflow_plan_path.write_text("{}", encoding="utf-8") + workflow_yaml_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + workflow_yaml_path.parent.mkdir(parents=True, exist_ok=True) + workflow_yaml_path.write_text("name: Linux Unified Gate\n", encoding="utf-8") + + report = gate.load_governance_publish_report(report_path) + decision_payload = gate.build_execution_decision_payload( + report, + source_path=report_path.resolve(), + workflow_plan_path=workflow_plan_path, + workflow_path=workflow_yaml_path, + on_block_policy="fail", + ) + outputs = gate.build_github_output_values( + decision_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_execution_decision.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_execution_decision.md"), + ) + + assert outputs["workflow_execution_decision"] == "execute" + assert outputs["workflow_execution_status"] == "approved" + assert outputs["workflow_execution_should_execute"] == "true" + assert outputs["workflow_execution_exit_code"] == "0" + assert outputs["workflow_execution_report_json"].endswith("ci_workflow_execution_decision.json") + assert outputs["workflow_execution_report_markdown"].endswith("ci_workflow_execution_decision.md") + diff --git a/tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py b/tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py new file mode 100644 index 0000000..168c6bd --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_dispatch_completion_gate_runtime.py @@ -0,0 +1,219 @@ +"""Contract tests for P2-28 Linux CI workflow dispatch completion gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_dispatch_completion_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_dispatch_completion_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_dispatch_completion_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_dispatch_trace_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "project_root": "/tmp", + "execution_status": "dispatched", + "tracking_status": "run_tracking_ready", + "trace_exit_code": 0, + "poll_now": False, + "dry_run": False, + "should_poll_workflow_run": True, + "run_id": 1234567890, + "run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "repo_owner": "acme", + "repo_name": "demo", + "workflow_path": ".github/workflows/linux_unified_gate.yml", + "workflow_ref": "main", + "poll_command": ( + "gh run view 1234567890 --json " + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt --repo acme/demo" + ), + "poll_command_parts": [ + "gh", + "run", + "view", + "1234567890", + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + "--repo", + "acme/demo", + ], + "poll_attempted": False, + "poll_returncode": None, + "poll_status": "", + "poll_conclusion": "", + "poll_url": "https://github.com/acme/demo/actions/runs/1234567890", + "poll_error_type": "", + "poll_error_message": "", + "poll_stdout_tail": "", + "poll_stderr_tail": "", + "dispatch_reason_codes": ["dispatch_command_succeeded"], + "reason_codes": ["dispatch_command_succeeded"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def _success_poll_result() -> dict: + return { + "returncode": 0, + "stdout_tail": '{"status":"completed","conclusion":"success","url":"https://github.com/acme/demo/actions/runs/1234567890"}', + "stderr_tail": "", + "payload": { + "status": "completed", + "conclusion": "success", + "url": "https://github.com/acme/demo/actions/runs/1234567890", + }, + "error_type": "", + "error_message": "", + } + + +def test_build_dispatch_completion_payload_reaches_success(tmp_path: Path): + gate = _load_ci_workflow_dispatch_completion_gate_module() + payload = _sample_dispatch_trace_payload() + report_path = tmp_path / "ci_workflow_dispatch_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_trace_report(report_path) + completion_payload = gate.build_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=3, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=lambda command_parts, cwd, timeout_seconds: _success_poll_result(), + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["completion_status"] == "run_completed_success" + assert completion_payload["completion_exit_code"] == 0 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 1 + assert completion_payload["poll_status"] == "completed" + assert completion_payload["poll_conclusion"] == "success" + assert completion_payload["reason_codes"] == ["run_completed_success"] + + +def test_build_dispatch_completion_payload_timeout_without_allow_in_progress(tmp_path: Path): + gate = _load_ci_workflow_dispatch_completion_gate_module() + payload = _sample_dispatch_trace_payload() + report_path = tmp_path / "ci_workflow_dispatch_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_trace_report(report_path) + + def _in_progress_result(command_parts, cwd, timeout_seconds): + return { + "returncode": 0, + "stdout_tail": '{"status":"in_progress","conclusion":null}', + "stderr_tail": "", + "payload": {"status": "in_progress", "conclusion": None}, + "error_type": "", + "error_message": "", + } + + completion_payload = gate.build_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=2, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=_in_progress_result, + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["completion_status"] == "run_await_timeout" + assert completion_payload["completion_exit_code"] == 1 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 2 + assert "run_await_timeout" in completion_payload["reason_codes"] + + +def test_load_dispatch_trace_report_rejects_command_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_dispatch_completion_gate_module() + payload = _sample_dispatch_trace_payload() + payload["poll_command_parts"] = [ + "gh", + "run", + "view", + "1234567890", + "--json", + "databaseId,status", + ] + report_path = tmp_path / "ci_workflow_dispatch_trace_bad.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_dispatch_trace_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_dispatch_completion_gate_module() + payload = _sample_dispatch_trace_payload() + payload["tracking_status"] = "run_completed_failure" + payload["trace_exit_code"] = 1 + payload["should_poll_workflow_run"] = False + payload["run_id"] = None + payload["poll_command"] = "" + payload["poll_command_parts"] = [] + payload["reason_codes"] = ["run_completed_with_failure"] + report_path = tmp_path / "ci_workflow_dispatch_trace_terminal.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_trace_report(report_path) + completion_payload = gate.build_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=1, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + sleep_fn=lambda seconds: None, + ) + outputs = gate.build_github_output_values( + completion_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md"), + ) + + assert outputs["workflow_dispatch_completion_status"] == "run_completed_failure" + assert outputs["workflow_dispatch_completion_exit_code"] == "1" + assert outputs["workflow_dispatch_completion_should_poll"] == "false" + assert outputs["workflow_dispatch_completion_poll_attempted"] == "false" + assert outputs["workflow_dispatch_completion_run_id"] == "" + assert outputs["workflow_dispatch_completion_report_json"].endswith( + "ci_workflow_dispatch_completion.json" + ) + assert outputs["workflow_dispatch_completion_report_markdown"].endswith( + "ci_workflow_dispatch_completion.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py b/tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py new file mode 100644 index 0000000..7640a08 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_dispatch_execution_gate_runtime.py @@ -0,0 +1,188 @@ +"""Contract tests for P2-25 Linux CI workflow dispatch execution gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_dispatch_execution_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_dispatch_execution_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_dispatch_execution_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_dispatch_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_execution_decision_report": "/tmp/ci_workflow_execution_decision.json", + "source_governance_publish_report": "/tmp/ci_workflow_governance_publish.json", + "workflow_path": ".github/workflows/linux_unified_gate.yml", + "workflow_plan_path": ".claude/reports/linux_unified_gate/ci_workflow_plan.json", + "workflow_ref": "main", + "decision": "execute", + "decision_status": "approved", + "dispatch_status": "ready", + "dispatch_mode": "workflow_dispatch", + "should_dispatch_workflow": True, + "exit_code": 0, + "on_block_policy": "fail", + "reason_codes": ["governance_passed"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + "dispatch_command": "gh workflow run .github/workflows/linux_unified_gate.yml --ref main", + "dispatch_command_parts": [ + "gh", + "workflow", + "run", + ".github/workflows/linux_unified_gate.yml", + "--ref", + "main", + ], + } + + +def test_build_dispatch_execution_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_dispatch_execution_gate_module() + payload = _sample_dispatch_payload() + report_path = tmp_path / "ci_workflow_dispatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_report(report_path) + execution_payload = gate.build_dispatch_execution_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + dry_run=True, + command_result=None, + ) + + assert execution_payload["execution_status"] == "ready_dry_run" + assert execution_payload["execution_exit_code"] == 0 + assert execution_payload["dispatch_attempted"] is False + assert execution_payload["should_dispatch_workflow"] is True + + +def test_build_dispatch_execution_payload_blocked_preserves_policy_exit(tmp_path: Path): + gate = _load_ci_workflow_dispatch_execution_gate_module() + payload = _sample_dispatch_payload() + payload["decision"] = "blocked" + payload["decision_status"] = "blocked" + payload["dispatch_status"] = "blocked" + payload["dispatch_mode"] = "none" + payload["should_dispatch_workflow"] = False + payload["on_block_policy"] = "skip" + payload["exit_code"] = 0 + payload["reason_codes"] = ["workflow_sync_drift"] + payload["failed_checks"] = ["workflow_sync_drift"] + payload["dispatch_command"] = "" + payload["dispatch_command_parts"] = [] + report_path = tmp_path / "ci_workflow_dispatch_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_report(report_path) + execution_payload = gate.build_dispatch_execution_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + dry_run=False, + command_result=None, + ) + + assert execution_payload["execution_status"] == "blocked" + assert execution_payload["execution_exit_code"] == 0 + assert execution_payload["dispatch_attempted"] is False + assert execution_payload["should_dispatch_workflow"] is False + assert execution_payload["reason_codes"] == ["workflow_sync_drift"] + + +def test_load_dispatch_report_rejects_command_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_dispatch_execution_gate_module() + payload = _sample_dispatch_payload() + payload["dispatch_command_parts"] = [ + "gh", + "workflow", + "run", + ".github/workflows/linux_unified_gate.yml", + "--ref", + "dev", + ] + report_path = tmp_path / "ci_workflow_dispatch_bad_contract.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_dispatch_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_dispatch_execution_payload_marks_failed_command(tmp_path: Path): + gate = _load_ci_workflow_dispatch_execution_gate_module() + payload = _sample_dispatch_payload() + report_path = tmp_path / "ci_workflow_dispatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_report(report_path) + execution_payload = gate.build_dispatch_execution_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + dry_run=False, + command_result={ + "attempted": True, + "returncode": 1, + "stdout_tail": "", + "stderr_tail": "dispatch failed", + "error_type": "", + "error_message": "", + }, + ) + + assert execution_payload["execution_status"] == "dispatch_failed" + assert execution_payload["execution_exit_code"] == 1 + assert execution_payload["dispatch_attempted"] is True + assert "dispatch_command_failed" in execution_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_dispatch_execution_gate_module() + payload = _sample_dispatch_payload() + report_path = tmp_path / "ci_workflow_dispatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_report(report_path) + execution_payload = gate.build_dispatch_execution_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + dry_run=True, + command_result=None, + ) + outputs = gate.build_github_output_values( + execution_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md"), + ) + + assert outputs["workflow_dispatch_execution_status"] == "ready_dry_run" + assert outputs["workflow_dispatch_execution_should_dispatch"] == "true" + assert outputs["workflow_dispatch_execution_attempted"] == "false" + assert outputs["workflow_dispatch_execution_exit_code"] == "0" + assert outputs["workflow_dispatch_execution_report_json"].endswith( + "ci_workflow_dispatch_execution.json" + ) + assert outputs["workflow_dispatch_execution_report_markdown"].endswith( + "ci_workflow_dispatch_execution.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py b/tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py new file mode 100644 index 0000000..356019c --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_dispatch_gate_runtime.py @@ -0,0 +1,141 @@ +"""Contract tests for P2-24 Linux CI workflow dispatch gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_dispatch_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_dispatch_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_dispatch_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_execution_decision_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_governance_publish_report": "/tmp/ci_workflow_governance_publish.json", + "workflow_plan_path": "/tmp/ci_workflow_plan.json", + "workflow_path": "/tmp/.github/workflows/linux_unified_gate.yml", + "governance_overall_status": "passed", + "decision": "execute", + "decision_status": "approved", + "should_execute_workflow": True, + "on_block_policy": "fail", + "exit_code": 0, + "failed_checks": [], + "failed_check_count": 0, + "structural_issues": [], + "structural_issue_count": 0, + "missing_artifacts": [], + "reason_codes": ["governance_passed"], + "primary_reason": "governance_passed", + } + + +def test_build_dispatch_payload_ready_when_execute_decision(tmp_path: Path): + gate = _load_ci_workflow_dispatch_gate_module() + payload = _sample_execution_decision_payload() + report_path = tmp_path / "ci_workflow_execution_decision.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_execution_decision(report_path) + dispatch_payload = gate.build_dispatch_payload( + report, + source_path=report_path.resolve(), + workflow_ref="main", + gh_executable="gh", + ) + + assert dispatch_payload["dispatch_status"] == "ready" + assert dispatch_payload["dispatch_mode"] == "workflow_dispatch" + assert dispatch_payload["should_dispatch_workflow"] is True + assert dispatch_payload["dispatch_command_parts"][:3] == ["gh", "workflow", "run"] + assert "--ref" in dispatch_payload["dispatch_command_parts"] + + +def test_build_dispatch_payload_blocked_when_decision_blocked(tmp_path: Path): + gate = _load_ci_workflow_dispatch_gate_module() + payload = _sample_execution_decision_payload() + payload["decision"] = "blocked" + payload["decision_status"] = "blocked" + payload["should_execute_workflow"] = False + payload["on_block_policy"] = "skip" + payload["exit_code"] = 0 + payload["failed_checks"] = ["workflow_sync_drift"] + payload["failed_check_count"] = 1 + payload["reason_codes"] = ["workflow_sync_drift"] + payload["primary_reason"] = "workflow_sync_drift" + report_path = tmp_path / "ci_workflow_execution_decision_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_execution_decision(report_path) + dispatch_payload = gate.build_dispatch_payload( + report, + source_path=report_path.resolve(), + workflow_ref="main", + gh_executable="gh", + ) + + assert dispatch_payload["dispatch_status"] == "blocked" + assert dispatch_payload["dispatch_mode"] == "none" + assert dispatch_payload["should_dispatch_workflow"] is False + assert dispatch_payload["dispatch_command"] == "" + assert dispatch_payload["dispatch_command_parts"] == [] + assert dispatch_payload["exit_code"] == 0 + + +def test_load_execution_decision_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_dispatch_gate_module() + payload = _sample_execution_decision_payload() + payload["decision"] = "execute" + payload["decision_status"] = "approved" + payload["should_execute_workflow"] = False + report_path = tmp_path / "ci_workflow_execution_decision_bad_contract.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_execution_decision(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_dispatch_gate_module() + payload = _sample_execution_decision_payload() + report_path = tmp_path / "ci_workflow_execution_decision.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_execution_decision(report_path) + dispatch_payload = gate.build_dispatch_payload( + report, + source_path=report_path.resolve(), + workflow_ref="main", + gh_executable="gh", + ) + outputs = gate.build_github_output_values( + dispatch_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch.md"), + ) + + assert outputs["workflow_dispatch_status"] == "ready" + assert outputs["workflow_dispatch_mode"] == "workflow_dispatch" + assert outputs["workflow_dispatch_should_dispatch"] == "true" + assert outputs["workflow_dispatch_exit_code"] == "0" + assert outputs["workflow_dispatch_command"].startswith("gh workflow run") + assert outputs["workflow_dispatch_report_json"].endswith("ci_workflow_dispatch.json") + assert outputs["workflow_dispatch_report_markdown"].endswith("ci_workflow_dispatch.md") + diff --git a/tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py b/tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py new file mode 100644 index 0000000..034ada1 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_dispatch_trace_gate_runtime.py @@ -0,0 +1,178 @@ +"""Contract tests for P2-27 Linux CI workflow dispatch trace gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_dispatch_trace_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_dispatch_trace_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_dispatch_trace_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_dispatch_execution_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "project_root": "/tmp", + "decision": "execute", + "decision_status": "approved", + "dispatch_status": "ready", + "dispatch_mode": "workflow_dispatch", + "should_dispatch_workflow": True, + "dispatch_attempted": True, + "dispatch_command": "gh workflow run .github/workflows/linux_unified_gate.yml --ref main", + "dispatch_command_parts": [ + "gh", + "workflow", + "run", + ".github/workflows/linux_unified_gate.yml", + "--ref", + "main", + ], + "dry_run": False, + "execution_status": "dispatched", + "execution_exit_code": 0, + "command_returncode": 0, + "command_stdout_tail": ( + "created workflow run: " + "https://github.com/acme/demo/actions/runs/1234567890" + ), + "command_stderr_tail": "", + "execution_error_type": "", + "execution_error_message": "", + "on_block_policy": "fail", + "reason_codes": ["dispatch_command_succeeded"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + "workflow_path": ".github/workflows/linux_unified_gate.yml", + "workflow_ref": "main", + "workflow_plan_path": ".claude/reports/linux_unified_gate/ci_workflow_plan.json", + } + + +def test_build_dispatch_trace_payload_extracts_run_reference(tmp_path: Path): + gate = _load_ci_workflow_dispatch_trace_gate_module() + payload = _sample_dispatch_execution_payload() + report_path = tmp_path / "ci_workflow_dispatch_execution.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + dispatch_execution_report = gate.load_dispatch_execution_report(report_path) + trace_payload = gate.build_dispatch_trace_payload( + dispatch_execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["tracking_status"] == "run_tracking_ready" + assert trace_payload["should_poll_workflow_run"] is True + assert trace_payload["run_id"] == 1234567890 + assert trace_payload["repo_owner"] == "acme" + assert trace_payload["repo_name"] == "demo" + assert trace_payload["trace_exit_code"] == 0 + assert trace_payload["poll_attempted"] is False + assert trace_payload["poll_command_parts"][:3] == ["gh", "run", "view"] + + +def test_build_dispatch_trace_payload_handles_missing_run_reference(tmp_path: Path): + gate = _load_ci_workflow_dispatch_trace_gate_module() + payload = _sample_dispatch_execution_payload() + payload["command_stdout_tail"] = "dispatch succeeded without URL" + report_path = tmp_path / "ci_workflow_dispatch_execution_no_run_url.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + dispatch_execution_report = gate.load_dispatch_execution_report(report_path) + trace_payload = gate.build_dispatch_trace_payload( + dispatch_execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["tracking_status"] == "run_tracking_missing" + assert trace_payload["should_poll_workflow_run"] is False + assert trace_payload["trace_exit_code"] == 1 + assert "dispatch_run_reference_missing" in trace_payload["reason_codes"] + + +def test_build_dispatch_trace_payload_blocked_passthrough(tmp_path: Path): + gate = _load_ci_workflow_dispatch_trace_gate_module() + payload = _sample_dispatch_execution_payload() + payload["execution_status"] = "blocked" + payload["execution_exit_code"] = 0 + payload["should_dispatch_workflow"] = False + payload["dispatch_attempted"] = False + payload["command_returncode"] = None + payload["command_stdout_tail"] = "" + payload["reason_codes"] = ["workflow_sync_drift"] + payload["failed_checks"] = ["workflow_sync_drift"] + report_path = tmp_path / "ci_workflow_dispatch_execution_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + dispatch_execution_report = gate.load_dispatch_execution_report(report_path) + trace_payload = gate.build_dispatch_trace_payload( + dispatch_execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=True, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["tracking_status"] == "blocked" + assert trace_payload["should_poll_workflow_run"] is False + assert trace_payload["run_id"] is None + assert trace_payload["trace_exit_code"] == 0 + assert trace_payload["poll_attempted"] is False + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_dispatch_trace_gate_module() + payload = _sample_dispatch_execution_payload() + report_path = tmp_path / "ci_workflow_dispatch_execution.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + dispatch_execution_report = gate.load_dispatch_execution_report(report_path) + trace_payload = gate.build_dispatch_trace_payload( + dispatch_execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + outputs = gate.build_github_output_values( + trace_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md"), + ) + + assert outputs["workflow_dispatch_trace_status"] == "run_tracking_ready" + assert outputs["workflow_dispatch_trace_should_poll"] == "true" + assert outputs["workflow_dispatch_trace_run_id"] == "1234567890" + assert outputs["workflow_dispatch_trace_run_url"].endswith("/actions/runs/1234567890") + assert outputs["workflow_dispatch_trace_exit_code"] == "0" + assert outputs["workflow_dispatch_trace_report_json"].endswith("ci_workflow_dispatch_trace.json") + assert outputs["workflow_dispatch_trace_report_markdown"].endswith( + "ci_workflow_dispatch_trace.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_governance_gate_runtime.py b/tests/test_p2_linux_ci_workflow_governance_gate_runtime.py new file mode 100644 index 0000000..b3ad944 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_governance_gate_runtime.py @@ -0,0 +1,290 @@ +"""Contract tests for P2-21 Linux CI workflow governance gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_governance_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_governance_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_ci_workflow_governance_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_ci_workflow_plan() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_ci_matrix": "/tmp/ci_matrix.json", + "shard_total": 2, + "manifest_total_tests": 4, + "selected_shards": 2, + "skipped_empty_shards": False, + "fan_out_matrix": { + "include": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 --shard-index 1 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-1", + "--shard-total", + "2", + "--shard-index", + "1", + "--continue-on-failure", + ], + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-2 --shard-total 2 --shard-index 2 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-2", + "--shard-total", + "2", + "--shard-index", + "2", + "--continue-on-failure", + ], + }, + ] + }, + "fan_out_jobs": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 --shard-index 1 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-1", + "--shard-total", + "2", + "--shard-index", + "1", + "--continue-on-failure", + ], + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --report-dir .claude/reports/linux_unified_gate/shards/shard-2 --shard-total 2 --shard-index 2 --continue-on-failure", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--report-dir", + ".claude/reports/linux_unified_gate/shards/shard-2", + "--shard-total", + "2", + "--shard-index", + "2", + "--continue-on-failure", + ], + }, + ], + "fan_in": { + "summary_paths": [ + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ], + "merged_summary_path": ".claude/reports/linux_unified_gate/merged_summary.json", + "aggregation_command": "python scripts/run_p2_linux_shard_aggregation_gate.py --output .claude/reports/linux_unified_gate/merged_summary.json --summary .claude/reports/linux_unified_gate/shards/shard-1/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "aggregation_command_parts": [ + "python", + "scripts/run_p2_linux_shard_aggregation_gate.py", + "--output", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "--summary", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ], + "publish_command": "python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md", + "publish_command_parts": [ + "python", + "scripts/run_p2_linux_report_publish_gate.py", + "--merged-summary", + ".claude/reports/linux_unified_gate/merged_summary.json", + "--output-json", + ".claude/reports/linux_unified_gate/final_report.json", + "--output-markdown", + ".claude/reports/linux_unified_gate/final_report.md", + ], + "final_report_json": ".claude/reports/linux_unified_gate/final_report.json", + "final_report_markdown": ".claude/reports/linux_unified_gate/final_report.md", + }, + } + + +def _render_artifacts( + gate, + *, + plan_path: Path, + workflow_path: Path, + metadata_path: Path, + workflow_name: str = "Linux Unified Gate", + python_version: str = "3.11", + artifact_prefix: str = "linux-unified-summary", +) -> None: + yaml_gate = gate._load_module( + script_name="run_p2_linux_ci_workflow_yaml_gate.py", + module_name="p2_linux_ci_workflow_yaml_gate_for_test", + ) + plan = yaml_gate.load_ci_workflow_plan(plan_path) + workflow_yaml = yaml_gate.build_ci_workflow_yaml( + plan, + workflow_name=workflow_name, + python_version=python_version, + artifact_prefix=artifact_prefix, + ) + metadata = yaml_gate.build_render_metadata( + plan, + source_plan_path=plan_path.resolve(), + workflow_output_path=workflow_path, + artifact_prefix=artifact_prefix, + ) + metadata["workflow_name"] = workflow_name + + workflow_path.parent.mkdir(parents=True, exist_ok=True) + workflow_path.write_text(workflow_yaml, encoding="utf-8") + metadata_path.parent.mkdir(parents=True, exist_ok=True) + metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") + + +def test_build_governance_report_passes_on_aligned_payload(tmp_path: Path): + gate = _load_ci_workflow_governance_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + plan_path.write_text(json.dumps(_sample_ci_workflow_plan(), indent=2), encoding="utf-8") + _render_artifacts(gate, plan_path=plan_path, workflow_path=workflow_path, metadata_path=metadata_path) + + report = gate.build_governance_report( + ci_workflow_plan_path=plan_path, + workflow_path=workflow_path, + metadata_path=metadata_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + strict_generated_at=False, + ) + + assert report["overall_status"] == "passed" + assert report["failed_checks"] == [] + assert report["command_guard"]["overall_status"] == "passed" + assert report["lineage"]["source_match"] is True + assert report["lineage"]["workflow_output_match"] is True + + +def test_build_governance_report_fails_on_workflow_drift(tmp_path: Path): + gate = _load_ci_workflow_governance_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + plan_path.write_text(json.dumps(_sample_ci_workflow_plan(), indent=2), encoding="utf-8") + _render_artifacts(gate, plan_path=plan_path, workflow_path=workflow_path, metadata_path=metadata_path) + workflow_path.write_text(workflow_path.read_text(encoding="utf-8").replace("workflow_dispatch", "workflow_call"), encoding="utf-8") + + report = gate.build_governance_report( + ci_workflow_plan_path=plan_path, + workflow_path=workflow_path, + metadata_path=metadata_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + strict_generated_at=False, + ) + + assert report["overall_status"] == "failed" + assert "workflow_sync_drift" in report["failed_checks"] + assert report["workflow_sync"]["workflow_drift"] is True + + +def test_build_governance_report_fails_on_command_guard_issues(tmp_path: Path): + gate = _load_ci_workflow_governance_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + payload = _sample_ci_workflow_plan() + payload["fan_out_matrix"]["include"][0]["command"] = ( + "python scripts/run_p2_linux_unified_execution_gate.py --report-dir " + ".claude/reports/linux_unified_gate/shards/shard-1 --shard-total 2 " + "--shard-index 1 --continue-on-failure --dry-run" + ) + plan_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + _render_artifacts(gate, plan_path=plan_path, workflow_path=workflow_path, metadata_path=metadata_path) + + report = gate.build_governance_report( + ci_workflow_plan_path=plan_path, + workflow_path=workflow_path, + metadata_path=metadata_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + strict_generated_at=False, + ) + + assert report["overall_status"] == "failed" + assert "command_guard_failed" in report["failed_checks"] + assert report["command_guard"]["issue_count"] > 0 + + +def test_build_governance_report_fails_on_metadata_lineage_mismatch(tmp_path: Path): + gate = _load_ci_workflow_governance_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + plan_path.write_text(json.dumps(_sample_ci_workflow_plan(), indent=2), encoding="utf-8") + _render_artifacts(gate, plan_path=plan_path, workflow_path=workflow_path, metadata_path=metadata_path) + + metadata_payload = json.loads(metadata_path.read_text(encoding="utf-8")) + metadata_payload["source_ci_workflow_plan"] = "/tmp/another-plan.json" + metadata_path.write_text(json.dumps(metadata_payload, indent=2), encoding="utf-8") + + report = gate.build_governance_report( + ci_workflow_plan_path=plan_path, + workflow_path=workflow_path, + metadata_path=metadata_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + strict_generated_at=False, + ) + + assert report["overall_status"] == "failed" + assert "lineage_mismatch" in report["failed_checks"] + issue_codes = [item["code"] for item in report["lineage"]["issues"]] + assert "source_ci_workflow_plan_mismatch" in issue_codes diff --git a/tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py new file mode 100644 index 0000000..6511ca2 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_governance_publish_gate_runtime.py @@ -0,0 +1,132 @@ +"""Contract tests for P2-22 Linux CI workflow governance publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_governance_publish_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_governance_publish_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_governance_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_governance_report() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_ci_workflow_plan": "/tmp/ci_workflow_plan.json", + "workflow_path": "/tmp/.github/workflows/linux_unified_gate.yml", + "metadata_path": "/tmp/.claude/reports/linux_unified_gate/ci_workflow_render.json", + "strict_generated_at": False, + "overall_status": "passed", + "failed_checks": [], + "workflow_sync": { + "workflow_missing": False, + "metadata_missing": False, + "workflow_drift": False, + "metadata_drift": False, + "workflow_diff": "", + "metadata_diff": "", + }, + "command_guard": { + "overall_status": "passed", + "failed_commands": 0, + "total_commands": 4, + "issue_count": 0, + "normalization_required": False, + "issues": [], + }, + "lineage": { + "expected_source_ci_workflow_plan": "/tmp/ci_workflow_plan.json", + "actual_source_ci_workflow_plan": "/tmp/ci_workflow_plan.json", + "source_match": True, + "expected_workflow_output_path": "/tmp/.github/workflows/linux_unified_gate.yml", + "actual_workflow_output_path": "/tmp/.github/workflows/linux_unified_gate.yml", + "workflow_output_match": True, + "issues": [], + }, + } + + +def test_build_governance_publish_payload_passes_on_aligned_report(tmp_path: Path): + gate = _load_ci_workflow_governance_publish_gate_module() + report_path = tmp_path / "ci_workflow_governance.json" + report_path.write_text(json.dumps(_sample_governance_report(), indent=2), encoding="utf-8") + + report = gate.load_governance_report(report_path) + payload = gate.build_governance_publish_payload(report, source_path=report_path.resolve()) + + assert payload["overall_status"] == "passed" + assert payload["should_execute_workflow"] is True + assert payload["failed_checks"] == [] + assert payload["structural_issues"] == [] + + +def test_build_governance_publish_payload_fails_on_reported_failed_checks(tmp_path: Path): + gate = _load_ci_workflow_governance_publish_gate_module() + payload = _sample_governance_report() + payload["overall_status"] = "failed" + payload["failed_checks"] = ["workflow_sync_drift", "command_guard_failed"] + payload["workflow_sync"]["workflow_drift"] = True + payload["command_guard"]["overall_status"] = "failed" + payload["command_guard"]["failed_commands"] = 1 + payload["command_guard"]["issue_count"] = 2 + + report_path = tmp_path / "ci_workflow_governance_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_governance_report(report_path) + publish_payload = gate.build_governance_publish_payload(report, source_path=report_path.resolve()) + + assert publish_payload["overall_status"] == "failed" + assert publish_payload["should_execute_workflow"] is False + assert "workflow_sync_drift" in publish_payload["failed_checks"] + assert "command_guard_failed" in publish_payload["failed_checks"] + + +def test_build_governance_publish_payload_detects_failed_check_mismatch(tmp_path: Path): + gate = _load_ci_workflow_governance_publish_gate_module() + payload = _sample_governance_report() + payload["overall_status"] = "passed" + payload["failed_checks"] = [] + payload["workflow_sync"]["workflow_drift"] = True + + report_path = tmp_path / "ci_workflow_governance_bad_checks.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_governance_report(report_path) + publish_payload = gate.build_governance_publish_payload(report, source_path=report_path.resolve()) + + assert publish_payload["overall_status"] == "failed" + assert "failed_checks_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_governance_publish_gate_module() + payload = _sample_governance_report() + report_path = tmp_path / "ci_workflow_governance.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_governance_report(report_path) + publish_payload = gate.build_governance_publish_payload(report, source_path=report_path.resolve()) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_governance_publish.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_governance_publish.md"), + ) + + assert outputs["governance_status"] == "passed" + assert outputs["governance_should_execute_workflow"] == "true" + assert outputs["governance_failed_checks"] == "[]" + assert outputs["governance_report_json"].endswith("ci_workflow_governance_publish.json") + assert outputs["governance_report_markdown"].endswith("ci_workflow_governance_publish.md") + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py new file mode 100644 index 0000000..06759f0 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py @@ -0,0 +1,165 @@ +"""Contract tests for P2-101 Linux CI workflow pipeline closure to P2-100 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_100_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_100_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_module() + + payload = gate.build_pipeline_closure_to_p2_100_payload( + project_root=Path("."), + pipeline_closure_p2_99_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py" + ), + pipeline_closure_p2_99_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-101" + assert payload["pipeline_closure_target_card_id"] == "P2-100" + assert payload["pipeline_closure_to_p2_100_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_100_decision"] + == "pipeline_closure_to_p2_100_verified" + ) + assert payload["pipeline_closure_to_p2_100_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_100_should_proceed"] is True + assert payload["pipeline_closure_to_p2_100_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_100_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_100_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_100_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_module() + + p2_99_gate_script = tmp_path / "pipeline_closure_p2_99_gate.py" + p2_99_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_99_runtime_test = tmp_path / "test_pipeline_closure_p2_99_gate_runtime.py" + p2_99_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_100_payload( + project_root=tmp_path, + pipeline_closure_p2_99_gate_script=p2_99_gate_script, + pipeline_closure_p2_99_runtime_test=p2_99_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_100_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_100_decision"] + == "hold_pipeline_closure_p2_100_manual_action" + ) + assert payload["pipeline_closure_to_p2_100_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_100_should_proceed"] is False + assert payload["pipeline_closure_to_p2_100_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_100_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_100_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_99_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_100_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_module() + + p2_99_gate_script = tmp_path / "pipeline_closure_p2_99_gate.py" + p2_99_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_100_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_99_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_100_payload( + project_root=tmp_path, + pipeline_closure_p2_99_gate_script=p2_99_gate_script, + pipeline_closure_p2_99_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_100_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_100_decision"] + == "hold_pipeline_closure_p2_100_manual_action" + ) + assert payload["pipeline_closure_to_p2_100_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_module() + payload = { + "pipeline_closure_to_p2_100_status": "failed", + "pipeline_closure_to_p2_100_decision": "hold_pipeline_closure_p2_100_manual_action", + "pipeline_closure_to_p2_100_exit_code": 1, + "pipeline_closure_to_p2_100_should_proceed": False, + "pipeline_closure_to_p2_100_requires_manual_action": True, + "pipeline_closure_to_p2_100_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_100_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_100_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_100.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_100.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_100_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_100_decision"] + == "hold_pipeline_closure_p2_100_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_100_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_100_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_100_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_100_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_100_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_100_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_100_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_100.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_100_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_100.md" + ) + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py new file mode 100644 index 0000000..62174b4 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py @@ -0,0 +1,167 @@ +"""Contract tests for P2-102 Linux CI workflow pipeline closure to P2-101 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_101_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_101_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_module() + + payload = gate.build_pipeline_closure_to_p2_101_payload( + project_root=Path("."), + pipeline_closure_p2_100_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_100_gate.py" + ), + pipeline_closure_p2_100_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-102" + assert payload["pipeline_closure_target_card_id"] == "P2-101" + assert payload["pipeline_closure_to_p2_101_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_101_decision"] + == "pipeline_closure_to_p2_101_verified" + ) + assert payload["pipeline_closure_to_p2_101_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_101_should_proceed"] is True + assert payload["pipeline_closure_to_p2_101_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_101_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_101_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_101_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_module() + + p2_100_gate_script = tmp_path / "pipeline_closure_p2_100_gate.py" + p2_100_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_100_runtime_test = tmp_path / "test_pipeline_closure_p2_100_gate_runtime.py" + p2_100_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_101_payload( + project_root=tmp_path, + pipeline_closure_p2_100_gate_script=p2_100_gate_script, + pipeline_closure_p2_100_runtime_test=p2_100_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_101_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_101_decision"] + == "hold_pipeline_closure_p2_101_manual_action" + ) + assert payload["pipeline_closure_to_p2_101_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_101_should_proceed"] is False + assert payload["pipeline_closure_to_p2_101_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_101_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_101_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_100_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_101_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_module() + + p2_100_gate_script = tmp_path / "pipeline_closure_p2_100_gate.py" + p2_100_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_101_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_100_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_101_payload( + project_root=tmp_path, + pipeline_closure_p2_100_gate_script=p2_100_gate_script, + pipeline_closure_p2_100_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_101_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_101_decision"] + == "hold_pipeline_closure_p2_101_manual_action" + ) + assert payload["pipeline_closure_to_p2_101_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_module() + payload = { + "pipeline_closure_to_p2_101_status": "failed", + "pipeline_closure_to_p2_101_decision": "hold_pipeline_closure_p2_101_manual_action", + "pipeline_closure_to_p2_101_exit_code": 1, + "pipeline_closure_to_p2_101_should_proceed": False, + "pipeline_closure_to_p2_101_requires_manual_action": True, + "pipeline_closure_to_p2_101_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_101_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_101_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_100_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_101.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_101.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_101_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_101_decision"] + == "hold_pipeline_closure_p2_101_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_101_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_101_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_101_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_101_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_101_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_101_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_101_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_101.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_101_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_101.md" + ) + + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py new file mode 100644 index 0000000..fa67f6f --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-103 Linux CI workflow pipeline closure to P2-102 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_102_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_102_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_module() + + payload = gate.build_pipeline_closure_to_p2_102_payload( + project_root=Path("."), + pipeline_closure_p2_101_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_101_gate.py" + ), + pipeline_closure_p2_101_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-103" + assert payload["pipeline_closure_target_card_id"] == "P2-102" + assert payload["pipeline_closure_to_p2_102_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_102_decision"] + == "pipeline_closure_to_p2_102_verified" + ) + assert payload["pipeline_closure_to_p2_102_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_102_should_proceed"] is True + assert payload["pipeline_closure_to_p2_102_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_102_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_102_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_102_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_module() + + p2_101_gate_script = tmp_path / "pipeline_closure_p2_101_gate.py" + p2_101_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_101_runtime_test = tmp_path / "test_pipeline_closure_p2_101_gate_runtime.py" + p2_101_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_102_payload( + project_root=tmp_path, + pipeline_closure_p2_101_gate_script=p2_101_gate_script, + pipeline_closure_p2_101_runtime_test=p2_101_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_102_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_102_decision"] + == "hold_pipeline_closure_p2_102_manual_action" + ) + assert payload["pipeline_closure_to_p2_102_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_102_should_proceed"] is False + assert payload["pipeline_closure_to_p2_102_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_102_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_102_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_101_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_102_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_module() + + p2_101_gate_script = tmp_path / "pipeline_closure_p2_101_gate.py" + p2_101_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_102_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_101_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_102_payload( + project_root=tmp_path, + pipeline_closure_p2_101_gate_script=p2_101_gate_script, + pipeline_closure_p2_101_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_102_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_102_decision"] + == "hold_pipeline_closure_p2_102_manual_action" + ) + assert payload["pipeline_closure_to_p2_102_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_module() + payload = { + "pipeline_closure_to_p2_102_status": "failed", + "pipeline_closure_to_p2_102_decision": "hold_pipeline_closure_p2_102_manual_action", + "pipeline_closure_to_p2_102_exit_code": 1, + "pipeline_closure_to_p2_102_should_proceed": False, + "pipeline_closure_to_p2_102_requires_manual_action": True, + "pipeline_closure_to_p2_102_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_102_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_102_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_101_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_102.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_102.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_102_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_102_decision"] + == "hold_pipeline_closure_p2_102_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_102_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_102_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_102_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_102_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_102_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_102_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_102_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_102.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_102_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_102.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py new file mode 100644 index 0000000..3f83cb9 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py @@ -0,0 +1,163 @@ +"""Contract tests for P2-103 Linux CI workflow pipeline closure to P2-102 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_103_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_103_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_module() + + payload = gate.build_pipeline_closure_to_p2_103_payload( + project_root=Path("."), + pipeline_closure_p2_102_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_102_gate.py" + ), + pipeline_closure_p2_102_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-104" + assert payload["pipeline_closure_target_card_id"] == "P2-103" + assert payload["pipeline_closure_to_p2_103_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_103_decision"] + == "pipeline_closure_to_p2_103_verified" + ) + assert payload["pipeline_closure_to_p2_103_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_103_should_proceed"] is True + assert payload["pipeline_closure_to_p2_103_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_103_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_103_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_103_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_module() + + p2_102_gate_script = tmp_path / "pipeline_closure_p2_102_gate.py" + p2_102_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_102_runtime_test = tmp_path / "test_pipeline_closure_p2_102_gate_runtime.py" + p2_102_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_103_payload( + project_root=tmp_path, + pipeline_closure_p2_102_gate_script=p2_102_gate_script, + pipeline_closure_p2_102_runtime_test=p2_102_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_103_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_103_decision"] + == "hold_pipeline_closure_p2_103_manual_action" + ) + assert payload["pipeline_closure_to_p2_103_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_103_should_proceed"] is False + assert payload["pipeline_closure_to_p2_103_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_103_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_103_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_102_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_103_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_module() + + p2_102_gate_script = tmp_path / "pipeline_closure_p2_102_gate.py" + p2_102_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_103_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_102_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_103_payload( + project_root=tmp_path, + pipeline_closure_p2_102_gate_script=p2_102_gate_script, + pipeline_closure_p2_102_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_103_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_103_decision"] + == "hold_pipeline_closure_p2_103_manual_action" + ) + assert payload["pipeline_closure_to_p2_103_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_103(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_module() + payload = { + "pipeline_closure_to_p2_103_status": "failed", + "pipeline_closure_to_p2_103_decision": "hold_pipeline_closure_p2_103_manual_action", + "pipeline_closure_to_p2_103_exit_code": 1, + "pipeline_closure_to_p2_103_should_proceed": False, + "pipeline_closure_to_p2_103_requires_manual_action": True, + "pipeline_closure_to_p2_103_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_103_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_103_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_102_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_103.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_103.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_103_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_103_decision"] + == "hold_pipeline_closure_p2_103_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_103_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_103_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_103_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_103_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_103_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_103_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_103_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_103.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_103_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_103.md" + ) + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py new file mode 100644 index 0000000..90f324b --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py @@ -0,0 +1,163 @@ +"""Contract tests for P2-105 Linux CI workflow pipeline closure to P2-104 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_104_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_104_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_module() + + payload = gate.build_pipeline_closure_to_p2_104_payload( + project_root=Path("."), + pipeline_closure_p2_103_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_103_gate.py" + ), + pipeline_closure_p2_103_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-105" + assert payload["pipeline_closure_target_card_id"] == "P2-104" + assert payload["pipeline_closure_to_p2_104_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_104_decision"] + == "pipeline_closure_to_p2_104_verified" + ) + assert payload["pipeline_closure_to_p2_104_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_104_should_proceed"] is True + assert payload["pipeline_closure_to_p2_104_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_104_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_104_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_104_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_module() + + p2_103_gate_script = tmp_path / "pipeline_closure_p2_103_gate.py" + p2_103_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_103_runtime_test = tmp_path / "test_pipeline_closure_p2_103_gate_runtime.py" + p2_103_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_104_payload( + project_root=tmp_path, + pipeline_closure_p2_103_gate_script=p2_103_gate_script, + pipeline_closure_p2_103_runtime_test=p2_103_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_104_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_104_decision"] + == "hold_pipeline_closure_p2_104_manual_action" + ) + assert payload["pipeline_closure_to_p2_104_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_104_should_proceed"] is False + assert payload["pipeline_closure_to_p2_104_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_104_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_104_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_103_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_104_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_module() + + p2_103_gate_script = tmp_path / "pipeline_closure_p2_103_gate.py" + p2_103_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_104_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_103_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_104_payload( + project_root=tmp_path, + pipeline_closure_p2_103_gate_script=p2_103_gate_script, + pipeline_closure_p2_103_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_104_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_104_decision"] + == "hold_pipeline_closure_p2_104_manual_action" + ) + assert payload["pipeline_closure_to_p2_104_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_104(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_module() + payload = { + "pipeline_closure_to_p2_104_status": "failed", + "pipeline_closure_to_p2_104_decision": "hold_pipeline_closure_p2_104_manual_action", + "pipeline_closure_to_p2_104_exit_code": 1, + "pipeline_closure_to_p2_104_should_proceed": False, + "pipeline_closure_to_p2_104_requires_manual_action": True, + "pipeline_closure_to_p2_104_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_104_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_104_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_103_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_104.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_104.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_104_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_104_decision"] + == "hold_pipeline_closure_p2_104_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_104_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_104_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_104_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_104_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_104_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_104_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_104_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_104.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_104_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_104.md" + ) + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py new file mode 100644 index 0000000..635b6bc --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py @@ -0,0 +1,164 @@ +"""Contract tests for P2-106 Linux CI workflow pipeline closure to P2-105 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_105_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_105_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_module() + + payload = gate.build_pipeline_closure_to_p2_105_payload( + project_root=Path("."), + pipeline_closure_p2_104_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_104_gate.py" + ), + pipeline_closure_p2_104_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-106" + assert payload["pipeline_closure_target_card_id"] == "P2-105" + assert payload["pipeline_closure_to_p2_105_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_105_decision"] + == "pipeline_closure_to_p2_105_verified" + ) + assert payload["pipeline_closure_to_p2_105_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_105_should_proceed"] is True + assert payload["pipeline_closure_to_p2_105_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_105_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_105_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_105_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_module() + + p2_104_gate_script = tmp_path / "pipeline_closure_p2_104_gate.py" + p2_104_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_104_runtime_test = tmp_path / "test_pipeline_closure_p2_104_gate_runtime.py" + p2_104_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_105_payload( + project_root=tmp_path, + pipeline_closure_p2_104_gate_script=p2_104_gate_script, + pipeline_closure_p2_104_runtime_test=p2_104_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_105_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_105_decision"] + == "hold_pipeline_closure_p2_105_manual_action" + ) + assert payload["pipeline_closure_to_p2_105_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_105_should_proceed"] is False + assert payload["pipeline_closure_to_p2_105_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_105_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_105_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_104_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_105_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_module() + + p2_104_gate_script = tmp_path / "pipeline_closure_p2_104_gate.py" + p2_104_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_105_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_104_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_105_payload( + project_root=tmp_path, + pipeline_closure_p2_104_gate_script=p2_104_gate_script, + pipeline_closure_p2_104_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_105_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_105_decision"] + == "hold_pipeline_closure_p2_105_manual_action" + ) + assert payload["pipeline_closure_to_p2_105_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_105(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_module() + payload = { + "pipeline_closure_to_p2_105_status": "failed", + "pipeline_closure_to_p2_105_decision": "hold_pipeline_closure_p2_105_manual_action", + "pipeline_closure_to_p2_105_exit_code": 1, + "pipeline_closure_to_p2_105_should_proceed": False, + "pipeline_closure_to_p2_105_requires_manual_action": True, + "pipeline_closure_to_p2_105_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_105_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_105_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_104_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_105.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_105.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_105_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_105_decision"] + == "hold_pipeline_closure_p2_105_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_105_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_105_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_105_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_105_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_105_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_105_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_105_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_105.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_105_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_105.md" + ) + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py new file mode 100644 index 0000000..d5b0d6c --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-107 Linux CI workflow pipeline closure to P2-106 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_106_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_106_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_module() + + payload = gate.build_pipeline_closure_to_p2_106_payload( + project_root=Path("."), + pipeline_closure_p2_105_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_105_gate.py" + ), + pipeline_closure_p2_105_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-107" + assert payload["pipeline_closure_target_card_id"] == "P2-106" + assert payload["pipeline_closure_to_p2_106_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_106_decision"] + == "pipeline_closure_to_p2_106_verified" + ) + assert payload["pipeline_closure_to_p2_106_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_106_should_proceed"] is True + assert payload["pipeline_closure_to_p2_106_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_106_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_106_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_106_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_module() + + p2_105_gate_script = tmp_path / "pipeline_closure_p2_105_gate.py" + p2_105_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_105_runtime_test = tmp_path / "test_pipeline_closure_p2_105_gate_runtime.py" + p2_105_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_106_payload( + project_root=tmp_path, + pipeline_closure_p2_105_gate_script=p2_105_gate_script, + pipeline_closure_p2_105_runtime_test=p2_105_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_106_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_106_decision"] + == "hold_pipeline_closure_p2_106_manual_action" + ) + assert payload["pipeline_closure_to_p2_106_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_106_should_proceed"] is False + assert payload["pipeline_closure_to_p2_106_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_106_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_106_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_105_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_106_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_module() + + p2_105_gate_script = tmp_path / "pipeline_closure_p2_105_gate.py" + p2_105_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_106_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_105_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_106_payload( + project_root=tmp_path, + pipeline_closure_p2_105_gate_script=p2_105_gate_script, + pipeline_closure_p2_105_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_106_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_106_decision"] + == "hold_pipeline_closure_p2_106_manual_action" + ) + assert payload["pipeline_closure_to_p2_106_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_106(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_module() + payload = { + "pipeline_closure_to_p2_106_status": "failed", + "pipeline_closure_to_p2_106_decision": "hold_pipeline_closure_p2_106_manual_action", + "pipeline_closure_to_p2_106_exit_code": 1, + "pipeline_closure_to_p2_106_should_proceed": False, + "pipeline_closure_to_p2_106_requires_manual_action": True, + "pipeline_closure_to_p2_106_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_106_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_106_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_105_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_106.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_106.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_106_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_106_decision"] + == "hold_pipeline_closure_p2_106_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_106_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_106_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_106_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_106_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_106_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_106_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_106_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_106.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_106_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_106.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py new file mode 100644 index 0000000..907d94f --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-108 Linux CI workflow pipeline closure to P2-107 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_107_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_107_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_module() + + payload = gate.build_pipeline_closure_to_p2_107_payload( + project_root=Path("."), + pipeline_closure_p2_106_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_106_gate.py" + ), + pipeline_closure_p2_106_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-108" + assert payload["pipeline_closure_target_card_id"] == "P2-107" + assert payload["pipeline_closure_to_p2_107_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_107_decision"] + == "pipeline_closure_to_p2_107_verified" + ) + assert payload["pipeline_closure_to_p2_107_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_107_should_proceed"] is True + assert payload["pipeline_closure_to_p2_107_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_107_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_107_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_107_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_module() + + p2_106_gate_script = tmp_path / "pipeline_closure_p2_106_gate.py" + p2_106_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_106_runtime_test = tmp_path / "test_pipeline_closure_p2_106_gate_runtime.py" + p2_106_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_107_payload( + project_root=tmp_path, + pipeline_closure_p2_106_gate_script=p2_106_gate_script, + pipeline_closure_p2_106_runtime_test=p2_106_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_107_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_107_decision"] + == "hold_pipeline_closure_p2_107_manual_action" + ) + assert payload["pipeline_closure_to_p2_107_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_107_should_proceed"] is False + assert payload["pipeline_closure_to_p2_107_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_107_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_107_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_106_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_107_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_module() + + p2_106_gate_script = tmp_path / "pipeline_closure_p2_106_gate.py" + p2_106_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_107_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_106_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_107_payload( + project_root=tmp_path, + pipeline_closure_p2_106_gate_script=p2_106_gate_script, + pipeline_closure_p2_106_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_107_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_107_decision"] + == "hold_pipeline_closure_p2_107_manual_action" + ) + assert payload["pipeline_closure_to_p2_107_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_107(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_module() + payload = { + "pipeline_closure_to_p2_107_status": "failed", + "pipeline_closure_to_p2_107_decision": "hold_pipeline_closure_p2_107_manual_action", + "pipeline_closure_to_p2_107_exit_code": 1, + "pipeline_closure_to_p2_107_should_proceed": False, + "pipeline_closure_to_p2_107_requires_manual_action": True, + "pipeline_closure_to_p2_107_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_107_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_107_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_106_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_107.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_107.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_107_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_107_decision"] + == "hold_pipeline_closure_p2_107_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_107_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_107_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_107_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_107_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_107_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_107_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_107_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_107.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_107_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_107.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py new file mode 100644 index 0000000..bc795b8 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-109 Linux CI workflow pipeline closure to P2-108 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_108_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_108_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_module() + + payload = gate.build_pipeline_closure_to_p2_108_payload( + project_root=Path("."), + pipeline_closure_p2_107_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_107_gate.py" + ), + pipeline_closure_p2_107_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-109" + assert payload["pipeline_closure_target_card_id"] == "P2-108" + assert payload["pipeline_closure_to_p2_108_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_108_decision"] + == "pipeline_closure_to_p2_108_verified" + ) + assert payload["pipeline_closure_to_p2_108_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_108_should_proceed"] is True + assert payload["pipeline_closure_to_p2_108_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_108_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_108_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_108_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_module() + + p2_107_gate_script = tmp_path / "pipeline_closure_p2_107_gate.py" + p2_107_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_107_runtime_test = tmp_path / "test_pipeline_closure_p2_107_gate_runtime.py" + p2_107_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_108_payload( + project_root=tmp_path, + pipeline_closure_p2_107_gate_script=p2_107_gate_script, + pipeline_closure_p2_107_runtime_test=p2_107_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_108_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_108_decision"] + == "hold_pipeline_closure_p2_108_manual_action" + ) + assert payload["pipeline_closure_to_p2_108_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_108_should_proceed"] is False + assert payload["pipeline_closure_to_p2_108_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_108_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_108_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_107_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_108_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_module() + + p2_107_gate_script = tmp_path / "pipeline_closure_p2_107_gate.py" + p2_107_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_108_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_107_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_108_payload( + project_root=tmp_path, + pipeline_closure_p2_107_gate_script=p2_107_gate_script, + pipeline_closure_p2_107_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_108_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_108_decision"] + == "hold_pipeline_closure_p2_108_manual_action" + ) + assert payload["pipeline_closure_to_p2_108_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_108(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_module() + payload = { + "pipeline_closure_to_p2_108_status": "failed", + "pipeline_closure_to_p2_108_decision": "hold_pipeline_closure_p2_108_manual_action", + "pipeline_closure_to_p2_108_exit_code": 1, + "pipeline_closure_to_p2_108_should_proceed": False, + "pipeline_closure_to_p2_108_requires_manual_action": True, + "pipeline_closure_to_p2_108_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_108_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_108_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_107_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_108.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_108.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_108_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_108_decision"] + == "hold_pipeline_closure_p2_108_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_108_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_108_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_108_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_108_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_108_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_108_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_108_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_108.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_108_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_108.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py new file mode 100644 index 0000000..45a845c --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py @@ -0,0 +1,162 @@ +"""Contract tests for P2-110 Linux CI workflow pipeline closure to P2-109 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_109_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_109_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_module() + + payload = gate.build_pipeline_closure_to_p2_109_payload( + project_root=Path("."), + pipeline_closure_p2_108_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_108_gate.py" + ), + pipeline_closure_p2_108_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-110" + assert payload["pipeline_closure_target_card_id"] == "P2-109" + assert payload["pipeline_closure_to_p2_109_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_109_decision"] + == "pipeline_closure_to_p2_109_verified" + ) + assert payload["pipeline_closure_to_p2_109_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_109_should_proceed"] is True + assert payload["pipeline_closure_to_p2_109_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_109_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_109_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_109_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_module() + + p2_108_gate_script = tmp_path / "pipeline_closure_p2_108_gate.py" + p2_108_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_108_runtime_test = tmp_path / "test_pipeline_closure_p2_108_gate_runtime.py" + p2_108_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_109_payload( + project_root=tmp_path, + pipeline_closure_p2_108_gate_script=p2_108_gate_script, + pipeline_closure_p2_108_runtime_test=p2_108_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_109_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_109_decision"] + == "hold_pipeline_closure_p2_109_manual_action" + ) + assert payload["pipeline_closure_to_p2_109_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_109_should_proceed"] is False + assert payload["pipeline_closure_to_p2_109_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_109_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_109_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_108_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_109_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_module() + + p2_108_gate_script = tmp_path / "pipeline_closure_p2_108_gate.py" + p2_108_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_109_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_108_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_109_payload( + project_root=tmp_path, + pipeline_closure_p2_108_gate_script=p2_108_gate_script, + pipeline_closure_p2_108_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_109_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_109_decision"] + == "hold_pipeline_closure_p2_109_manual_action" + ) + assert payload["pipeline_closure_to_p2_109_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_109(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_module() + payload = { + "pipeline_closure_to_p2_109_status": "failed", + "pipeline_closure_to_p2_109_decision": "hold_pipeline_closure_p2_109_manual_action", + "pipeline_closure_to_p2_109_exit_code": 1, + "pipeline_closure_to_p2_109_should_proceed": False, + "pipeline_closure_to_p2_109_requires_manual_action": True, + "pipeline_closure_to_p2_109_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_109_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_109_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_108_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_109.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_109.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_109_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_109_decision"] + == "hold_pipeline_closure_p2_109_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_109_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_109_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_109_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_109_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_109_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_109_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_109_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_109.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_109_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_109.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py new file mode 100644 index 0000000..4a4f379 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py @@ -0,0 +1,163 @@ +"""Contract tests for P2-111 Linux CI workflow pipeline closure to P2-110 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_110_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_110_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_module() + + payload = gate.build_pipeline_closure_to_p2_110_payload( + project_root=Path("."), + pipeline_closure_p2_109_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_109_gate.py" + ), + pipeline_closure_p2_109_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-111" + assert payload["pipeline_closure_target_card_id"] == "P2-110" + assert payload["pipeline_closure_to_p2_110_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_110_decision"] + == "pipeline_closure_to_p2_110_verified" + ) + assert payload["pipeline_closure_to_p2_110_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_110_should_proceed"] is True + assert payload["pipeline_closure_to_p2_110_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_110_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_110_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_110_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_module() + + p2_109_gate_script = tmp_path / "pipeline_closure_p2_109_gate.py" + p2_109_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_109_runtime_test = tmp_path / "test_pipeline_closure_p2_109_gate_runtime.py" + p2_109_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_110_payload( + project_root=tmp_path, + pipeline_closure_p2_109_gate_script=p2_109_gate_script, + pipeline_closure_p2_109_runtime_test=p2_109_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_110_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_110_decision"] + == "hold_pipeline_closure_p2_110_manual_action" + ) + assert payload["pipeline_closure_to_p2_110_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_110_should_proceed"] is False + assert payload["pipeline_closure_to_p2_110_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_110_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_110_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_109_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_110_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_module() + + p2_109_gate_script = tmp_path / "pipeline_closure_p2_109_gate.py" + p2_109_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_110_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_109_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_110_payload( + project_root=tmp_path, + pipeline_closure_p2_109_gate_script=p2_109_gate_script, + pipeline_closure_p2_109_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_110_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_110_decision"] + == "hold_pipeline_closure_p2_110_manual_action" + ) + assert payload["pipeline_closure_to_p2_110_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_110(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_module() + payload = { + "pipeline_closure_to_p2_110_status": "failed", + "pipeline_closure_to_p2_110_decision": "hold_pipeline_closure_p2_110_manual_action", + "pipeline_closure_to_p2_110_exit_code": 1, + "pipeline_closure_to_p2_110_should_proceed": False, + "pipeline_closure_to_p2_110_requires_manual_action": True, + "pipeline_closure_to_p2_110_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_110_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_110_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_109_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_110.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_110.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_110_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_110_decision"] + == "hold_pipeline_closure_p2_110_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_110_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_110_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_110_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_110_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_110_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_110_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_110_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_110.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_110_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_110.md" + ) + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py new file mode 100644 index 0000000..9b1f850 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py @@ -0,0 +1,163 @@ +"""Contract tests for P2-112 Linux CI workflow pipeline closure to P2-111 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_111_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_111_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_module() + + payload = gate.build_pipeline_closure_to_p2_111_payload( + project_root=Path("."), + pipeline_closure_p2_110_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_110_gate.py" + ), + pipeline_closure_p2_110_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-112" + assert payload["pipeline_closure_target_card_id"] == "P2-111" + assert payload["pipeline_closure_to_p2_111_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_111_decision"] + == "pipeline_closure_to_p2_111_verified" + ) + assert payload["pipeline_closure_to_p2_111_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_111_should_proceed"] is True + assert payload["pipeline_closure_to_p2_111_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_111_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_111_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_111_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_module() + + p2_110_gate_script = tmp_path / "pipeline_closure_p2_110_gate.py" + p2_110_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_110_runtime_test = tmp_path / "test_pipeline_closure_p2_110_gate_runtime.py" + p2_110_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_111_payload( + project_root=tmp_path, + pipeline_closure_p2_110_gate_script=p2_110_gate_script, + pipeline_closure_p2_110_runtime_test=p2_110_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_111_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_111_decision"] + == "hold_pipeline_closure_p2_111_manual_action" + ) + assert payload["pipeline_closure_to_p2_111_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_111_should_proceed"] is False + assert payload["pipeline_closure_to_p2_111_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_111_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_111_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_110_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_111_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_module() + + p2_110_gate_script = tmp_path / "pipeline_closure_p2_110_gate.py" + p2_110_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_111_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_110_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_111_payload( + project_root=tmp_path, + pipeline_closure_p2_110_gate_script=p2_110_gate_script, + pipeline_closure_p2_110_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_111_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_111_decision"] + == "hold_pipeline_closure_p2_111_manual_action" + ) + assert payload["pipeline_closure_to_p2_111_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_111(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_module() + payload = { + "pipeline_closure_to_p2_111_status": "failed", + "pipeline_closure_to_p2_111_decision": "hold_pipeline_closure_p2_111_manual_action", + "pipeline_closure_to_p2_111_exit_code": 1, + "pipeline_closure_to_p2_111_should_proceed": False, + "pipeline_closure_to_p2_111_requires_manual_action": True, + "pipeline_closure_to_p2_111_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_111_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_111_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_110_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_111.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_111.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_111_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_111_decision"] + == "hold_pipeline_closure_p2_111_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_111_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_111_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_111_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_111_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_111_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_111_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_111_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_111.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_111_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_111.md" + ) + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py new file mode 100644 index 0000000..1430f86 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py @@ -0,0 +1,164 @@ +"""Contract tests for P2-113 Linux CI workflow pipeline closure to P2-112 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_112_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_112_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_module() + + payload = gate.build_pipeline_closure_to_p2_112_payload( + project_root=Path("."), + pipeline_closure_p2_111_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_111_gate.py" + ), + pipeline_closure_p2_111_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-113" + assert payload["pipeline_closure_target_card_id"] == "P2-112" + assert payload["pipeline_closure_to_p2_112_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_112_decision"] + == "pipeline_closure_to_p2_112_verified" + ) + assert payload["pipeline_closure_to_p2_112_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_112_should_proceed"] is True + assert payload["pipeline_closure_to_p2_112_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_112_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_112_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_112_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_module() + + p2_111_gate_script = tmp_path / "pipeline_closure_p2_111_gate.py" + p2_111_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_111_runtime_test = tmp_path / "test_pipeline_closure_p2_111_gate_runtime.py" + p2_111_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_112_payload( + project_root=tmp_path, + pipeline_closure_p2_111_gate_script=p2_111_gate_script, + pipeline_closure_p2_111_runtime_test=p2_111_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_112_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_112_decision"] + == "hold_pipeline_closure_p2_112_manual_action" + ) + assert payload["pipeline_closure_to_p2_112_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_112_should_proceed"] is False + assert payload["pipeline_closure_to_p2_112_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_112_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_112_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_111_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_112_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_module() + + p2_111_gate_script = tmp_path / "pipeline_closure_p2_111_gate.py" + p2_111_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_112_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_111_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_112_payload( + project_root=tmp_path, + pipeline_closure_p2_111_gate_script=p2_111_gate_script, + pipeline_closure_p2_111_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_112_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_112_decision"] + == "hold_pipeline_closure_p2_112_manual_action" + ) + assert payload["pipeline_closure_to_p2_112_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_112(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_module() + payload = { + "pipeline_closure_to_p2_112_status": "failed", + "pipeline_closure_to_p2_112_decision": "hold_pipeline_closure_p2_112_manual_action", + "pipeline_closure_to_p2_112_exit_code": 1, + "pipeline_closure_to_p2_112_should_proceed": False, + "pipeline_closure_to_p2_112_requires_manual_action": True, + "pipeline_closure_to_p2_112_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_112_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_112_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_111_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_112.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_112.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_112_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_112_decision"] + == "hold_pipeline_closure_p2_112_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_112_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_112_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_112_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_112_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_112_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_112_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_112_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_112.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_112_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_112.md" + ) + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py new file mode 100644 index 0000000..677e0e8 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py @@ -0,0 +1,166 @@ +"""Contract tests for P2-114 Linux CI workflow pipeline closure to P2-113 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_113_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_113_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_module() + + payload = gate.build_pipeline_closure_to_p2_113_payload( + project_root=Path("."), + pipeline_closure_p2_112_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_112_gate.py" + ), + pipeline_closure_p2_112_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-114" + assert payload["pipeline_closure_target_card_id"] == "P2-113" + assert payload["pipeline_closure_to_p2_113_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_113_decision"] + == "pipeline_closure_to_p2_113_verified" + ) + assert payload["pipeline_closure_to_p2_113_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_113_should_proceed"] is True + assert payload["pipeline_closure_to_p2_113_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_113_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_113_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_113_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_module() + + p2_112_gate_script = tmp_path / "pipeline_closure_p2_112_gate.py" + p2_112_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_112_runtime_test = tmp_path / "test_pipeline_closure_p2_112_gate_runtime.py" + p2_112_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_113_payload( + project_root=tmp_path, + pipeline_closure_p2_112_gate_script=p2_112_gate_script, + pipeline_closure_p2_112_runtime_test=p2_112_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_113_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_113_decision"] + == "hold_pipeline_closure_p2_113_manual_action" + ) + assert payload["pipeline_closure_to_p2_113_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_113_should_proceed"] is False + assert payload["pipeline_closure_to_p2_113_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_113_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_113_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_112_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_113_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_module() + + p2_112_gate_script = tmp_path / "pipeline_closure_p2_112_gate.py" + p2_112_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_113_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_112_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_113_payload( + project_root=tmp_path, + pipeline_closure_p2_112_gate_script=p2_112_gate_script, + pipeline_closure_p2_112_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_113_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_113_decision"] + == "hold_pipeline_closure_p2_113_manual_action" + ) + assert payload["pipeline_closure_to_p2_113_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_113(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_module() + payload = { + "pipeline_closure_to_p2_113_status": "failed", + "pipeline_closure_to_p2_113_decision": "hold_pipeline_closure_p2_113_manual_action", + "pipeline_closure_to_p2_113_exit_code": 1, + "pipeline_closure_to_p2_113_should_proceed": False, + "pipeline_closure_to_p2_113_requires_manual_action": True, + "pipeline_closure_to_p2_113_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_113_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_113_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_112_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_113.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_113.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_113_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_113_decision"] + == "hold_pipeline_closure_p2_113_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_113_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_113_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_113_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_113_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_113_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_113_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_113_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_113.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_113_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_113.md" + ) + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py new file mode 100644 index 0000000..a1472c9 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py @@ -0,0 +1,167 @@ +"""Contract tests for P2-115 Linux CI workflow pipeline closure to P2-114 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_114_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_114_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_module() + + payload = gate.build_pipeline_closure_to_p2_114_payload( + project_root=Path("."), + pipeline_closure_p2_113_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_113_gate.py" + ), + pipeline_closure_p2_113_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-115" + assert payload["pipeline_closure_target_card_id"] == "P2-114" + assert payload["pipeline_closure_to_p2_114_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_114_decision"] + == "pipeline_closure_to_p2_114_verified" + ) + assert payload["pipeline_closure_to_p2_114_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_114_should_proceed"] is True + assert payload["pipeline_closure_to_p2_114_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_114_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_114_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_114_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_module() + + p2_113_gate_script = tmp_path / "pipeline_closure_p2_113_gate.py" + p2_113_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_113_runtime_test = tmp_path / "test_pipeline_closure_p2_113_gate_runtime.py" + p2_113_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_114_payload( + project_root=tmp_path, + pipeline_closure_p2_113_gate_script=p2_113_gate_script, + pipeline_closure_p2_113_runtime_test=p2_113_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_114_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_114_decision"] + == "hold_pipeline_closure_p2_114_manual_action" + ) + assert payload["pipeline_closure_to_p2_114_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_114_should_proceed"] is False + assert payload["pipeline_closure_to_p2_114_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_114_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_114_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_113_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_114_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_module() + + p2_113_gate_script = tmp_path / "pipeline_closure_p2_113_gate.py" + p2_113_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_114_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_113_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_114_payload( + project_root=tmp_path, + pipeline_closure_p2_113_gate_script=p2_113_gate_script, + pipeline_closure_p2_113_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_114_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_114_decision"] + == "hold_pipeline_closure_p2_114_manual_action" + ) + assert payload["pipeline_closure_to_p2_114_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_114(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_module() + payload = { + "pipeline_closure_to_p2_114_status": "failed", + "pipeline_closure_to_p2_114_decision": "hold_pipeline_closure_p2_114_manual_action", + "pipeline_closure_to_p2_114_exit_code": 1, + "pipeline_closure_to_p2_114_should_proceed": False, + "pipeline_closure_to_p2_114_requires_manual_action": True, + "pipeline_closure_to_p2_114_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_114_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_114_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_113_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_114.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_114.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_114_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_114_decision"] + == "hold_pipeline_closure_p2_114_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_114_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_114_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_114_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_114_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_114_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_114_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_114_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_114.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_114_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_114.md" + ) + + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py new file mode 100644 index 0000000..727fa59 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py @@ -0,0 +1,167 @@ +"""Contract tests for P2-116 Linux CI workflow pipeline closure to P2-115 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_115_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_115_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_module() + + payload = gate.build_pipeline_closure_to_p2_115_payload( + project_root=Path("."), + pipeline_closure_p2_114_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_114_gate.py" + ), + pipeline_closure_p2_114_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-116" + assert payload["pipeline_closure_target_card_id"] == "P2-115" + assert payload["pipeline_closure_to_p2_115_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_115_decision"] + == "pipeline_closure_to_p2_115_verified" + ) + assert payload["pipeline_closure_to_p2_115_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_115_should_proceed"] is True + assert payload["pipeline_closure_to_p2_115_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_115_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_115_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_115_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_module() + + p2_114_gate_script = tmp_path / "pipeline_closure_p2_114_gate.py" + p2_114_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_114_runtime_test = tmp_path / "test_pipeline_closure_p2_114_gate_runtime.py" + p2_114_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_115_payload( + project_root=tmp_path, + pipeline_closure_p2_114_gate_script=p2_114_gate_script, + pipeline_closure_p2_114_runtime_test=p2_114_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_115_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_115_decision"] + == "hold_pipeline_closure_p2_115_manual_action" + ) + assert payload["pipeline_closure_to_p2_115_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_115_should_proceed"] is False + assert payload["pipeline_closure_to_p2_115_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_115_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_115_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_114_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_115_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_module() + + p2_114_gate_script = tmp_path / "pipeline_closure_p2_114_gate.py" + p2_114_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_115_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_114_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_115_payload( + project_root=tmp_path, + pipeline_closure_p2_114_gate_script=p2_114_gate_script, + pipeline_closure_p2_114_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_115_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_115_decision"] + == "hold_pipeline_closure_p2_115_manual_action" + ) + assert payload["pipeline_closure_to_p2_115_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_115(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_module() + payload = { + "pipeline_closure_to_p2_115_status": "failed", + "pipeline_closure_to_p2_115_decision": "hold_pipeline_closure_p2_115_manual_action", + "pipeline_closure_to_p2_115_exit_code": 1, + "pipeline_closure_to_p2_115_should_proceed": False, + "pipeline_closure_to_p2_115_requires_manual_action": True, + "pipeline_closure_to_p2_115_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_115_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_115_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_114_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_115.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_115.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_115_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_115_decision"] + == "hold_pipeline_closure_p2_115_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_115_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_115_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_115_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_115_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_115_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_115_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_115_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_115.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_115_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_115.md" + ) + + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py new file mode 100644 index 0000000..dff28c6 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py @@ -0,0 +1,168 @@ +"""Contract tests for P2-117 Linux CI workflow pipeline closure to P2-116 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_116_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_116_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_module() + + payload = gate.build_pipeline_closure_to_p2_116_payload( + project_root=Path("."), + pipeline_closure_p2_115_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_115_gate.py" + ), + pipeline_closure_p2_115_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-117" + assert payload["pipeline_closure_target_card_id"] == "P2-116" + assert payload["pipeline_closure_to_p2_116_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_116_decision"] + == "pipeline_closure_to_p2_116_verified" + ) + assert payload["pipeline_closure_to_p2_116_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_116_should_proceed"] is True + assert payload["pipeline_closure_to_p2_116_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_116_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_116_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_116_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_module() + + p2_115_gate_script = tmp_path / "pipeline_closure_p2_115_gate.py" + p2_115_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_115_runtime_test = tmp_path / "test_pipeline_closure_p2_115_gate_runtime.py" + p2_115_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_116_payload( + project_root=tmp_path, + pipeline_closure_p2_115_gate_script=p2_115_gate_script, + pipeline_closure_p2_115_runtime_test=p2_115_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_116_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_116_decision"] + == "hold_pipeline_closure_p2_116_manual_action" + ) + assert payload["pipeline_closure_to_p2_116_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_116_should_proceed"] is False + assert payload["pipeline_closure_to_p2_116_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_116_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_116_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_115_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_116_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_module() + + p2_115_gate_script = tmp_path / "pipeline_closure_p2_115_gate.py" + p2_115_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_116_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_115_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_116_payload( + project_root=tmp_path, + pipeline_closure_p2_115_gate_script=p2_115_gate_script, + pipeline_closure_p2_115_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_116_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_116_decision"] + == "hold_pipeline_closure_p2_116_manual_action" + ) + assert payload["pipeline_closure_to_p2_116_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_116(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_module() + payload = { + "pipeline_closure_to_p2_116_status": "failed", + "pipeline_closure_to_p2_116_decision": "hold_pipeline_closure_p2_116_manual_action", + "pipeline_closure_to_p2_116_exit_code": 1, + "pipeline_closure_to_p2_116_should_proceed": False, + "pipeline_closure_to_p2_116_requires_manual_action": True, + "pipeline_closure_to_p2_116_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_116_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_116_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_115_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_116.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_116.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_116_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_116_decision"] + == "hold_pipeline_closure_p2_116_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_116_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_116_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_116_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_116_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_116_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_116_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_116_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_116.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_116_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_116.md" + ) + + + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_runtime.py new file mode 100644 index 0000000..69b56c5 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_runtime.py @@ -0,0 +1,168 @@ +"""Contract tests for P2-118 Linux CI workflow pipeline closure to P2-117 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_117_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_117_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_117_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_module() + + payload = gate.build_pipeline_closure_to_p2_117_payload( + project_root=Path("."), + pipeline_closure_p2_116_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_116_gate.py" + ), + pipeline_closure_p2_116_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-118" + assert payload["pipeline_closure_target_card_id"] == "P2-117" + assert payload["pipeline_closure_to_p2_117_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_117_decision"] + == "pipeline_closure_to_p2_117_verified" + ) + assert payload["pipeline_closure_to_p2_117_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_117_should_proceed"] is True + assert payload["pipeline_closure_to_p2_117_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_117_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_117_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_117_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_module() + + p2_116_gate_script = tmp_path / "pipeline_closure_p2_116_gate.py" + p2_116_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_116_runtime_test = tmp_path / "test_pipeline_closure_p2_116_gate_runtime.py" + p2_116_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_117_payload( + project_root=tmp_path, + pipeline_closure_p2_116_gate_script=p2_116_gate_script, + pipeline_closure_p2_116_runtime_test=p2_116_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_117_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_117_decision"] + == "hold_pipeline_closure_p2_117_manual_action" + ) + assert payload["pipeline_closure_to_p2_117_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_117_should_proceed"] is False + assert payload["pipeline_closure_to_p2_117_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_117_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_117_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_116_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_117_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_module() + + p2_116_gate_script = tmp_path / "pipeline_closure_p2_116_gate.py" + p2_116_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_117_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_116_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_117_payload( + project_root=tmp_path, + pipeline_closure_p2_116_gate_script=p2_116_gate_script, + pipeline_closure_p2_116_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_117_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_117_decision"] + == "hold_pipeline_closure_p2_117_manual_action" + ) + assert payload["pipeline_closure_to_p2_117_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract_p2_117(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_117_gate_module() + payload = { + "pipeline_closure_to_p2_117_status": "failed", + "pipeline_closure_to_p2_117_decision": "hold_pipeline_closure_p2_117_manual_action", + "pipeline_closure_to_p2_117_exit_code": 1, + "pipeline_closure_to_p2_117_should_proceed": False, + "pipeline_closure_to_p2_117_requires_manual_action": True, + "pipeline_closure_to_p2_117_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_117_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_117_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_116_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_117.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_117.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_117_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_117_decision"] + == "hold_pipeline_closure_p2_117_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_117_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_117_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_117_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_117_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_117_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_117_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_117_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_117.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_117_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_117.md" + ) + + + + + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py new file mode 100644 index 0000000..b8e7f74 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py @@ -0,0 +1,150 @@ +"""Contract tests for P2-83 Linux CI workflow pipeline closure to P2-82 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_82_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_82_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_module() + + payload = gate.build_pipeline_closure_to_p2_82_payload( + project_root=Path("."), + pipeline_gate_script=Path("scripts/run_p2_linux_ci_workflow_pipeline_gate.py"), + closure_publish_archive_gate_script=Path( + "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-83" + assert payload["pipeline_closure_target_card_id"] == "P2-82" + assert payload["pipeline_closure_to_p2_82_status"] == "passed" + assert payload["pipeline_closure_to_p2_82_decision"] == "pipeline_closure_to_p2_82_verified" + assert payload["pipeline_closure_to_p2_82_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_82_should_proceed"] is True + assert payload["pipeline_closure_to_p2_82_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_82_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_stage_closure_publish_archive_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_82_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_module() + + pipeline_script = tmp_path / "pipeline_gate.py" + pipeline_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + closure_publish_archive_gate_script = tmp_path / "closure_archive.py" + closure_publish_archive_gate_script.write_text( + "# present", + encoding="utf-8", + ) + + payload = gate.build_pipeline_closure_to_p2_82_payload( + project_root=tmp_path, + pipeline_gate_script=pipeline_script, + closure_publish_archive_gate_script=closure_publish_archive_gate_script, + ) + + assert payload["pipeline_closure_to_p2_82_status"] == "failed" + assert payload["pipeline_closure_to_p2_82_decision"] == "hold_pipeline_closure_manual_action" + assert payload["pipeline_closure_to_p2_82_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_82_should_proceed"] is False + assert payload["pipeline_closure_to_p2_82_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_82_channel"] == "blocker" + assert "pipeline_chain_to_p2_82_missing" in payload["structural_issues"] + assert ( + "pipeline_stage_closure_publish_archive_missing" in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_82_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_module() + + pipeline_script = tmp_path / "pipeline_gate.py" + pipeline_script.write_text( + "\n".join(gate.REQUIRED_PIPELINE_PATTERNS.values()), + encoding="utf-8", + ) + missing_script = tmp_path / "missing_closure_archive.py" + + payload = gate.build_pipeline_closure_to_p2_82_payload( + project_root=tmp_path, + pipeline_gate_script=pipeline_script, + closure_publish_archive_gate_script=missing_script, + ) + + assert payload["pipeline_closure_to_p2_82_status"] == "failed" + assert payload["pipeline_closure_to_p2_82_decision"] == "hold_pipeline_closure_manual_action" + assert payload["pipeline_closure_to_p2_82_exit_code"] == 1 + assert str(missing_script) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_module() + payload = { + "pipeline_closure_to_p2_82_status": "failed", + "pipeline_closure_to_p2_82_decision": "hold_pipeline_closure_manual_action", + "pipeline_closure_to_p2_82_exit_code": 1, + "pipeline_closure_to_p2_82_should_proceed": False, + "pipeline_closure_to_p2_82_requires_manual_action": True, + "pipeline_closure_to_p2_82_channel": "blocker", + "reason_codes": ["pipeline_chain_to_p2_82_missing"], + "structural_issues": ["pipeline_chain_to_p2_82_missing"], + "missing_artifacts": ["scripts/run_p2_linux_ci_workflow_pipeline_gate.py"], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_82.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_82_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_82_decision"] + == "hold_pipeline_closure_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_82_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_82_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_82_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_82_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_82_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_82_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_82_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_82.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_82_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_82.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py new file mode 100644 index 0000000..216a9b3 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py @@ -0,0 +1,149 @@ +"""Contract tests for P2-84 Linux CI workflow pipeline closure to P2-83 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_83_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_83_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_module() + + payload = gate.build_pipeline_closure_to_p2_83_payload( + project_root=Path("."), + pipeline_closure_p2_82_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_82_gate.py" + ), + pipeline_closure_p2_82_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-84" + assert payload["pipeline_closure_target_card_id"] == "P2-83" + assert payload["pipeline_closure_to_p2_83_status"] == "passed" + assert payload["pipeline_closure_to_p2_83_decision"] == "pipeline_closure_to_p2_83_verified" + assert payload["pipeline_closure_to_p2_83_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_83_should_proceed"] is True + assert payload["pipeline_closure_to_p2_83_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_83_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_83_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_83_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_module() + + p2_83_gate_script = tmp_path / "pipeline_closure_p2_82_gate.py" + p2_83_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_83_runtime_test = tmp_path / "test_pipeline_closure_p2_82_gate_runtime.py" + p2_83_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_83_payload( + project_root=tmp_path, + pipeline_closure_p2_82_gate_script=p2_83_gate_script, + pipeline_closure_p2_82_runtime_test=p2_83_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_83_status"] == "failed" + assert payload["pipeline_closure_to_p2_83_decision"] == "hold_pipeline_closure_p2_83_manual_action" + assert payload["pipeline_closure_to_p2_83_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_83_should_proceed"] is False + assert payload["pipeline_closure_to_p2_83_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_83_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_83_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_82_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_83_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_module() + + p2_83_gate_script = tmp_path / "pipeline_closure_p2_82_gate.py" + p2_83_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_83_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_82_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_83_payload( + project_root=tmp_path, + pipeline_closure_p2_82_gate_script=p2_83_gate_script, + pipeline_closure_p2_82_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_83_status"] == "failed" + assert payload["pipeline_closure_to_p2_83_decision"] == "hold_pipeline_closure_p2_83_manual_action" + assert payload["pipeline_closure_to_p2_83_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_module() + payload = { + "pipeline_closure_to_p2_83_status": "failed", + "pipeline_closure_to_p2_83_decision": "hold_pipeline_closure_p2_83_manual_action", + "pipeline_closure_to_p2_83_exit_code": 1, + "pipeline_closure_to_p2_83_should_proceed": False, + "pipeline_closure_to_p2_83_requires_manual_action": True, + "pipeline_closure_to_p2_83_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_83_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_83_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_82_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_83.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_83_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_83_decision"] + == "hold_pipeline_closure_p2_83_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_83_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_83_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_83_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_83_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_83_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_83_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_83_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_83.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_83_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_83.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py new file mode 100644 index 0000000..6c9ac02 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py @@ -0,0 +1,149 @@ +"""Contract tests for P2-85 Linux CI workflow pipeline closure to P2-84 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_84_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_84_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_module() + + payload = gate.build_pipeline_closure_to_p2_84_payload( + project_root=Path("."), + pipeline_closure_p2_83_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_83_gate.py" + ), + pipeline_closure_p2_83_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-85" + assert payload["pipeline_closure_target_card_id"] == "P2-84" + assert payload["pipeline_closure_to_p2_84_status"] == "passed" + assert payload["pipeline_closure_to_p2_84_decision"] == "pipeline_closure_to_p2_84_verified" + assert payload["pipeline_closure_to_p2_84_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_84_should_proceed"] is True + assert payload["pipeline_closure_to_p2_84_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_84_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_84_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_84_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_module() + + p2_84_gate_script = tmp_path / "pipeline_closure_p2_83_gate.py" + p2_84_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_84_runtime_test = tmp_path / "test_pipeline_closure_p2_83_gate_runtime.py" + p2_84_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_84_payload( + project_root=tmp_path, + pipeline_closure_p2_83_gate_script=p2_84_gate_script, + pipeline_closure_p2_83_runtime_test=p2_84_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_84_status"] == "failed" + assert payload["pipeline_closure_to_p2_84_decision"] == "hold_pipeline_closure_p2_84_manual_action" + assert payload["pipeline_closure_to_p2_84_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_84_should_proceed"] is False + assert payload["pipeline_closure_to_p2_84_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_84_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_84_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_83_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_84_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_module() + + p2_84_gate_script = tmp_path / "pipeline_closure_p2_83_gate.py" + p2_84_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_84_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_83_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_84_payload( + project_root=tmp_path, + pipeline_closure_p2_83_gate_script=p2_84_gate_script, + pipeline_closure_p2_83_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_84_status"] == "failed" + assert payload["pipeline_closure_to_p2_84_decision"] == "hold_pipeline_closure_p2_84_manual_action" + assert payload["pipeline_closure_to_p2_84_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_module() + payload = { + "pipeline_closure_to_p2_84_status": "failed", + "pipeline_closure_to_p2_84_decision": "hold_pipeline_closure_p2_84_manual_action", + "pipeline_closure_to_p2_84_exit_code": 1, + "pipeline_closure_to_p2_84_should_proceed": False, + "pipeline_closure_to_p2_84_requires_manual_action": True, + "pipeline_closure_to_p2_84_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_84_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_84_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_83_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_84.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_84_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_84_decision"] + == "hold_pipeline_closure_p2_84_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_84_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_84_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_84_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_84_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_84_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_84_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_84_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_84.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_84_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_84.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py new file mode 100644 index 0000000..37fc742 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py @@ -0,0 +1,149 @@ +"""Contract tests for P2-86 Linux CI workflow pipeline closure to P2-85 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_85_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_85_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_module() + + payload = gate.build_pipeline_closure_to_p2_85_payload( + project_root=Path("."), + pipeline_closure_p2_84_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_84_gate.py" + ), + pipeline_closure_p2_84_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-86" + assert payload["pipeline_closure_target_card_id"] == "P2-85" + assert payload["pipeline_closure_to_p2_85_status"] == "passed" + assert payload["pipeline_closure_to_p2_85_decision"] == "pipeline_closure_to_p2_85_verified" + assert payload["pipeline_closure_to_p2_85_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_85_should_proceed"] is True + assert payload["pipeline_closure_to_p2_85_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_85_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_85_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_85_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_module() + + p2_85_gate_script = tmp_path / "pipeline_closure_p2_84_gate.py" + p2_85_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_85_runtime_test = tmp_path / "test_pipeline_closure_p2_84_gate_runtime.py" + p2_85_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_85_payload( + project_root=tmp_path, + pipeline_closure_p2_84_gate_script=p2_85_gate_script, + pipeline_closure_p2_84_runtime_test=p2_85_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_85_status"] == "failed" + assert payload["pipeline_closure_to_p2_85_decision"] == "hold_pipeline_closure_p2_85_manual_action" + assert payload["pipeline_closure_to_p2_85_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_85_should_proceed"] is False + assert payload["pipeline_closure_to_p2_85_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_85_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_85_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_84_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_85_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_module() + + p2_85_gate_script = tmp_path / "pipeline_closure_p2_84_gate.py" + p2_85_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_85_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_84_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_85_payload( + project_root=tmp_path, + pipeline_closure_p2_84_gate_script=p2_85_gate_script, + pipeline_closure_p2_84_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_85_status"] == "failed" + assert payload["pipeline_closure_to_p2_85_decision"] == "hold_pipeline_closure_p2_85_manual_action" + assert payload["pipeline_closure_to_p2_85_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_module() + payload = { + "pipeline_closure_to_p2_85_status": "failed", + "pipeline_closure_to_p2_85_decision": "hold_pipeline_closure_p2_85_manual_action", + "pipeline_closure_to_p2_85_exit_code": 1, + "pipeline_closure_to_p2_85_should_proceed": False, + "pipeline_closure_to_p2_85_requires_manual_action": True, + "pipeline_closure_to_p2_85_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_85_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_85_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_84_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_85.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_85_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_85_decision"] + == "hold_pipeline_closure_p2_85_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_85_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_85_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_85_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_85_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_85_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_85_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_85_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_85.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_85_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_85.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py new file mode 100644 index 0000000..32c0836 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py @@ -0,0 +1,150 @@ +"""Contract tests for P2-87 Linux CI workflow pipeline closure to P2-86 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_86_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_86_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_module() + + payload = gate.build_pipeline_closure_to_p2_86_payload( + project_root=Path("."), + pipeline_closure_p2_85_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_85_gate.py" + ), + pipeline_closure_p2_85_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-87" + assert payload["pipeline_closure_target_card_id"] == "P2-86" + assert payload["pipeline_closure_to_p2_86_status"] == "passed" + assert payload["pipeline_closure_to_p2_86_decision"] == "pipeline_closure_to_p2_86_verified" + assert payload["pipeline_closure_to_p2_86_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_86_should_proceed"] is True + assert payload["pipeline_closure_to_p2_86_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_86_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_86_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_86_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_module() + + p2_86_gate_script = tmp_path / "pipeline_closure_p2_85_gate.py" + p2_86_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_86_runtime_test = tmp_path / "test_pipeline_closure_p2_85_gate_runtime.py" + p2_86_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_86_payload( + project_root=tmp_path, + pipeline_closure_p2_85_gate_script=p2_86_gate_script, + pipeline_closure_p2_85_runtime_test=p2_86_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_86_status"] == "failed" + assert payload["pipeline_closure_to_p2_86_decision"] == "hold_pipeline_closure_p2_86_manual_action" + assert payload["pipeline_closure_to_p2_86_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_86_should_proceed"] is False + assert payload["pipeline_closure_to_p2_86_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_86_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_86_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_85_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_86_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_module() + + p2_86_gate_script = tmp_path / "pipeline_closure_p2_85_gate.py" + p2_86_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_86_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_85_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_86_payload( + project_root=tmp_path, + pipeline_closure_p2_85_gate_script=p2_86_gate_script, + pipeline_closure_p2_85_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_86_status"] == "failed" + assert payload["pipeline_closure_to_p2_86_decision"] == "hold_pipeline_closure_p2_86_manual_action" + assert payload["pipeline_closure_to_p2_86_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_module() + payload = { + "pipeline_closure_to_p2_86_status": "failed", + "pipeline_closure_to_p2_86_decision": "hold_pipeline_closure_p2_86_manual_action", + "pipeline_closure_to_p2_86_exit_code": 1, + "pipeline_closure_to_p2_86_should_proceed": False, + "pipeline_closure_to_p2_86_requires_manual_action": True, + "pipeline_closure_to_p2_86_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_86_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_86_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_85_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_86.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_86_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_86_decision"] + == "hold_pipeline_closure_p2_86_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_86_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_86_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_86_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_86_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_86_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_86_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_86_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_86.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_86_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_86.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py new file mode 100644 index 0000000..43e945e --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py @@ -0,0 +1,149 @@ +"""Contract tests for P2-88 Linux CI workflow pipeline closure to P2-87 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_87_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_87_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_module() + + payload = gate.build_pipeline_closure_to_p2_87_payload( + project_root=Path("."), + pipeline_closure_p2_86_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_86_gate.py" + ), + pipeline_closure_p2_86_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-88" + assert payload["pipeline_closure_target_card_id"] == "P2-87" + assert payload["pipeline_closure_to_p2_87_status"] == "passed" + assert payload["pipeline_closure_to_p2_87_decision"] == "pipeline_closure_to_p2_87_verified" + assert payload["pipeline_closure_to_p2_87_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_87_should_proceed"] is True + assert payload["pipeline_closure_to_p2_87_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_87_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_87_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_87_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_module() + + p2_87_gate_script = tmp_path / "pipeline_closure_p2_86_gate.py" + p2_87_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_87_runtime_test = tmp_path / "test_pipeline_closure_p2_86_gate_runtime.py" + p2_87_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_87_payload( + project_root=tmp_path, + pipeline_closure_p2_86_gate_script=p2_87_gate_script, + pipeline_closure_p2_86_runtime_test=p2_87_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_87_status"] == "failed" + assert payload["pipeline_closure_to_p2_87_decision"] == "hold_pipeline_closure_p2_87_manual_action" + assert payload["pipeline_closure_to_p2_87_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_87_should_proceed"] is False + assert payload["pipeline_closure_to_p2_87_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_87_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_87_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_86_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_87_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_module() + + p2_87_gate_script = tmp_path / "pipeline_closure_p2_86_gate.py" + p2_87_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_87_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_86_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_87_payload( + project_root=tmp_path, + pipeline_closure_p2_86_gate_script=p2_87_gate_script, + pipeline_closure_p2_86_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_87_status"] == "failed" + assert payload["pipeline_closure_to_p2_87_decision"] == "hold_pipeline_closure_p2_87_manual_action" + assert payload["pipeline_closure_to_p2_87_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_module() + payload = { + "pipeline_closure_to_p2_87_status": "failed", + "pipeline_closure_to_p2_87_decision": "hold_pipeline_closure_p2_87_manual_action", + "pipeline_closure_to_p2_87_exit_code": 1, + "pipeline_closure_to_p2_87_should_proceed": False, + "pipeline_closure_to_p2_87_requires_manual_action": True, + "pipeline_closure_to_p2_87_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_87_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_87_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_86_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_87.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_87_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_87_decision"] + == "hold_pipeline_closure_p2_87_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_87_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_87_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_87_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_87_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_87_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_87_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_87_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_87.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_87_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_87.md" + ) \ No newline at end of file diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py new file mode 100644 index 0000000..ba11c15 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py @@ -0,0 +1,149 @@ +"""Contract tests for P2-89 Linux CI workflow pipeline closure to P2-88 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_88_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_88_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_module() + + payload = gate.build_pipeline_closure_to_p2_88_payload( + project_root=Path("."), + pipeline_closure_p2_87_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_87_gate.py" + ), + pipeline_closure_p2_87_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-89" + assert payload["pipeline_closure_target_card_id"] == "P2-88" + assert payload["pipeline_closure_to_p2_88_status"] == "passed" + assert payload["pipeline_closure_to_p2_88_decision"] == "pipeline_closure_to_p2_88_verified" + assert payload["pipeline_closure_to_p2_88_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_88_should_proceed"] is True + assert payload["pipeline_closure_to_p2_88_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_88_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_88_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_88_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_module() + + p2_88_gate_script = tmp_path / "pipeline_closure_p2_87_gate.py" + p2_88_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_88_runtime_test = tmp_path / "test_pipeline_closure_p2_87_gate_runtime.py" + p2_88_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_88_payload( + project_root=tmp_path, + pipeline_closure_p2_87_gate_script=p2_88_gate_script, + pipeline_closure_p2_87_runtime_test=p2_88_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_88_status"] == "failed" + assert payload["pipeline_closure_to_p2_88_decision"] == "hold_pipeline_closure_p2_88_manual_action" + assert payload["pipeline_closure_to_p2_88_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_88_should_proceed"] is False + assert payload["pipeline_closure_to_p2_88_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_88_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_88_missing" in payload["structural_issues"] + assert "pipeline_closure_decision_verified_p2_87_missing" in payload["structural_issues"] + + +def test_build_pipeline_closure_to_p2_88_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_module() + + p2_88_gate_script = tmp_path / "pipeline_closure_p2_87_gate.py" + p2_88_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_88_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_87_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_88_payload( + project_root=tmp_path, + pipeline_closure_p2_87_gate_script=p2_88_gate_script, + pipeline_closure_p2_87_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_88_status"] == "failed" + assert payload["pipeline_closure_to_p2_88_decision"] == "hold_pipeline_closure_p2_88_manual_action" + assert payload["pipeline_closure_to_p2_88_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_module() + payload = { + "pipeline_closure_to_p2_88_status": "failed", + "pipeline_closure_to_p2_88_decision": "hold_pipeline_closure_p2_88_manual_action", + "pipeline_closure_to_p2_88_exit_code": 1, + "pipeline_closure_to_p2_88_should_proceed": False, + "pipeline_closure_to_p2_88_requires_manual_action": True, + "pipeline_closure_to_p2_88_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_88_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_88_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_87_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_88.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_88_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_88_decision"] + == "hold_pipeline_closure_p2_88_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_88_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_88_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_88_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_88_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_88_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_88_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_88_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_88.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_88_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_88.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py new file mode 100644 index 0000000..dd1d6fb --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-90 Linux CI workflow pipeline closure to P2-89 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_89_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_89_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_module() + + payload = gate.build_pipeline_closure_to_p2_89_payload( + project_root=Path("."), + pipeline_closure_p2_88_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_88_gate.py" + ), + pipeline_closure_p2_88_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-90" + assert payload["pipeline_closure_target_card_id"] == "P2-89" + assert payload["pipeline_closure_to_p2_89_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_89_decision"] + == "pipeline_closure_to_p2_89_verified" + ) + assert payload["pipeline_closure_to_p2_89_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_89_should_proceed"] is True + assert payload["pipeline_closure_to_p2_89_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_89_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_89_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_89_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_module() + + p2_89_gate_script = tmp_path / "pipeline_closure_p2_88_gate.py" + p2_89_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_89_runtime_test = tmp_path / "test_pipeline_closure_p2_88_gate_runtime.py" + p2_89_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_89_payload( + project_root=tmp_path, + pipeline_closure_p2_88_gate_script=p2_89_gate_script, + pipeline_closure_p2_88_runtime_test=p2_89_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_89_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_89_decision"] + == "hold_pipeline_closure_p2_89_manual_action" + ) + assert payload["pipeline_closure_to_p2_89_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_89_should_proceed"] is False + assert payload["pipeline_closure_to_p2_89_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_89_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_89_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_88_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_89_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_module() + + p2_89_gate_script = tmp_path / "pipeline_closure_p2_88_gate.py" + p2_89_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_89_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_88_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_89_payload( + project_root=tmp_path, + pipeline_closure_p2_88_gate_script=p2_89_gate_script, + pipeline_closure_p2_88_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_89_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_89_decision"] + == "hold_pipeline_closure_p2_89_manual_action" + ) + assert payload["pipeline_closure_to_p2_89_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_module() + payload = { + "pipeline_closure_to_p2_89_status": "failed", + "pipeline_closure_to_p2_89_decision": "hold_pipeline_closure_p2_89_manual_action", + "pipeline_closure_to_p2_89_exit_code": 1, + "pipeline_closure_to_p2_89_should_proceed": False, + "pipeline_closure_to_p2_89_requires_manual_action": True, + "pipeline_closure_to_p2_89_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_89_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_89_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_88_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_89.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_89_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_89_decision"] + == "hold_pipeline_closure_p2_89_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_89_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_89_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_89_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_89_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_89_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_89_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_89_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_89.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_89_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_89.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py new file mode 100644 index 0000000..44ed734 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py @@ -0,0 +1,162 @@ +"""Contract tests for P2-91 Linux CI workflow pipeline closure to P2-90 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_90_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_90_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_module() + + payload = gate.build_pipeline_closure_to_p2_90_payload( + project_root=Path("."), + pipeline_closure_p2_89_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_89_gate.py" + ), + pipeline_closure_p2_89_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-91" + assert payload["pipeline_closure_target_card_id"] == "P2-90" + assert payload["pipeline_closure_to_p2_90_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_90_decision"] + == "pipeline_closure_to_p2_90_verified" + ) + assert payload["pipeline_closure_to_p2_90_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_90_should_proceed"] is True + assert payload["pipeline_closure_to_p2_90_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_90_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_90_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_90_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_module() + + p2_90_gate_script = tmp_path / "pipeline_closure_p2_89_gate.py" + p2_90_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_90_runtime_test = tmp_path / "test_pipeline_closure_p2_89_gate_runtime.py" + p2_90_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_90_payload( + project_root=tmp_path, + pipeline_closure_p2_89_gate_script=p2_90_gate_script, + pipeline_closure_p2_89_runtime_test=p2_90_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_90_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_90_decision"] + == "hold_pipeline_closure_p2_90_manual_action" + ) + assert payload["pipeline_closure_to_p2_90_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_90_should_proceed"] is False + assert payload["pipeline_closure_to_p2_90_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_90_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_90_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_89_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_90_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_module() + + p2_90_gate_script = tmp_path / "pipeline_closure_p2_89_gate.py" + p2_90_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_90_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_89_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_90_payload( + project_root=tmp_path, + pipeline_closure_p2_89_gate_script=p2_90_gate_script, + pipeline_closure_p2_89_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_90_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_90_decision"] + == "hold_pipeline_closure_p2_90_manual_action" + ) + assert payload["pipeline_closure_to_p2_90_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_module() + payload = { + "pipeline_closure_to_p2_90_status": "failed", + "pipeline_closure_to_p2_90_decision": "hold_pipeline_closure_p2_90_manual_action", + "pipeline_closure_to_p2_90_exit_code": 1, + "pipeline_closure_to_p2_90_should_proceed": False, + "pipeline_closure_to_p2_90_requires_manual_action": True, + "pipeline_closure_to_p2_90_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_90_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_90_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_89_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_90.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_90_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_90_decision"] + == "hold_pipeline_closure_p2_90_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_90_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_90_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_90_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_90_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_90_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_90_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_90_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_90.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_90_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_90.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py new file mode 100644 index 0000000..9c9fcaa --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-92 Linux CI workflow pipeline closure to P2-91 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_91_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_91_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_module() + + payload = gate.build_pipeline_closure_to_p2_91_payload( + project_root=Path("."), + pipeline_closure_p2_90_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_90_gate.py" + ), + pipeline_closure_p2_90_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-92" + assert payload["pipeline_closure_target_card_id"] == "P2-91" + assert payload["pipeline_closure_to_p2_91_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_91_decision"] + == "pipeline_closure_to_p2_91_verified" + ) + assert payload["pipeline_closure_to_p2_91_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_91_should_proceed"] is True + assert payload["pipeline_closure_to_p2_91_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_91_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_91_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_91_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_module() + + p2_91_gate_script = tmp_path / "pipeline_closure_p2_90_gate.py" + p2_91_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_91_runtime_test = tmp_path / "test_pipeline_closure_p2_90_gate_runtime.py" + p2_91_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_91_payload( + project_root=tmp_path, + pipeline_closure_p2_90_gate_script=p2_91_gate_script, + pipeline_closure_p2_90_runtime_test=p2_91_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_91_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_91_decision"] + == "hold_pipeline_closure_p2_91_manual_action" + ) + assert payload["pipeline_closure_to_p2_91_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_91_should_proceed"] is False + assert payload["pipeline_closure_to_p2_91_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_91_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_91_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_90_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_91_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_module() + + p2_91_gate_script = tmp_path / "pipeline_closure_p2_90_gate.py" + p2_91_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_91_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_90_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_91_payload( + project_root=tmp_path, + pipeline_closure_p2_90_gate_script=p2_91_gate_script, + pipeline_closure_p2_90_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_91_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_91_decision"] + == "hold_pipeline_closure_p2_91_manual_action" + ) + assert payload["pipeline_closure_to_p2_91_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_module() + payload = { + "pipeline_closure_to_p2_91_status": "failed", + "pipeline_closure_to_p2_91_decision": "hold_pipeline_closure_p2_91_manual_action", + "pipeline_closure_to_p2_91_exit_code": 1, + "pipeline_closure_to_p2_91_should_proceed": False, + "pipeline_closure_to_p2_91_requires_manual_action": True, + "pipeline_closure_to_p2_91_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_91_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_91_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_90_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_91.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_91.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_91_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_91_decision"] + == "hold_pipeline_closure_p2_91_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_91_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_91_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_91_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_91_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_91_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_91_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_91_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_91.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_91_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_91.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py new file mode 100644 index 0000000..d86bd9c --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-93 Linux CI workflow pipeline closure to P2-92 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_92_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_92_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_module() + + payload = gate.build_pipeline_closure_to_p2_92_payload( + project_root=Path("."), + pipeline_closure_p2_91_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_91_gate.py" + ), + pipeline_closure_p2_91_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-93" + assert payload["pipeline_closure_target_card_id"] == "P2-92" + assert payload["pipeline_closure_to_p2_92_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_92_decision"] + == "pipeline_closure_to_p2_92_verified" + ) + assert payload["pipeline_closure_to_p2_92_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_92_should_proceed"] is True + assert payload["pipeline_closure_to_p2_92_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_92_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_92_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_92_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_module() + + p2_92_gate_script = tmp_path / "pipeline_closure_p2_91_gate.py" + p2_92_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_92_runtime_test = tmp_path / "test_pipeline_closure_p2_91_gate_runtime.py" + p2_92_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_92_payload( + project_root=tmp_path, + pipeline_closure_p2_91_gate_script=p2_92_gate_script, + pipeline_closure_p2_91_runtime_test=p2_92_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_92_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_92_decision"] + == "hold_pipeline_closure_p2_92_manual_action" + ) + assert payload["pipeline_closure_to_p2_92_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_92_should_proceed"] is False + assert payload["pipeline_closure_to_p2_92_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_92_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_92_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_91_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_92_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_module() + + p2_92_gate_script = tmp_path / "pipeline_closure_p2_91_gate.py" + p2_92_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_92_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_91_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_92_payload( + project_root=tmp_path, + pipeline_closure_p2_91_gate_script=p2_92_gate_script, + pipeline_closure_p2_91_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_92_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_92_decision"] + == "hold_pipeline_closure_p2_92_manual_action" + ) + assert payload["pipeline_closure_to_p2_92_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_module() + payload = { + "pipeline_closure_to_p2_92_status": "failed", + "pipeline_closure_to_p2_92_decision": "hold_pipeline_closure_p2_92_manual_action", + "pipeline_closure_to_p2_92_exit_code": 1, + "pipeline_closure_to_p2_92_should_proceed": False, + "pipeline_closure_to_p2_92_requires_manual_action": True, + "pipeline_closure_to_p2_92_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_92_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_92_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_91_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_92.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_92.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_92_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_92_decision"] + == "hold_pipeline_closure_p2_92_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_92_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_92_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_92_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_92_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_92_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_92_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_92_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_92.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_92_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_92.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py new file mode 100644 index 0000000..3f2c9f4 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-94 Linux CI workflow pipeline closure to P2-93 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_93_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_93_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_module() + + payload = gate.build_pipeline_closure_to_p2_93_payload( + project_root=Path("."), + pipeline_closure_p2_92_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_92_gate.py" + ), + pipeline_closure_p2_92_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-94" + assert payload["pipeline_closure_target_card_id"] == "P2-93" + assert payload["pipeline_closure_to_p2_93_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_93_decision"] + == "pipeline_closure_to_p2_93_verified" + ) + assert payload["pipeline_closure_to_p2_93_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_93_should_proceed"] is True + assert payload["pipeline_closure_to_p2_93_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_93_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_93_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_93_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_module() + + p2_93_gate_script = tmp_path / "pipeline_closure_p2_92_gate.py" + p2_93_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_93_runtime_test = tmp_path / "test_pipeline_closure_p2_92_gate_runtime.py" + p2_93_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_93_payload( + project_root=tmp_path, + pipeline_closure_p2_92_gate_script=p2_93_gate_script, + pipeline_closure_p2_92_runtime_test=p2_93_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_93_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_93_decision"] + == "hold_pipeline_closure_p2_93_manual_action" + ) + assert payload["pipeline_closure_to_p2_93_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_93_should_proceed"] is False + assert payload["pipeline_closure_to_p2_93_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_93_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_93_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_92_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_93_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_module() + + p2_93_gate_script = tmp_path / "pipeline_closure_p2_92_gate.py" + p2_93_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_93_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_92_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_93_payload( + project_root=tmp_path, + pipeline_closure_p2_92_gate_script=p2_93_gate_script, + pipeline_closure_p2_92_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_93_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_93_decision"] + == "hold_pipeline_closure_p2_93_manual_action" + ) + assert payload["pipeline_closure_to_p2_93_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_module() + payload = { + "pipeline_closure_to_p2_93_status": "failed", + "pipeline_closure_to_p2_93_decision": "hold_pipeline_closure_p2_93_manual_action", + "pipeline_closure_to_p2_93_exit_code": 1, + "pipeline_closure_to_p2_93_should_proceed": False, + "pipeline_closure_to_p2_93_requires_manual_action": True, + "pipeline_closure_to_p2_93_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_93_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_93_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_92_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_93.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_93.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_93_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_93_decision"] + == "hold_pipeline_closure_p2_93_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_93_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_93_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_93_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_93_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_93_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_93_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_93_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_93.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_93_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_93.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py new file mode 100644 index 0000000..503bda4 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py @@ -0,0 +1,162 @@ +"""Contract tests for P2-95 Linux CI workflow pipeline closure to P2-94 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_94_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_94_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_module() + + payload = gate.build_pipeline_closure_to_p2_94_payload( + project_root=Path("."), + pipeline_closure_p2_93_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_93_gate.py" + ), + pipeline_closure_p2_93_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-95" + assert payload["pipeline_closure_target_card_id"] == "P2-94" + assert payload["pipeline_closure_to_p2_94_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_94_decision"] + == "pipeline_closure_to_p2_94_verified" + ) + assert payload["pipeline_closure_to_p2_94_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_94_should_proceed"] is True + assert payload["pipeline_closure_to_p2_94_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_94_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_94_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_94_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_module() + + p2_94_gate_script = tmp_path / "pipeline_closure_p2_93_gate.py" + p2_94_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_94_runtime_test = tmp_path / "test_pipeline_closure_p2_93_gate_runtime.py" + p2_94_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_94_payload( + project_root=tmp_path, + pipeline_closure_p2_93_gate_script=p2_94_gate_script, + pipeline_closure_p2_93_runtime_test=p2_94_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_94_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_94_decision"] + == "hold_pipeline_closure_p2_94_manual_action" + ) + assert payload["pipeline_closure_to_p2_94_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_94_should_proceed"] is False + assert payload["pipeline_closure_to_p2_94_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_94_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_94_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_93_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_94_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_module() + + p2_94_gate_script = tmp_path / "pipeline_closure_p2_93_gate.py" + p2_94_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_94_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_93_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_94_payload( + project_root=tmp_path, + pipeline_closure_p2_93_gate_script=p2_94_gate_script, + pipeline_closure_p2_93_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_94_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_94_decision"] + == "hold_pipeline_closure_p2_94_manual_action" + ) + assert payload["pipeline_closure_to_p2_94_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_module() + payload = { + "pipeline_closure_to_p2_94_status": "failed", + "pipeline_closure_to_p2_94_decision": "hold_pipeline_closure_p2_94_manual_action", + "pipeline_closure_to_p2_94_exit_code": 1, + "pipeline_closure_to_p2_94_should_proceed": False, + "pipeline_closure_to_p2_94_requires_manual_action": True, + "pipeline_closure_to_p2_94_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_94_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_94_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_93_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_94.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_94.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_94_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_94_decision"] + == "hold_pipeline_closure_p2_94_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_94_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_94_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_94_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_94_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_94_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_94_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_94_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_94.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_94_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_94.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py new file mode 100644 index 0000000..c5a0479 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-96 Linux CI workflow pipeline closure to P2-95 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_95_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_95_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_module() + + payload = gate.build_pipeline_closure_to_p2_95_payload( + project_root=Path("."), + pipeline_closure_p2_94_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_94_gate.py" + ), + pipeline_closure_p2_94_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-96" + assert payload["pipeline_closure_target_card_id"] == "P2-95" + assert payload["pipeline_closure_to_p2_95_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_95_decision"] + == "pipeline_closure_to_p2_95_verified" + ) + assert payload["pipeline_closure_to_p2_95_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_95_should_proceed"] is True + assert payload["pipeline_closure_to_p2_95_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_95_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_95_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_95_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_module() + + p2_95_gate_script = tmp_path / "pipeline_closure_p2_94_gate.py" + p2_95_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_95_runtime_test = tmp_path / "test_pipeline_closure_p2_94_gate_runtime.py" + p2_95_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_95_payload( + project_root=tmp_path, + pipeline_closure_p2_94_gate_script=p2_95_gate_script, + pipeline_closure_p2_94_runtime_test=p2_95_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_95_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_95_decision"] + == "hold_pipeline_closure_p2_95_manual_action" + ) + assert payload["pipeline_closure_to_p2_95_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_95_should_proceed"] is False + assert payload["pipeline_closure_to_p2_95_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_95_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_95_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_94_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_95_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_module() + + p2_95_gate_script = tmp_path / "pipeline_closure_p2_94_gate.py" + p2_95_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_95_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_94_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_95_payload( + project_root=tmp_path, + pipeline_closure_p2_94_gate_script=p2_95_gate_script, + pipeline_closure_p2_94_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_95_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_95_decision"] + == "hold_pipeline_closure_p2_95_manual_action" + ) + assert payload["pipeline_closure_to_p2_95_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_module() + payload = { + "pipeline_closure_to_p2_95_status": "failed", + "pipeline_closure_to_p2_95_decision": "hold_pipeline_closure_p2_95_manual_action", + "pipeline_closure_to_p2_95_exit_code": 1, + "pipeline_closure_to_p2_95_should_proceed": False, + "pipeline_closure_to_p2_95_requires_manual_action": True, + "pipeline_closure_to_p2_95_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_95_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_95_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_94_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_95.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_95.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_95_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_95_decision"] + == "hold_pipeline_closure_p2_95_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_95_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_95_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_95_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_95_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_95_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_95_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_95_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_95.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_95_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_95.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py new file mode 100644 index 0000000..4df3c03 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py @@ -0,0 +1,162 @@ +"""Contract tests for P2-97 Linux CI workflow pipeline closure to P2-96 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_96_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_96_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_module() + + payload = gate.build_pipeline_closure_to_p2_96_payload( + project_root=Path("."), + pipeline_closure_p2_95_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_95_gate.py" + ), + pipeline_closure_p2_95_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-97" + assert payload["pipeline_closure_target_card_id"] == "P2-96" + assert payload["pipeline_closure_to_p2_96_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_96_decision"] + == "pipeline_closure_to_p2_96_verified" + ) + assert payload["pipeline_closure_to_p2_96_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_96_should_proceed"] is True + assert payload["pipeline_closure_to_p2_96_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_96_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_96_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_96_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_module() + + p2_96_gate_script = tmp_path / "pipeline_closure_p2_95_gate.py" + p2_96_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_96_runtime_test = tmp_path / "test_pipeline_closure_p2_95_gate_runtime.py" + p2_96_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_96_payload( + project_root=tmp_path, + pipeline_closure_p2_95_gate_script=p2_96_gate_script, + pipeline_closure_p2_95_runtime_test=p2_96_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_96_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_96_decision"] + == "hold_pipeline_closure_p2_96_manual_action" + ) + assert payload["pipeline_closure_to_p2_96_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_96_should_proceed"] is False + assert payload["pipeline_closure_to_p2_96_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_96_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_96_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_95_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_96_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_module() + + p2_96_gate_script = tmp_path / "pipeline_closure_p2_95_gate.py" + p2_96_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_96_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_95_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_96_payload( + project_root=tmp_path, + pipeline_closure_p2_95_gate_script=p2_96_gate_script, + pipeline_closure_p2_95_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_96_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_96_decision"] + == "hold_pipeline_closure_p2_96_manual_action" + ) + assert payload["pipeline_closure_to_p2_96_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_module() + payload = { + "pipeline_closure_to_p2_96_status": "failed", + "pipeline_closure_to_p2_96_decision": "hold_pipeline_closure_p2_96_manual_action", + "pipeline_closure_to_p2_96_exit_code": 1, + "pipeline_closure_to_p2_96_should_proceed": False, + "pipeline_closure_to_p2_96_requires_manual_action": True, + "pipeline_closure_to_p2_96_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_96_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_96_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_95_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_96.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_96_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_96_decision"] + == "hold_pipeline_closure_p2_96_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_96_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_96_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_96_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_96_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_96_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_96_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_96_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_96.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_96_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_96.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py new file mode 100644 index 0000000..130184f --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py @@ -0,0 +1,163 @@ +"""Contract tests for P2-98 Linux CI workflow pipeline closure to P2-97 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_97_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_97_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_module() + + payload = gate.build_pipeline_closure_to_p2_97_payload( + project_root=Path("."), + pipeline_closure_p2_96_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_96_gate.py" + ), + pipeline_closure_p2_96_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-98" + assert payload["pipeline_closure_target_card_id"] == "P2-97" + assert payload["pipeline_closure_to_p2_97_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_97_decision"] + == "pipeline_closure_to_p2_97_verified" + ) + assert payload["pipeline_closure_to_p2_97_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_97_should_proceed"] is True + assert payload["pipeline_closure_to_p2_97_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_97_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_97_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_97_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_module() + + p2_96_gate_script = tmp_path / "pipeline_closure_p2_96_gate.py" + p2_96_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_96_runtime_test = tmp_path / "test_pipeline_closure_p2_96_gate_runtime.py" + p2_96_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_97_payload( + project_root=tmp_path, + pipeline_closure_p2_96_gate_script=p2_96_gate_script, + pipeline_closure_p2_96_runtime_test=p2_96_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_97_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_97_decision"] + == "hold_pipeline_closure_p2_97_manual_action" + ) + assert payload["pipeline_closure_to_p2_97_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_97_should_proceed"] is False + assert payload["pipeline_closure_to_p2_97_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_97_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_97_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_96_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_97_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_module() + + p2_96_gate_script = tmp_path / "pipeline_closure_p2_96_gate.py" + p2_96_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_97_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_96_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_97_payload( + project_root=tmp_path, + pipeline_closure_p2_96_gate_script=p2_96_gate_script, + pipeline_closure_p2_96_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_97_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_97_decision"] + == "hold_pipeline_closure_p2_97_manual_action" + ) + assert payload["pipeline_closure_to_p2_97_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_module() + payload = { + "pipeline_closure_to_p2_97_status": "failed", + "pipeline_closure_to_p2_97_decision": "hold_pipeline_closure_p2_97_manual_action", + "pipeline_closure_to_p2_97_exit_code": 1, + "pipeline_closure_to_p2_97_should_proceed": False, + "pipeline_closure_to_p2_97_requires_manual_action": True, + "pipeline_closure_to_p2_97_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_97_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_97_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_96_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_97.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_97.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_97_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_97_decision"] + == "hold_pipeline_closure_p2_97_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_97_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_97_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_97_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_97_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_97_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_97_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_97_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_97.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_97_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_97.md" + ) + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py new file mode 100644 index 0000000..bc87b19 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py @@ -0,0 +1,164 @@ +"""Contract tests for P2-99 Linux CI workflow pipeline closure to P2-98 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_98_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_98_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_module() + + payload = gate.build_pipeline_closure_to_p2_98_payload( + project_root=Path("."), + pipeline_closure_p2_97_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_97_gate.py" + ), + pipeline_closure_p2_97_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-99" + assert payload["pipeline_closure_target_card_id"] == "P2-98" + assert payload["pipeline_closure_to_p2_98_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_98_decision"] + == "pipeline_closure_to_p2_98_verified" + ) + assert payload["pipeline_closure_to_p2_98_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_98_should_proceed"] is True + assert payload["pipeline_closure_to_p2_98_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_98_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_98_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_98_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_module() + + p2_97_gate_script = tmp_path / "pipeline_closure_p2_97_gate.py" + p2_97_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_97_runtime_test = tmp_path / "test_pipeline_closure_p2_97_gate_runtime.py" + p2_97_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_98_payload( + project_root=tmp_path, + pipeline_closure_p2_97_gate_script=p2_97_gate_script, + pipeline_closure_p2_97_runtime_test=p2_97_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_98_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_98_decision"] + == "hold_pipeline_closure_p2_98_manual_action" + ) + assert payload["pipeline_closure_to_p2_98_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_98_should_proceed"] is False + assert payload["pipeline_closure_to_p2_98_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_98_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_98_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_97_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_98_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_module() + + p2_97_gate_script = tmp_path / "pipeline_closure_p2_97_gate.py" + p2_97_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_98_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_97_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_98_payload( + project_root=tmp_path, + pipeline_closure_p2_97_gate_script=p2_97_gate_script, + pipeline_closure_p2_97_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_98_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_98_decision"] + == "hold_pipeline_closure_p2_98_manual_action" + ) + assert payload["pipeline_closure_to_p2_98_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_module() + payload = { + "pipeline_closure_to_p2_98_status": "failed", + "pipeline_closure_to_p2_98_decision": "hold_pipeline_closure_p2_98_manual_action", + "pipeline_closure_to_p2_98_exit_code": 1, + "pipeline_closure_to_p2_98_should_proceed": False, + "pipeline_closure_to_p2_98_requires_manual_action": True, + "pipeline_closure_to_p2_98_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_98_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_98_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_97_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_98.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_98.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_98_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_98_decision"] + == "hold_pipeline_closure_p2_98_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_98_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_98_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_98_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_98_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_98_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_98_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_98_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_98.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_98_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_98.md" + ) + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py new file mode 100644 index 0000000..2d7f5da --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_runtime.py @@ -0,0 +1,164 @@ +"""Contract tests for P2-100 Linux CI workflow pipeline closure to P2-99 gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_pipeline_closure_p2_99_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_closure_p2_99_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_closure_to_p2_99_payload_passes_on_repo_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_module() + + payload = gate.build_pipeline_closure_to_p2_99_payload( + project_root=Path("."), + pipeline_closure_p2_98_gate_script=Path( + "scripts/run_p2_linux_ci_workflow_pipeline_closure_p2_98_gate.py" + ), + pipeline_closure_p2_98_runtime_test=Path( + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py" + ), + ) + + assert payload["pipeline_closure_card_id"] == "P2-100" + assert payload["pipeline_closure_target_card_id"] == "P2-99" + assert payload["pipeline_closure_to_p2_99_status"] == "passed" + assert ( + payload["pipeline_closure_to_p2_99_decision"] + == "pipeline_closure_to_p2_99_verified" + ) + assert payload["pipeline_closure_to_p2_99_exit_code"] == 0 + assert payload["pipeline_closure_to_p2_99_should_proceed"] is True + assert payload["pipeline_closure_to_p2_99_requires_manual_action"] is False + assert payload["pipeline_closure_to_p2_99_channel"] == "release" + assert payload["structural_issues"] == [] + assert payload["missing_artifacts"] == [] + assert "pipeline_closure_p2_99_contract_wired" in payload["reason_codes"] + + +def test_build_pipeline_closure_to_p2_99_payload_fails_on_missing_pattern(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_module() + + p2_98_gate_script = tmp_path / "pipeline_closure_p2_98_gate.py" + p2_98_gate_script.write_text( + "\n".join( + [ + '"""tmp"""', + "def main() -> int:", + " return 0", + ] + ), + encoding="utf-8", + ) + p2_98_runtime_test = tmp_path / "test_pipeline_closure_p2_98_gate_runtime.py" + p2_98_runtime_test.write_text("# present", encoding="utf-8") + + payload = gate.build_pipeline_closure_to_p2_99_payload( + project_root=tmp_path, + pipeline_closure_p2_98_gate_script=p2_98_gate_script, + pipeline_closure_p2_98_runtime_test=p2_98_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_99_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_99_decision"] + == "hold_pipeline_closure_p2_99_manual_action" + ) + assert payload["pipeline_closure_to_p2_99_exit_code"] == 1 + assert payload["pipeline_closure_to_p2_99_should_proceed"] is False + assert payload["pipeline_closure_to_p2_99_requires_manual_action"] is True + assert payload["pipeline_closure_to_p2_99_channel"] == "blocker" + assert "pipeline_closure_card_id_p2_99_missing" in payload["structural_issues"] + assert ( + "pipeline_closure_decision_verified_p2_98_missing" + in payload["structural_issues"] + ) + + +def test_build_pipeline_closure_to_p2_99_payload_fails_on_missing_artifact(tmp_path: Path): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_module() + + p2_98_gate_script = tmp_path / "pipeline_closure_p2_98_gate.py" + p2_98_gate_script.write_text( + "\n".join(gate.REQUIRED_P2_99_CLOSURE_PATTERNS.values()), + encoding="utf-8", + ) + missing_runtime_test = tmp_path / "missing_pipeline_closure_p2_98_gate_runtime.py" + + payload = gate.build_pipeline_closure_to_p2_99_payload( + project_root=tmp_path, + pipeline_closure_p2_98_gate_script=p2_98_gate_script, + pipeline_closure_p2_98_runtime_test=missing_runtime_test, + ) + + assert payload["pipeline_closure_to_p2_99_status"] == "failed" + assert ( + payload["pipeline_closure_to_p2_99_decision"] + == "hold_pipeline_closure_p2_99_manual_action" + ) + assert payload["pipeline_closure_to_p2_99_exit_code"] == 1 + assert str(missing_runtime_test) in payload["missing_artifacts"] + assert "pipeline_closure_required_artifacts_missing" in payload["reason_codes"] + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_ci_workflow_pipeline_closure_p2_99_gate_module() + payload = { + "pipeline_closure_to_p2_99_status": "failed", + "pipeline_closure_to_p2_99_decision": "hold_pipeline_closure_p2_99_manual_action", + "pipeline_closure_to_p2_99_exit_code": 1, + "pipeline_closure_to_p2_99_should_proceed": False, + "pipeline_closure_to_p2_99_requires_manual_action": True, + "pipeline_closure_to_p2_99_channel": "blocker", + "reason_codes": ["pipeline_closure_card_id_p2_99_missing"], + "structural_issues": ["pipeline_closure_card_id_p2_99_missing"], + "missing_artifacts": [ + "tests/test_p2_linux_ci_workflow_pipeline_closure_p2_98_gate_runtime.py" + ], + } + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_99.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_pipeline_closure_p2_99.md" + ), + ) + + assert outputs["workflow_pipeline_closure_to_p2_99_status"] == "failed" + assert ( + outputs["workflow_pipeline_closure_to_p2_99_decision"] + == "hold_pipeline_closure_p2_99_manual_action" + ) + assert outputs["workflow_pipeline_closure_to_p2_99_exit_code"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_99_should_proceed"] == "false" + assert ( + outputs["workflow_pipeline_closure_to_p2_99_requires_manual_action"] == "true" + ) + assert outputs["workflow_pipeline_closure_to_p2_99_channel"] == "blocker" + assert outputs["workflow_pipeline_closure_to_p2_99_structural_issues_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_99_missing_artifacts_count"] == "1" + assert outputs["workflow_pipeline_closure_to_p2_99_report_json"].endswith( + "ci_workflow_pipeline_closure_p2_99.json" + ) + assert outputs["workflow_pipeline_closure_to_p2_99_report_markdown"].endswith( + "ci_workflow_pipeline_closure_p2_99.md" + ) + + + diff --git a/tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py b/tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py new file mode 100644 index 0000000..8baba47 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_pipeline_gate_runtime.py @@ -0,0 +1,4989 @@ +"""Contract tests for P2-26 Linux CI workflow pipeline gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_ci_workflow_pipeline_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_pipeline_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_pipeline_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _build_commands(**overrides): + gate = _load_ci_workflow_pipeline_gate_module() + kwargs = { + "python_executable": "python", + "ci_matrix_path": Path(".claude/reports/linux_unified_gate/ci_matrix.json"), + "workflow_plan_output": Path(".claude/reports/linux_unified_gate/ci_workflow_plan.json"), + "workflow_path": Path(".github/workflows/linux_unified_gate.yml"), + "metadata_path": Path(".claude/reports/linux_unified_gate/ci_workflow_render.json"), + "command_guard_output": Path(".claude/reports/linux_unified_gate/ci_workflow_command_guard.json"), + "governance_report_output": Path(".claude/reports/linux_unified_gate/ci_workflow_governance.json"), + "governance_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_governance_publish.json" + ), + "governance_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_governance_publish.md" + ), + "execution_decision_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_execution_decision.json" + ), + "execution_decision_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_execution_decision.md" + ), + "dispatch_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch.json"), + "dispatch_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_dispatch.md"), + "dispatch_execution_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.json" + ), + "dispatch_execution_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_execution.md" + ), + "dispatch_trace_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.json" + ), + "dispatch_trace_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_trace.md" + ), + "dispatch_completion_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.json" + ), + "dispatch_completion_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_dispatch_completion.md" + ), + "terminal_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json" + ), + "terminal_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md" + ), + "release_handoff_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_handoff.json" + ), + "release_handoff_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_handoff.md" + ), + "release_trigger_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_trigger.json" + ), + "release_trigger_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_trigger.md" + ), + "release_trace_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_trace.json" + ), + "release_trace_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_trace.md" + ), + "release_completion_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_completion.json" + ), + "release_completion_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_completion.md" + ), + "release_terminal_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json" + ), + "release_terminal_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md" + ), + "release_finalization_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_finalization.json" + ), + "release_finalization_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_finalization.md" + ), + "release_closure_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_closure.json" + ), + "release_closure_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_closure.md" + ), + "release_archive_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_archive.json" + ), + "release_archive_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_archive.md" + ), + "release_verdict_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_verdict.json" + ), + "release_verdict_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_verdict.md" + ), + "release_incident_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_incident.json" + ), + "release_incident_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_incident.md" + ), + "release_terminal_verdict_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json" + ), + "release_terminal_verdict_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md" + ), + "release_delivery_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery.json" + ), + "release_delivery_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery.md" + ), + "release_delivery_terminal_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json" + ), + "release_delivery_terminal_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md" + ), + "release_delivery_final_verdict_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json" + ), + "release_delivery_final_verdict_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md" + ), + "release_follow_up_dispatch_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json" + ), + "release_follow_up_dispatch_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md" + ), + "release_follow_up_closure_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json" + ), + "release_follow_up_closure_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md" + ), + "release_follow_up_terminal_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json" + ), + "release_follow_up_terminal_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md" + ), + "release_follow_up_final_verdict_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json" + ), + "release_follow_up_final_verdict_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md" + ), + "release_final_outcome_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json" + ), + "release_final_outcome_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md" + ), + "release_final_terminal_publish_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json" + ), + "release_final_terminal_publish_markdown_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md" + ), + "release_final_handoff_json_output": Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json" + ), + "release_final_handoff_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md"), + "release_final_closure_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json"), + "release_final_closure_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md"), + "release_final_closure_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json"), + "release_final_closure_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md"), + "release_final_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json"), + "release_final_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md"), + "release_final_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json"), + "release_final_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md"), + "release_final_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json"), + "release_final_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md"), + "release_final_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json"), + "release_final_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md"), + "gate_manifest_drift_json_output": Path(".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json"), + "gate_manifest_drift_markdown_output": Path(".claude/reports/linux_unified_gate/linux_gate_manifest_drift.md"), + "terminal_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json"), + "terminal_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md"), + "linux_validation_dispatch_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json"), + "linux_validation_dispatch_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md"), + "linux_validation_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json"), + "linux_validation_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md"), + "linux_validation_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json"), + "linux_validation_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md"), + "linux_validation_terminal_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json"), + "linux_validation_terminal_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md"), + "linux_validation_final_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json"), + "linux_validation_final_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md"), + "linux_validation_final_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json"), + "linux_validation_final_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md"), + "linux_validation_final_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json"), + "linux_validation_final_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md"), + "linux_validation_terminal_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json"), + "linux_validation_terminal_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md"), + "linux_validation_terminal_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json"), + "linux_validation_terminal_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md"), + "linux_validation_terminal_dispatch_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json"), + "linux_validation_terminal_dispatch_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md"), + "linux_validation_terminal_dispatch_execution_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json"), + "linux_validation_terminal_dispatch_execution_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md"), + "linux_validation_terminal_dispatch_trace_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json"), + "linux_validation_terminal_dispatch_trace_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md"), + "linux_validation_terminal_dispatch_completion_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json"), + "linux_validation_terminal_dispatch_completion_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md"), + "linux_validation_terminal_dispatch_terminal_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json"), + "linux_validation_terminal_dispatch_terminal_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md"), + "linux_validation_terminal_dispatch_final_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json"), + "linux_validation_terminal_dispatch_final_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md"), + "linux_validation_terminal_dispatch_final_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json"), + "linux_validation_terminal_dispatch_final_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md"), + "linux_validation_terminal_dispatch_final_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json"), + "linux_validation_terminal_dispatch_final_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md"), + "linux_validation_terminal_dispatch_terminal_verdict_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json"), + "linux_validation_terminal_dispatch_terminal_verdict_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md"), + "linux_validation_terminal_dispatch_terminal_verdict_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json"), + "linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md"), + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json"), + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json"), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_output": Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md"), + "workflow_name": "Linux Unified Gate", + "python_version": "3.11", + "artifact_prefix": "linux-unified-summary", + "target_environment": "production", + "release_channel": "stable", + "workflow_ref": "main", + "release_workflow_path": ".github/workflows/release.yml", + "release_workflow_ref": "main", + "release_timeout_seconds": 900, + "release_command": "", + "release_trace_poll_timeout_seconds": 600, + "release_completion_poll_interval_seconds": 30, + "release_completion_max_polls": 20, + "release_completion_poll_timeout_seconds": 600, + "incident_timeout_seconds": 600, + "incident_command": "", + "incident_repo": "", + "incident_label": "release-incident", + "incident_title_prefix": "[release-incident]", + "follow_up_timeout_seconds": 600, + "follow_up_command": "", + "follow_up_repo": "", + "follow_up_label": "release-follow-up", + "follow_up_title_prefix": "[release-follow-up]", + "on_block_policy": "fail", + "dispatch_timeout_seconds": 300, + "trace_poll_timeout_seconds": 600, + "completion_poll_interval_seconds": 30, + "completion_max_polls": 20, + "completion_poll_timeout_seconds": 600, + "linux_validation_timeout_seconds": 7200, + "strict_generated_at": False, + "sync_write": False, + "command_guard_write": False, + "dispatch_trace_poll_now": False, + "release_trace_poll_now": False, + "completion_allow_in_progress": False, + "release_completion_allow_in_progress": False, + "on_release_hold_policy": "pass", + "skip_plan": False, + "skip_yaml": False, + "skip_sync": False, + "skip_command_guard": False, + "skip_governance": False, + "skip_governance_publish": False, + "skip_decision": False, + "skip_dispatch": False, + "skip_dispatch_execution": False, + "skip_dispatch_trace": False, + "skip_dispatch_completion": False, + "skip_dispatch_terminal_publish": False, + "skip_dispatch_release_handoff": False, + "skip_release_trigger": False, + "skip_release_trace": False, + "skip_release_completion": False, + "skip_release_terminal_publish": False, + "skip_release_finalization": False, + "skip_release_closure": False, + "skip_release_archive": False, + "skip_release_verdict": False, + "skip_release_incident": False, + "skip_release_terminal_verdict": False, + "skip_release_delivery": False, + "skip_release_delivery_terminal_publish": False, + "skip_release_delivery_final_verdict": False, + "skip_release_follow_up_dispatch": False, + "skip_release_follow_up_closure": False, + "skip_release_follow_up_terminal_publish": False, + "skip_release_follow_up_final_verdict": False, + "skip_release_final_outcome": False, + "skip_release_final_terminal_publish": False, + "skip_release_final_handoff": False, + "skip_release_final_closure": False, + "skip_release_final_closure_publish": False, + "skip_release_final_archive": False, + "skip_release_final_verdict": False, + "skip_release_final_verdict_publish": False, + "skip_release_final_publish_archive": False, + "skip_gate_manifest_drift": False, + "skip_terminal_verdict": False, + "skip_linux_validation_dispatch": False, + "skip_linux_validation_verdict": False, + "skip_linux_validation_verdict_publish": False, + "skip_linux_validation_terminal_publish": False, + "skip_linux_validation_final_verdict": False, + "skip_linux_validation_final_verdict_publish": False, + "skip_linux_validation_final_publish_archive": False, + "skip_linux_validation_terminal_verdict": False, + "skip_linux_validation_terminal_verdict_publish": False, + "skip_linux_validation_terminal_dispatch": False, + "skip_linux_validation_terminal_dispatch_execution": False, + "skip_linux_validation_terminal_dispatch_trace": False, + "skip_linux_validation_terminal_dispatch_completion": False, + "skip_linux_validation_terminal_dispatch_terminal_publish": False, + "skip_linux_validation_terminal_dispatch_final_verdict": False, + "skip_linux_validation_terminal_dispatch_final_verdict_publish": False, + "skip_linux_validation_terminal_dispatch_final_publish_archive": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict_publish": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict_closure": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish": False, + "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive": False, + } + kwargs.update(overrides) + if "skip_terminal_verdict" not in overrides: + kwargs["skip_terminal_verdict"] = bool( + kwargs["skip_gate_manifest_drift"] + or kwargs["skip_release_final_publish_archive"] + ) + if "skip_linux_validation_dispatch" not in overrides: + kwargs["skip_linux_validation_dispatch"] = bool(kwargs["skip_terminal_verdict"]) + if "skip_linux_validation_verdict" not in overrides: + kwargs["skip_linux_validation_verdict"] = bool( + kwargs["skip_linux_validation_dispatch"] + ) + if "skip_linux_validation_verdict_publish" not in overrides: + kwargs["skip_linux_validation_verdict_publish"] = bool( + kwargs["skip_linux_validation_verdict"] + ) + if "skip_linux_validation_terminal_publish" not in overrides: + kwargs["skip_linux_validation_terminal_publish"] = bool( + kwargs["skip_linux_validation_verdict_publish"] + ) + if "skip_linux_validation_final_verdict" not in overrides: + kwargs["skip_linux_validation_final_verdict"] = bool( + kwargs["skip_linux_validation_terminal_publish"] + ) + if "skip_linux_validation_final_verdict_publish" not in overrides: + kwargs["skip_linux_validation_final_verdict_publish"] = bool( + kwargs["skip_linux_validation_final_verdict"] + ) + if "skip_linux_validation_final_publish_archive" not in overrides: + kwargs["skip_linux_validation_final_publish_archive"] = bool( + kwargs["skip_linux_validation_final_verdict_publish"] + ) + if "skip_linux_validation_terminal_verdict" not in overrides: + kwargs["skip_linux_validation_terminal_verdict"] = bool( + kwargs["skip_linux_validation_final_publish_archive"] + ) + if "skip_linux_validation_terminal_verdict_publish" not in overrides: + kwargs["skip_linux_validation_terminal_verdict_publish"] = bool( + kwargs["skip_linux_validation_terminal_verdict"] + ) + if "skip_linux_validation_terminal_dispatch" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch"] = bool( + kwargs["skip_linux_validation_terminal_verdict_publish"] + ) + if "skip_linux_validation_terminal_dispatch_execution" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_execution"] = bool( + kwargs["skip_linux_validation_terminal_dispatch"] + ) + if "skip_linux_validation_terminal_dispatch_trace" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_trace"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_execution"] + ) + if "skip_linux_validation_terminal_dispatch_completion" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_completion"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_trace"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_publish" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_publish"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_completion"] + ) + if "skip_linux_validation_terminal_dispatch_final_verdict" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_final_verdict"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_publish"] + ) + if "skip_linux_validation_terminal_dispatch_final_verdict_publish" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_final_verdict_publish"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_final_verdict"] + ) + if "skip_linux_validation_terminal_dispatch_final_publish_archive" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_final_publish_archive"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_final_verdict_publish"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_final_publish_archive"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict_publish" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure"] + ) + if "skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" not in overrides: + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive"] = bool( + kwargs["skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish"] + ) + return gate.build_ci_workflow_pipeline_commands(**kwargs) + + +def test_build_ci_workflow_pipeline_commands_default_stage_chain(): + commands = _build_commands() + assert [stage for stage, _ in commands] == [ + "workflow_plan", + "workflow_yaml", + "workflow_sync", + "workflow_command_guard", + "workflow_governance", + "workflow_governance_publish", + "workflow_decision", + "workflow_dispatch", + "workflow_dispatch_execution", + "workflow_dispatch_trace", + "workflow_dispatch_completion", + "workflow_terminal_publish", + "workflow_release_handoff", + "workflow_release_trigger", + "workflow_release_trace", + "workflow_release_completion", + "workflow_release_terminal_publish", + "workflow_release_finalization", + "workflow_release_closure", + "workflow_release_archive", + "workflow_release_verdict", + "workflow_release_incident", + "workflow_release_terminal_verdict", + "workflow_release_delivery", + "workflow_release_delivery_terminal_publish", + "workflow_release_delivery_final_verdict", + "workflow_release_follow_up_dispatch", + "workflow_release_follow_up_closure", + "workflow_release_follow_up_terminal_publish", + "workflow_release_follow_up_final_verdict", + "workflow_release_final_outcome", + "workflow_release_final_terminal_publish", + "workflow_release_final_handoff", + "workflow_release_final_closure", + "workflow_release_final_closure_publish", + "workflow_release_final_archive", + "workflow_release_final_verdict", + "workflow_release_final_verdict_publish", + "workflow_release_final_publish_archive", + "workflow_gate_manifest_drift", + "workflow_terminal_verdict", + "workflow_linux_validation_dispatch", + "workflow_linux_validation_verdict", + "workflow_linux_validation_verdict_publish", + "workflow_linux_validation_terminal_publish", + "workflow_linux_validation_final_verdict", + "workflow_linux_validation_final_verdict_publish", + "workflow_linux_validation_final_publish_archive", + "workflow_linux_validation_terminal_verdict", + "workflow_linux_validation_terminal_verdict_publish", + "workflow_linux_validation_terminal_dispatch", + "workflow_linux_validation_terminal_dispatch_execution", + "workflow_linux_validation_terminal_dispatch_trace", + "workflow_linux_validation_terminal_dispatch_completion", + "workflow_linux_validation_terminal_dispatch_terminal_publish", + "workflow_linux_validation_terminal_dispatch_final_verdict", + "workflow_linux_validation_terminal_dispatch_final_verdict_publish", + "workflow_linux_validation_terminal_dispatch_final_publish_archive", + "workflow_linux_validation_terminal_dispatch_terminal_verdict", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive", + ] + + assert commands[0][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_plan_gate.py"] + assert commands[1][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_yaml_gate.py"] + assert commands[2][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_sync_gate.py"] + assert commands[3][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_command_guard_gate.py", + ] + assert commands[4][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_governance_gate.py"] + assert commands[5][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_governance_publish_gate.py", + ] + assert commands[6][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_decision_gate.py"] + assert commands[7][1][:2] == ["python", "scripts/run_p2_linux_ci_workflow_dispatch_gate.py"] + assert commands[8][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_dispatch_execution_gate.py", + ] + assert commands[9][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_dispatch_trace_gate.py", + ] + assert commands[10][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_dispatch_completion_gate.py", + ] + assert commands[11][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_terminal_publish_gate.py", + ] + assert commands[12][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_handoff_gate.py", + ] + assert commands[13][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_trigger_gate.py", + ] + assert commands[14][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_trace_gate.py", + ] + assert commands[15][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_completion_gate.py", + ] + assert commands[16][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_terminal_publish_gate.py", + ] + assert commands[17][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_finalization_gate.py", + ] + assert commands[18][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_closure_gate.py", + ] + assert commands[19][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_archive_gate.py", + ] + assert commands[20][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_verdict_gate.py", + ] + assert commands[21][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_incident_gate.py", + ] + assert commands[22][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py", + ] + assert commands[23][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_gate.py", + ] + assert commands[24][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py", + ] + assert commands[25][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py", + ] + assert commands[26][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py", + ] + assert commands[27][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py", + ] + assert commands[28][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py", + ] + assert commands[29][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py", + ] + assert commands[30][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py", + ] + assert commands[31][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py", + ] + assert commands[32][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py", + ] + assert commands[33][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py", + ] + assert commands[34][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py", + ] + assert commands[35][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py", + ] + assert commands[36][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py", + ] + assert commands[37][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py", + ] + assert commands[38][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py", + ] + assert commands[39][1][:2] == [ + "python", + "scripts/run_p2_linux_gate_manifest_drift_gate.py", + ] + assert commands[40][1][:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py", + ] + assert commands[41][1][:2] == [ + "python", + "scripts/run_p2_lv_dispatch_gate.py", + ] + assert commands[42][1][:2] == [ + "python", + "scripts/run_p2_lv_verdict_gate.py", + ] + assert commands[43][1][:2] == [ + "python", + "scripts/run_p2_lv_verdict_publish_gate.py", + ] + assert commands[44][1][:2] == [ + "python", + "scripts/run_p2_lv_terminal_publish_gate.py", + ] + assert commands[45][1][:2] == [ + "python", + "scripts/run_p2_lv_final_verdict_gate.py", + ] + assert commands[46][1][:2] == [ + "python", + "scripts/run_p2_lv_final_verdict_publish_gate.py", + ] + assert commands[47][1][:2] == [ + "python", + "scripts/run_p2_lv_final_publish_archive_gate.py", + ] + assert commands[48][1][:2] == [ + "python", + "scripts/run_p2_lv_terminal_verdict_gate.py", + ] + assert commands[49][1][:2] == [ + "python", + "scripts/run_p2_lv_terminal_verdict_publish_gate.py", + ] + assert commands[50][1][:2] == [ + "python", + "scripts/run_p2_lv_td_dispatch_gate.py", + ] + assert commands[51][1][:2] == [ + "python", + "scripts/run_p2_lv_td_execution_gate.py", + ] + assert commands[52][1][:2] == [ + "python", + "scripts/run_p2_lv_td_trace_gate.py", + ] + assert commands[53][1][:2] == [ + "python", + "scripts/run_p2_lv_td_completion_gate.py", + ] + assert commands[54][1][:2] == [ + "python", + "scripts/run_p2_lv_td_terminal_publish_gate.py", + ] + assert commands[55][1][:2] == [ + "python", + "scripts/run_p2_lv_td_final_verdict_gate.py", + ] + assert commands[56][1][:2] == [ + "python", + "scripts/run_p2_lv_td_final_verdict_publish_gate.py", + ] + assert commands[57][1][:2] == [ + "python", + "scripts/run_p2_lv_td_final_publish_archive_gate.py", + ] + assert commands[58][1][:2] == [ + "python", + "scripts/run_p2_lv_td_terminal_verdict_gate.py", + ] + assert commands[59][1][:2] == [ + "python", + "scripts/run_p2_lv_td_tverdict_publish_gate.py", + ] + assert commands[60][1][:2] == [ + "python", + "scripts/run_p2_lv_td_tverdict_publish_archive_gate.py", + ] + assert commands[61][1][:2] == [ + "python", + "scripts/run_p2_lv_td_tverdict_closure_gate.py", + ] + + +def test_build_ci_workflow_pipeline_commands_propagates_optional_flags(): + commands = _build_commands( + strict_generated_at=True, + sync_write=True, + command_guard_write=True, + dispatch_trace_poll_now=True, + on_block_policy="skip", + workflow_ref="release", + dispatch_timeout_seconds=120, + trace_poll_timeout_seconds=240, + completion_poll_interval_seconds=15, + completion_max_polls=8, + completion_poll_timeout_seconds=180, + completion_allow_in_progress=True, + target_environment="staging", + release_channel="canary", + release_workflow_path=".github/workflows/release-prod.yml", + release_workflow_ref="release", + release_timeout_seconds=1200, + release_trace_poll_timeout_seconds=480, + release_completion_poll_interval_seconds=12, + release_completion_max_polls=7, + release_completion_poll_timeout_seconds=150, + release_command=( + "gh workflow run .github/workflows/release-prod.yml " + "--ref release --raw-field mode=promote" + ), + incident_timeout_seconds=300, + incident_command=( + "gh issue create --repo acme/demo --title " + "\"[release-incident] auto\" --body \"incident\"" + ), + incident_repo="acme/demo", + incident_label="sev-1", + incident_title_prefix="[release-incident][sev1]", + follow_up_timeout_seconds=450, + follow_up_command=( + "gh issue create --repo acme/demo --title " + "\"[release-follow-up] auto\" --body \"follow-up\"" + ), + follow_up_repo="acme/demo", + follow_up_label="release-follow-up-sev1", + follow_up_title_prefix="[release-follow-up][sev1]", + release_trace_poll_now=True, + release_completion_allow_in_progress=True, + on_release_hold_policy="fail", + ) + command_map = {stage: command for stage, command in commands} + + sync_command = command_map["workflow_sync"] + assert "--strict-generated-at" in sync_command + assert "--write" in sync_command + + command_guard_command = command_map["workflow_command_guard"] + assert "--write" in command_guard_command + + governance_command = command_map["workflow_governance"] + assert "--strict-generated-at" in governance_command + + decision_command = command_map["workflow_decision"] + on_block_index = decision_command.index("--on-block") + 1 + assert decision_command[on_block_index] == "skip" + + dispatch_command = command_map["workflow_dispatch"] + workflow_ref_index = dispatch_command.index("--workflow-ref") + 1 + assert dispatch_command[workflow_ref_index] == "release" + + dispatch_execution_command = command_map["workflow_dispatch_execution"] + timeout_index = dispatch_execution_command.index("--dispatch-timeout-seconds") + 1 + assert dispatch_execution_command[timeout_index] == "120" + + dispatch_trace_command = command_map["workflow_dispatch_trace"] + assert "--poll-now" in dispatch_trace_command + trace_timeout_index = dispatch_trace_command.index("--poll-timeout-seconds") + 1 + assert dispatch_trace_command[trace_timeout_index] == "240" + + dispatch_completion_command = command_map["workflow_dispatch_completion"] + interval_index = dispatch_completion_command.index("--poll-interval-seconds") + 1 + assert dispatch_completion_command[interval_index] == "15" + max_polls_index = dispatch_completion_command.index("--max-polls") + 1 + assert dispatch_completion_command[max_polls_index] == "8" + completion_timeout_index = dispatch_completion_command.index("--poll-timeout-seconds") + 1 + assert dispatch_completion_command[completion_timeout_index] == "180" + assert "--allow-in-progress" in dispatch_completion_command + + release_handoff_command = command_map["workflow_release_handoff"] + environment_index = release_handoff_command.index("--target-environment") + 1 + assert release_handoff_command[environment_index] == "staging" + channel_index = release_handoff_command.index("--release-channel") + 1 + assert release_handoff_command[channel_index] == "canary" + + release_trigger_command = command_map["workflow_release_trigger"] + workflow_path_index = release_trigger_command.index("--release-workflow-path") + 1 + assert release_trigger_command[workflow_path_index] == ".github/workflows/release-prod.yml" + workflow_ref_index = release_trigger_command.index("--release-workflow-ref") + 1 + assert release_trigger_command[workflow_ref_index] == "release" + timeout_index = release_trigger_command.index("--release-timeout-seconds") + 1 + assert release_trigger_command[timeout_index] == "1200" + release_command_index = release_trigger_command.index("--release-command") + 1 + assert release_trigger_command[release_command_index].startswith("gh workflow run") + + release_trace_command = command_map["workflow_release_trace"] + trace_timeout_index = release_trace_command.index("--poll-timeout-seconds") + 1 + assert release_trace_command[trace_timeout_index] == "480" + assert "--poll-now" in release_trace_command + + release_completion_command = command_map["workflow_release_completion"] + interval_index = release_completion_command.index("--poll-interval-seconds") + 1 + assert release_completion_command[interval_index] == "12" + max_polls_index = release_completion_command.index("--max-polls") + 1 + assert release_completion_command[max_polls_index] == "7" + timeout_index = release_completion_command.index("--poll-timeout-seconds") + 1 + assert release_completion_command[timeout_index] == "150" + assert "--allow-in-progress" in release_completion_command + + release_finalization_command = command_map["workflow_release_finalization"] + hold_policy_index = release_finalization_command.index("--on-hold") + 1 + assert release_finalization_command[hold_policy_index] == "fail" + + release_incident_command = command_map["workflow_release_incident"] + timeout_index = release_incident_command.index("--incident-timeout-seconds") + 1 + assert release_incident_command[timeout_index] == "300" + repo_index = release_incident_command.index("--incident-repo") + 1 + assert release_incident_command[repo_index] == "acme/demo" + label_index = release_incident_command.index("--incident-label") + 1 + assert release_incident_command[label_index] == "sev-1" + title_prefix_index = release_incident_command.index("--incident-title-prefix") + 1 + assert release_incident_command[title_prefix_index] == "[release-incident][sev1]" + incident_command_index = release_incident_command.index("--incident-command") + 1 + assert release_incident_command[incident_command_index].startswith("gh issue create") + + release_terminal_verdict_command = command_map["workflow_release_terminal_verdict"] + assert release_terminal_verdict_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_terminal_verdict_gate.py", + ] + incident_report_index = ( + release_terminal_verdict_command.index("--release-incident-report") + 1 + ) + assert release_terminal_verdict_command[incident_report_index].endswith( + "ci_workflow_release_incident.json" + ) + release_delivery_command = command_map["workflow_release_delivery"] + assert release_delivery_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_gate.py", + ] + terminal_verdict_report_index = ( + release_delivery_command.index("--release-terminal-verdict-report") + 1 + ) + assert release_delivery_command[terminal_verdict_report_index].endswith( + "ci_workflow_release_terminal_verdict.json" + ) + release_delivery_terminal_publish_command = command_map["workflow_release_delivery_terminal_publish"] + assert release_delivery_terminal_publish_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py", + ] + release_delivery_report_index = ( + release_delivery_terminal_publish_command.index("--release-delivery-report") + 1 + ) + assert release_delivery_terminal_publish_command[release_delivery_report_index].endswith( + "ci_workflow_release_delivery.json" + ) + release_delivery_final_verdict_command = command_map["workflow_release_delivery_final_verdict"] + assert release_delivery_final_verdict_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py", + ] + release_delivery_terminal_publish_report_index = ( + release_delivery_final_verdict_command.index( + "--release-delivery-terminal-publish-report" + ) + + 1 + ) + assert release_delivery_final_verdict_command[ + release_delivery_terminal_publish_report_index + ].endswith("ci_workflow_release_delivery_terminal_publish.json") + release_follow_up_dispatch_command = command_map["workflow_release_follow_up_dispatch"] + assert release_follow_up_dispatch_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py", + ] + release_delivery_final_verdict_report_index = ( + release_follow_up_dispatch_command.index("--release-delivery-final-verdict-report") + 1 + ) + assert release_follow_up_dispatch_command[ + release_delivery_final_verdict_report_index + ].endswith("ci_workflow_release_delivery_final_verdict.json") + release_follow_up_closure_command = command_map["workflow_release_follow_up_closure"] + assert release_follow_up_closure_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_closure_gate.py", + ] + release_follow_up_dispatch_report_index = ( + release_follow_up_closure_command.index("--release-follow-up-dispatch-report") + 1 + ) + assert release_follow_up_closure_command[ + release_follow_up_dispatch_report_index + ].endswith("ci_workflow_release_follow_up_dispatch.json") + follow_up_timeout_index = ( + release_follow_up_closure_command.index("--follow-up-timeout-seconds") + 1 + ) + assert release_follow_up_closure_command[follow_up_timeout_index] == "450" + follow_up_repo_index = release_follow_up_closure_command.index("--follow-up-repo") + 1 + assert release_follow_up_closure_command[follow_up_repo_index] == "acme/demo" + follow_up_label_index = release_follow_up_closure_command.index("--follow-up-label") + 1 + assert release_follow_up_closure_command[follow_up_label_index] == "release-follow-up-sev1" + follow_up_title_prefix_index = ( + release_follow_up_closure_command.index("--follow-up-title-prefix") + 1 + ) + assert ( + release_follow_up_closure_command[follow_up_title_prefix_index] + == "[release-follow-up][sev1]" + ) + follow_up_command_index = ( + release_follow_up_closure_command.index("--follow-up-command") + 1 + ) + assert release_follow_up_closure_command[follow_up_command_index].startswith( + "gh issue create" + ) + release_follow_up_terminal_publish_command = command_map[ + "workflow_release_follow_up_terminal_publish" + ] + assert release_follow_up_terminal_publish_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py", + ] + release_follow_up_closure_report_index = ( + release_follow_up_terminal_publish_command.index("--release-follow-up-closure-report") + 1 + ) + assert release_follow_up_terminal_publish_command[ + release_follow_up_closure_report_index + ].endswith("ci_workflow_release_follow_up_closure.json") + release_follow_up_final_verdict_command = command_map[ + "workflow_release_follow_up_final_verdict" + ] + assert release_follow_up_final_verdict_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py", + ] + release_follow_up_terminal_publish_report_index = ( + release_follow_up_final_verdict_command.index( + "--release-follow-up-terminal-publish-report" + ) + + 1 + ) + assert release_follow_up_final_verdict_command[ + release_follow_up_terminal_publish_report_index + ].endswith("ci_workflow_release_follow_up_terminal_publish.json") + release_final_outcome_command = command_map["workflow_release_final_outcome"] + assert release_final_outcome_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_outcome_gate.py", + ] + release_final_outcome_report_index = ( + release_final_outcome_command.index("--release-delivery-final-verdict-report") + 1 + ) + assert release_final_outcome_command[release_final_outcome_report_index].endswith( + "ci_workflow_release_delivery_final_verdict.json" + ) + release_final_terminal_publish_command = command_map[ + "workflow_release_final_terminal_publish" + ] + assert release_final_terminal_publish_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py", + ] + release_final_terminal_publish_report_index = ( + release_final_terminal_publish_command.index("--release-final-outcome-report") + 1 + ) + assert release_final_terminal_publish_command[ + release_final_terminal_publish_report_index + ].endswith("ci_workflow_release_final_outcome.json") + release_final_handoff_command = command_map["workflow_release_final_handoff"] + assert release_final_handoff_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_handoff_gate.py", + ] + release_final_handoff_report_index = ( + release_final_handoff_command.index("--release-final-terminal-publish-report") + 1 + ) + assert release_final_handoff_command[release_final_handoff_report_index].endswith( + "ci_workflow_release_final_terminal_publish.json" + ) + release_final_closure_command = command_map["workflow_release_final_closure"] + assert release_final_closure_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_closure_gate.py", + ] + release_final_closure_report_index = ( + release_final_closure_command.index("--release-final-handoff-report") + 1 + ) + assert release_final_closure_command[release_final_closure_report_index].endswith( + "ci_workflow_release_final_handoff.json" + ) + release_final_closure_publish_command = command_map["workflow_release_final_closure_publish"] + assert release_final_closure_publish_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_closure_publish_gate.py", + ] + release_final_closure_publish_report_index = ( + release_final_closure_publish_command.index("--release-final-closure-report") + 1 + ) + assert release_final_closure_publish_command[ + release_final_closure_publish_report_index + ].endswith("ci_workflow_release_final_closure.json") + release_final_archive_command = command_map["workflow_release_final_archive"] + assert release_final_archive_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_archive_gate.py", + ] + release_final_archive_report_index = ( + release_final_archive_command.index("--release-final-closure-publish-report") + 1 + ) + assert release_final_archive_command[release_final_archive_report_index].endswith( + "ci_workflow_release_final_closure_publish.json" + ) + release_final_verdict_command = command_map["workflow_release_final_verdict"] + assert release_final_verdict_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_verdict_gate.py", + ] + release_final_verdict_report_index = ( + release_final_verdict_command.index("--release-final-archive-report") + 1 + ) + assert release_final_verdict_command[release_final_verdict_report_index].endswith( + "ci_workflow_release_final_archive.json" + ) + release_final_verdict_publish_command = command_map[ + "workflow_release_final_verdict_publish" + ] + assert release_final_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py", + ] + release_final_verdict_publish_report_index = ( + release_final_verdict_publish_command.index("--release-final-verdict-report") + 1 + ) + assert release_final_verdict_publish_command[ + release_final_verdict_publish_report_index + ].endswith("ci_workflow_release_final_verdict.json") + release_final_publish_archive_command = command_map[ + "workflow_release_final_publish_archive" + ] + assert release_final_publish_archive_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_release_final_publish_archive_gate.py", + ] + release_final_publish_archive_report_index = ( + release_final_publish_archive_command.index("--release-final-verdict-publish-report") + + 1 + ) + assert release_final_publish_archive_command[ + release_final_publish_archive_report_index + ].endswith("ci_workflow_release_final_verdict_publish.json") + gate_manifest_drift_command = command_map["workflow_gate_manifest_drift"] + assert gate_manifest_drift_command[:2] == [ + "python", + "scripts/run_p2_linux_gate_manifest_drift_gate.py", + ] + gate_manifest_drift_json_index = gate_manifest_drift_command.index("--output-json") + 1 + assert gate_manifest_drift_command[gate_manifest_drift_json_index].endswith( + "linux_gate_manifest_drift.json" + ) + gate_manifest_drift_markdown_index = ( + gate_manifest_drift_command.index("--output-markdown") + 1 + ) + assert gate_manifest_drift_command[gate_manifest_drift_markdown_index].endswith( + "linux_gate_manifest_drift.md" + ) + terminal_verdict_command = command_map["workflow_terminal_verdict"] + assert terminal_verdict_command[:2] == [ + "python", + "scripts/run_p2_linux_ci_workflow_terminal_verdict_gate.py", + ] + terminal_verdict_release_report_index = ( + terminal_verdict_command.index("--release-final-publish-archive-report") + 1 + ) + assert terminal_verdict_command[terminal_verdict_release_report_index].endswith( + "ci_workflow_release_final_publish_archive.json" + ) + terminal_verdict_drift_report_index = ( + terminal_verdict_command.index("--gate-manifest-drift-report") + 1 + ) + assert terminal_verdict_command[terminal_verdict_drift_report_index].endswith( + "linux_gate_manifest_drift.json" + ) + terminal_verdict_json_index = terminal_verdict_command.index("--output-json") + 1 + assert terminal_verdict_command[terminal_verdict_json_index].endswith( + "ci_workflow_terminal_verdict.json" + ) + terminal_verdict_markdown_index = terminal_verdict_command.index("--output-markdown") + 1 + assert terminal_verdict_command[terminal_verdict_markdown_index].endswith( + "ci_workflow_terminal_verdict.md" + ) + linux_validation_dispatch_command = command_map["workflow_linux_validation_dispatch"] + assert linux_validation_dispatch_command[:2] == [ + "python", + "scripts/run_p2_lv_dispatch_gate.py", + ] + linux_validation_terminal_verdict_report_index = ( + linux_validation_dispatch_command.index("--terminal-verdict-report") + 1 + ) + assert linux_validation_dispatch_command[ + linux_validation_terminal_verdict_report_index + ].endswith("ci_workflow_terminal_verdict.json") + linux_validation_project_root_index = ( + linux_validation_dispatch_command.index("--project-root") + 1 + ) + assert linux_validation_dispatch_command[linux_validation_project_root_index] == "." + linux_validation_python_index = ( + linux_validation_dispatch_command.index("--python-executable") + 1 + ) + assert linux_validation_dispatch_command[linux_validation_python_index] == "python" + linux_validation_timeout_index = ( + linux_validation_dispatch_command.index("--linux-validation-timeout-seconds") + 1 + ) + assert linux_validation_dispatch_command[linux_validation_timeout_index] == "7200" + linux_validation_json_index = ( + linux_validation_dispatch_command.index("--output-json") + 1 + ) + assert linux_validation_dispatch_command[linux_validation_json_index].endswith( + "ci_workflow_linux_validation_dispatch.json" + ) + linux_validation_markdown_index = ( + linux_validation_dispatch_command.index("--output-markdown") + 1 + ) + assert linux_validation_dispatch_command[linux_validation_markdown_index].endswith( + "ci_workflow_linux_validation_dispatch.md" + ) + linux_validation_verdict_command = command_map["workflow_linux_validation_verdict"] + assert linux_validation_verdict_command[:2] == [ + "python", + "scripts/run_p2_lv_verdict_gate.py", + ] + linux_validation_verdict_dispatch_report_index = ( + linux_validation_verdict_command.index("--linux-validation-dispatch-report") + 1 + ) + assert linux_validation_verdict_command[ + linux_validation_verdict_dispatch_report_index + ].endswith("ci_workflow_linux_validation_dispatch.json") + linux_validation_verdict_json_index = ( + linux_validation_verdict_command.index("--output-json") + 1 + ) + assert linux_validation_verdict_command[linux_validation_verdict_json_index].endswith( + "ci_workflow_linux_validation_verdict.json" + ) + linux_validation_verdict_markdown_index = ( + linux_validation_verdict_command.index("--output-markdown") + 1 + ) + assert linux_validation_verdict_command[ + linux_validation_verdict_markdown_index + ].endswith("ci_workflow_linux_validation_verdict.md") + linux_validation_verdict_publish_command = command_map[ + "workflow_linux_validation_verdict_publish" + ] + assert linux_validation_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_verdict_publish_gate.py", + ] + linux_validation_verdict_publish_report_index = ( + linux_validation_verdict_publish_command.index("--linux-validation-verdict-report") + + 1 + ) + assert linux_validation_verdict_publish_command[ + linux_validation_verdict_publish_report_index + ].endswith("ci_workflow_linux_validation_verdict.json") + linux_validation_verdict_publish_json_index = ( + linux_validation_verdict_publish_command.index("--output-json") + 1 + ) + assert linux_validation_verdict_publish_command[ + linux_validation_verdict_publish_json_index + ].endswith("ci_workflow_linux_validation_verdict_publish.json") + linux_validation_verdict_publish_markdown_index = ( + linux_validation_verdict_publish_command.index("--output-markdown") + 1 + ) + assert linux_validation_verdict_publish_command[ + linux_validation_verdict_publish_markdown_index + ].endswith("ci_workflow_linux_validation_verdict_publish.md") + linux_validation_terminal_publish_command = command_map[ + "workflow_linux_validation_terminal_publish" + ] + assert linux_validation_terminal_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_terminal_publish_gate.py", + ] + linux_validation_terminal_publish_report_index = ( + linux_validation_terminal_publish_command.index( + "--linux-validation-verdict-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_publish_command[ + linux_validation_terminal_publish_report_index + ].endswith("ci_workflow_linux_validation_verdict_publish.json") + linux_validation_terminal_publish_json_index = ( + linux_validation_terminal_publish_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_publish_command[ + linux_validation_terminal_publish_json_index + ].endswith("ci_workflow_linux_validation_terminal_publish.json") + linux_validation_terminal_publish_markdown_index = ( + linux_validation_terminal_publish_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_publish_command[ + linux_validation_terminal_publish_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_publish.md") + linux_validation_final_verdict_command = command_map[ + "workflow_linux_validation_final_verdict" + ] + assert linux_validation_final_verdict_command[:2] == [ + "python", + "scripts/run_p2_lv_final_verdict_gate.py", + ] + linux_validation_final_verdict_report_index = ( + linux_validation_final_verdict_command.index( + "--linux-validation-terminal-publish-report" + ) + + 1 + ) + assert linux_validation_final_verdict_command[ + linux_validation_final_verdict_report_index + ].endswith("ci_workflow_linux_validation_terminal_publish.json") + linux_validation_final_verdict_json_index = ( + linux_validation_final_verdict_command.index("--output-json") + 1 + ) + assert linux_validation_final_verdict_command[ + linux_validation_final_verdict_json_index + ].endswith("ci_workflow_linux_validation_final_verdict.json") + linux_validation_final_verdict_markdown_index = ( + linux_validation_final_verdict_command.index("--output-markdown") + 1 + ) + assert linux_validation_final_verdict_command[ + linux_validation_final_verdict_markdown_index + ].endswith("ci_workflow_linux_validation_final_verdict.md") + linux_validation_final_verdict_publish_command = command_map[ + "workflow_linux_validation_final_verdict_publish" + ] + assert linux_validation_final_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_final_verdict_publish_gate.py", + ] + linux_validation_final_verdict_publish_report_index = ( + linux_validation_final_verdict_publish_command.index( + "--linux-validation-final-verdict-report" + ) + + 1 + ) + assert linux_validation_final_verdict_publish_command[ + linux_validation_final_verdict_publish_report_index + ].endswith("ci_workflow_linux_validation_final_verdict.json") + linux_validation_final_verdict_publish_json_index = ( + linux_validation_final_verdict_publish_command.index("--output-json") + 1 + ) + assert linux_validation_final_verdict_publish_command[ + linux_validation_final_verdict_publish_json_index + ].endswith("ci_workflow_linux_validation_final_verdict_publish.json") + linux_validation_final_verdict_publish_markdown_index = ( + linux_validation_final_verdict_publish_command.index("--output-markdown") + 1 + ) + assert linux_validation_final_verdict_publish_command[ + linux_validation_final_verdict_publish_markdown_index + ].endswith("ci_workflow_linux_validation_final_verdict_publish.md") + linux_validation_final_publish_archive_command = command_map[ + "workflow_linux_validation_final_publish_archive" + ] + assert linux_validation_final_publish_archive_command[:2] == [ + "python", + "scripts/run_p2_lv_final_publish_archive_gate.py", + ] + linux_validation_final_publish_archive_report_index = ( + linux_validation_final_publish_archive_command.index( + "--linux-validation-final-verdict-publish-report" + ) + + 1 + ) + assert linux_validation_final_publish_archive_command[ + linux_validation_final_publish_archive_report_index + ].endswith("ci_workflow_linux_validation_final_verdict_publish.json") + linux_validation_final_publish_archive_json_index = ( + linux_validation_final_publish_archive_command.index("--output-json") + 1 + ) + assert linux_validation_final_publish_archive_command[ + linux_validation_final_publish_archive_json_index + ].endswith("ci_workflow_linux_validation_final_publish_archive.json") + linux_validation_final_publish_archive_markdown_index = ( + linux_validation_final_publish_archive_command.index("--output-markdown") + 1 + ) + assert linux_validation_final_publish_archive_command[ + linux_validation_final_publish_archive_markdown_index + ].endswith("ci_workflow_linux_validation_final_publish_archive.md") + linux_validation_terminal_verdict_command = command_map[ + "workflow_linux_validation_terminal_verdict" + ] + assert linux_validation_terminal_verdict_command[:2] == [ + "python", + "scripts/run_p2_lv_terminal_verdict_gate.py", + ] + linux_validation_terminal_verdict_report_index = ( + linux_validation_terminal_verdict_command.index( + "--linux-validation-final-publish-archive-report" + ) + + 1 + ) + assert linux_validation_terminal_verdict_command[ + linux_validation_terminal_verdict_report_index + ].endswith("ci_workflow_linux_validation_final_publish_archive.json") + linux_validation_terminal_verdict_json_index = ( + linux_validation_terminal_verdict_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_verdict_command[ + linux_validation_terminal_verdict_json_index + ].endswith("ci_workflow_linux_validation_terminal_verdict.json") + linux_validation_terminal_verdict_markdown_index = ( + linux_validation_terminal_verdict_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_verdict_command[ + linux_validation_terminal_verdict_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_verdict.md") + linux_validation_terminal_verdict_publish_command = command_map[ + "workflow_linux_validation_terminal_verdict_publish" + ] + assert linux_validation_terminal_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_terminal_verdict_publish_gate.py", + ] + linux_validation_terminal_verdict_publish_report_index = ( + linux_validation_terminal_verdict_publish_command.index( + "--linux-validation-terminal-verdict-report" + ) + + 1 + ) + assert linux_validation_terminal_verdict_publish_command[ + linux_validation_terminal_verdict_publish_report_index + ].endswith("ci_workflow_linux_validation_terminal_verdict.json") + linux_validation_terminal_verdict_publish_json_index = ( + linux_validation_terminal_verdict_publish_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_verdict_publish_command[ + linux_validation_terminal_verdict_publish_json_index + ].endswith("ci_workflow_linux_validation_terminal_verdict_publish.json") + linux_validation_terminal_verdict_publish_markdown_index = ( + linux_validation_terminal_verdict_publish_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_verdict_publish_command[ + linux_validation_terminal_verdict_publish_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_verdict_publish.md") + linux_validation_terminal_dispatch_command = command_map[ + "workflow_linux_validation_terminal_dispatch" + ] + assert linux_validation_terminal_dispatch_command[:2] == [ + "python", + "scripts/run_p2_lv_td_dispatch_gate.py", + ] + linux_validation_terminal_dispatch_report_index = ( + linux_validation_terminal_dispatch_command.index( + "--linux-validation-terminal-verdict-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_command[ + linux_validation_terminal_dispatch_report_index + ].endswith("ci_workflow_linux_validation_terminal_verdict_publish.json") + linux_validation_terminal_dispatch_json_index = ( + linux_validation_terminal_dispatch_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_command[ + linux_validation_terminal_dispatch_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch.json") + linux_validation_terminal_dispatch_markdown_index = ( + linux_validation_terminal_dispatch_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_dispatch_command[ + linux_validation_terminal_dispatch_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch.md") + linux_validation_terminal_dispatch_execution_command = command_map[ + "workflow_linux_validation_terminal_dispatch_execution" + ] + assert linux_validation_terminal_dispatch_execution_command[:2] == [ + "python", + "scripts/run_p2_lv_td_execution_gate.py", + ] + linux_validation_terminal_dispatch_execution_report_index = ( + linux_validation_terminal_dispatch_execution_command.index( + "--linux-validation-terminal-dispatch-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_execution_command[ + linux_validation_terminal_dispatch_execution_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch.json") + linux_validation_terminal_dispatch_execution_json_index = ( + linux_validation_terminal_dispatch_execution_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_execution_command[ + linux_validation_terminal_dispatch_execution_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_execution.json") + linux_validation_terminal_dispatch_execution_markdown_index = ( + linux_validation_terminal_dispatch_execution_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_dispatch_execution_command[ + linux_validation_terminal_dispatch_execution_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_execution.md") + linux_validation_terminal_dispatch_trace_command = command_map[ + "workflow_linux_validation_terminal_dispatch_trace" + ] + assert linux_validation_terminal_dispatch_trace_command[:2] == [ + "python", + "scripts/run_p2_lv_td_trace_gate.py", + ] + linux_validation_terminal_dispatch_trace_report_index = ( + linux_validation_terminal_dispatch_trace_command.index( + "--linux-validation-terminal-dispatch-execution-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_trace_command[ + linux_validation_terminal_dispatch_trace_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_execution.json") + linux_validation_terminal_dispatch_trace_timeout_index = ( + linux_validation_terminal_dispatch_trace_command.index("--poll-timeout-seconds") + 1 + ) + assert linux_validation_terminal_dispatch_trace_command[ + linux_validation_terminal_dispatch_trace_timeout_index + ] == "240" + linux_validation_terminal_dispatch_trace_json_index = ( + linux_validation_terminal_dispatch_trace_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_trace_command[ + linux_validation_terminal_dispatch_trace_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_trace.json") + linux_validation_terminal_dispatch_trace_markdown_index = ( + linux_validation_terminal_dispatch_trace_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_dispatch_trace_command[ + linux_validation_terminal_dispatch_trace_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_trace.md") + assert "--poll-now" in linux_validation_terminal_dispatch_trace_command + + linux_validation_terminal_dispatch_completion_command = command_map[ + "workflow_linux_validation_terminal_dispatch_completion" + ] + assert linux_validation_terminal_dispatch_completion_command[:2] == [ + "python", + "scripts/run_p2_lv_td_completion_gate.py", + ] + linux_validation_terminal_dispatch_completion_report_index = ( + linux_validation_terminal_dispatch_completion_command.index( + "--linux-validation-terminal-dispatch-trace-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_trace.json") + linux_validation_terminal_dispatch_completion_interval_index = ( + linux_validation_terminal_dispatch_completion_command.index("--poll-interval-seconds") + + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_interval_index + ] == "15" + linux_validation_terminal_dispatch_completion_max_polls_index = ( + linux_validation_terminal_dispatch_completion_command.index("--max-polls") + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_max_polls_index + ] == "8" + linux_validation_terminal_dispatch_completion_timeout_index = ( + linux_validation_terminal_dispatch_completion_command.index("--poll-timeout-seconds") + + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_timeout_index + ] == "180" + linux_validation_terminal_dispatch_completion_json_index = ( + linux_validation_terminal_dispatch_completion_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_completion.json") + linux_validation_terminal_dispatch_completion_markdown_index = ( + linux_validation_terminal_dispatch_completion_command.index("--output-markdown") + 1 + ) + assert linux_validation_terminal_dispatch_completion_command[ + linux_validation_terminal_dispatch_completion_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_completion.md") + assert "--allow-in-progress" in linux_validation_terminal_dispatch_completion_command + + linux_validation_terminal_dispatch_terminal_publish_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_publish" + ] + assert linux_validation_terminal_dispatch_terminal_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_td_terminal_publish_gate.py", + ] + linux_validation_terminal_dispatch_terminal_publish_report_index = ( + linux_validation_terminal_dispatch_terminal_publish_command.index( + "--linux-validation-terminal-dispatch-completion-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_publish_command[ + linux_validation_terminal_dispatch_terminal_publish_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_completion.json") + linux_validation_terminal_dispatch_terminal_publish_json_index = ( + linux_validation_terminal_dispatch_terminal_publish_command.index("--output-json") + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_publish_command[ + linux_validation_terminal_dispatch_terminal_publish_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json") + linux_validation_terminal_dispatch_terminal_publish_markdown_index = ( + linux_validation_terminal_dispatch_terminal_publish_command.index("--output-markdown") + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_publish_command[ + linux_validation_terminal_dispatch_terminal_publish_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md") + + linux_validation_terminal_dispatch_final_verdict_command = command_map[ + "workflow_linux_validation_terminal_dispatch_final_verdict" + ] + assert linux_validation_terminal_dispatch_final_verdict_command[:2] == [ + "python", + "scripts/run_p2_lv_td_final_verdict_gate.py", + ] + linux_validation_terminal_dispatch_final_verdict_report_index = ( + linux_validation_terminal_dispatch_final_verdict_command.index( + "--linux-validation-terminal-dispatch-terminal-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_command[ + linux_validation_terminal_dispatch_final_verdict_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json") + linux_validation_terminal_dispatch_final_verdict_json_index = ( + linux_validation_terminal_dispatch_final_verdict_command.index("--output-json") + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_command[ + linux_validation_terminal_dispatch_final_verdict_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict.json") + linux_validation_terminal_dispatch_final_verdict_markdown_index = ( + linux_validation_terminal_dispatch_final_verdict_command.index("--output-markdown") + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_command[ + linux_validation_terminal_dispatch_final_verdict_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict.md") + + linux_validation_terminal_dispatch_final_verdict_publish_command = command_map[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish" + ] + assert linux_validation_terminal_dispatch_final_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_td_final_verdict_publish_gate.py", + ] + linux_validation_terminal_dispatch_final_verdict_publish_report_index = ( + linux_validation_terminal_dispatch_final_verdict_publish_command.index( + "--linux-validation-terminal-dispatch-final-verdict-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_publish_command[ + linux_validation_terminal_dispatch_final_verdict_publish_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict.json") + linux_validation_terminal_dispatch_final_verdict_publish_json_index = ( + linux_validation_terminal_dispatch_final_verdict_publish_command.index("--output-json") + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_publish_command[ + linux_validation_terminal_dispatch_final_verdict_publish_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json") + linux_validation_terminal_dispatch_final_verdict_publish_markdown_index = ( + linux_validation_terminal_dispatch_final_verdict_publish_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_final_verdict_publish_command[ + linux_validation_terminal_dispatch_final_verdict_publish_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md") + + linux_validation_terminal_dispatch_final_publish_archive_command = command_map[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive" + ] + assert linux_validation_terminal_dispatch_final_publish_archive_command[:2] == [ + "python", + "scripts/run_p2_lv_td_final_publish_archive_gate.py", + ] + linux_validation_terminal_dispatch_final_publish_archive_report_index = ( + linux_validation_terminal_dispatch_final_publish_archive_command.index( + "--linux-validation-terminal-dispatch-final-verdict-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_final_publish_archive_command[ + linux_validation_terminal_dispatch_final_publish_archive_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json") + linux_validation_terminal_dispatch_final_publish_archive_json_index = ( + linux_validation_terminal_dispatch_final_publish_archive_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_final_publish_archive_command[ + linux_validation_terminal_dispatch_final_publish_archive_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json") + linux_validation_terminal_dispatch_final_publish_archive_markdown_index = ( + linux_validation_terminal_dispatch_final_publish_archive_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_final_publish_archive_command[ + linux_validation_terminal_dispatch_final_publish_archive_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md") + + linux_validation_terminal_dispatch_terminal_verdict_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict" + ] + assert linux_validation_terminal_dispatch_terminal_verdict_command[:2] == [ + "python", + "scripts/run_p2_lv_td_terminal_verdict_gate.py", + ] + linux_validation_terminal_dispatch_terminal_verdict_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_command.index( + "--linux-validation-terminal-dispatch-final-publish-archive-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_command[ + linux_validation_terminal_dispatch_terminal_verdict_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json") + linux_validation_terminal_dispatch_terminal_verdict_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_command.index("--output-json") + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_command[ + linux_validation_terminal_dispatch_terminal_verdict_json_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json") + linux_validation_terminal_dispatch_terminal_verdict_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_command[ + linux_validation_terminal_dispatch_terminal_verdict_markdown_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md") + linux_validation_terminal_dispatch_terminal_verdict_publish_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish" + ] + assert linux_validation_terminal_dispatch_terminal_verdict_publish_command[:2] == [ + "python", + "scripts/run_p2_lv_td_tverdict_publish_gate.py", + ] + linux_validation_terminal_dispatch_terminal_verdict_publish_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_command.index( + "--linux-validation-terminal-dispatch-terminal-verdict-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_report_index + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json") + linux_validation_terminal_dispatch_terminal_verdict_publish_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_command.index("--output-json") + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_json_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_markdown_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md" + ) + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" + ] + assert ( + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command[:2] + == [ + "python", + "scripts/run_p2_lv_td_tverdict_publish_archive_gate.py", + ] + ) + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command.index( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command.index( + "--output-json" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_json_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_publish_archive_markdown_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure" + ] + assert linux_validation_terminal_dispatch_terminal_verdict_closure_command[:2] == [ + "python", + "scripts/run_p2_lv_td_tverdict_closure_gate.py", + ] + linux_validation_terminal_dispatch_terminal_verdict_closure_archive_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_command.index( + "--linux-validation-terminal-dispatch-terminal-verdict-publish-archive-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_archive_report_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_drift_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_command.index( + "--gate-manifest-drift-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_drift_report_index + ].endswith("linux_gate_manifest_drift.json") + linux_validation_terminal_dispatch_terminal_verdict_closure_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_command.index("--output-json") + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_json_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_markdown_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + ] + assert ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[:2] + == [ + "python", + "scripts/run_p2_lv_td_tverdict_closure_publish_gate.py", + ] + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command.index( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command.index( + "--output-json" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_json_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_markdown_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command = command_map[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ] + assert ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[:2] + == [ + "python", + "scripts/run_p2_lv_td_tverdict_closure_publish_archive_gate.py", + ] + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + "--linux-validation-terminal-dispatch-terminal-verdict-closure-publish-report" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + "--output-json" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_json_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + ) + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index = ( + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command.index( + "--output-markdown" + ) + + 1 + ) + assert linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_command[ + linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_markdown_index + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + ) + + +def test_build_ci_workflow_pipeline_commands_supports_partial_skip(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_governance_publish", + "workflow_decision", + "workflow_dispatch", + "workflow_dispatch_execution", + "workflow_dispatch_trace", + "workflow_dispatch_completion", + "workflow_terminal_publish", + "workflow_release_handoff", + "workflow_release_trigger", + "workflow_release_trace", + "workflow_release_completion", + "workflow_release_terminal_publish", + "workflow_release_finalization", + "workflow_release_closure", + "workflow_release_archive", + "workflow_release_verdict", + "workflow_release_incident", + "workflow_release_terminal_verdict", + "workflow_release_delivery", + "workflow_release_delivery_terminal_publish", + "workflow_release_delivery_final_verdict", + "workflow_release_follow_up_dispatch", + "workflow_release_follow_up_closure", + "workflow_release_follow_up_terminal_publish", + "workflow_release_follow_up_final_verdict", + "workflow_release_final_outcome", + "workflow_release_final_terminal_publish", + "workflow_release_final_handoff", + "workflow_release_final_closure", + "workflow_release_final_closure_publish", + "workflow_release_final_archive", + "workflow_release_final_verdict", + "workflow_release_final_verdict_publish", + "workflow_release_final_publish_archive", + "workflow_gate_manifest_drift", + "workflow_terminal_verdict", + "workflow_linux_validation_dispatch", + "workflow_linux_validation_verdict", + "workflow_linux_validation_verdict_publish", + "workflow_linux_validation_terminal_publish", + "workflow_linux_validation_final_verdict", + "workflow_linux_validation_final_verdict_publish", + "workflow_linux_validation_final_publish_archive", + "workflow_linux_validation_terminal_verdict", + "workflow_linux_validation_terminal_verdict_publish", + "workflow_linux_validation_terminal_dispatch", + "workflow_linux_validation_terminal_dispatch_execution", + "workflow_linux_validation_terminal_dispatch_trace", + "workflow_linux_validation_terminal_dispatch_completion", + "workflow_linux_validation_terminal_dispatch_terminal_publish", + "workflow_linux_validation_terminal_dispatch_final_verdict", + "workflow_linux_validation_terminal_dispatch_final_verdict_publish", + "workflow_linux_validation_terminal_dispatch_final_publish_archive", + "workflow_linux_validation_terminal_dispatch_terminal_verdict", + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish", + ] + + +def test_build_ci_workflow_pipeline_commands_skip_all_returns_empty_plan(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + ) + assert commands == [] + + +def test_build_ci_workflow_pipeline_commands_supports_completion_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=False, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_dispatch_completion"] + + +def test_build_ci_workflow_pipeline_commands_supports_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=False, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_terminal_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_handoff_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=False, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_handoff"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_trigger_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=False, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_trigger"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_trace_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=False, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_trace"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_completion_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=False, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_completion"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=False, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_terminal_publish"] + + + + +def test_build_ci_workflow_pipeline_commands_supports_release_finalization_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=False, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_finalization"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_closure_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=False, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_closure"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=False, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_archive"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=False, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_incident_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=False, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_incident"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_terminal_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=False, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_terminal_verdict"] + + + +def test_build_ci_workflow_pipeline_commands_supports_release_delivery_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=False, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_delivery"] + + + + + +def test_build_ci_workflow_pipeline_commands_supports_release_delivery_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=False, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_delivery_terminal_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_delivery_final_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=False, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_delivery_final_verdict"] + + + +def test_build_ci_workflow_pipeline_commands_supports_release_follow_up_dispatch_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=False, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_follow_up_dispatch"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_follow_up_closure_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=False, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_follow_up_closure"] + + + + + + +def test_build_ci_workflow_pipeline_commands_supports_release_follow_up_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=False, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_follow_up_terminal_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_follow_up_final_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=False, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_follow_up_final_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_outcome_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=False, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_outcome"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=False, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_terminal_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_handoff_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=False, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_handoff"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_closure_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=False, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_closure"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_closure_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=False, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_closure_publish"] + + + + + + + + + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=False, + skip_release_final_verdict=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_archive"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=False, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=False, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_verdict_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_release_final_publish_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=False, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_release_final_publish_archive"] + + +def test_build_ci_workflow_pipeline_commands_supports_gate_manifest_drift_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=False, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + ) + assert [stage for stage, _ in commands] == ["workflow_gate_manifest_drift"] + + +def test_build_ci_workflow_pipeline_commands_supports_terminal_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=False, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + ) + assert [stage for stage, _ in commands] == ["workflow_terminal_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_dispatch_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=False, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == ["workflow_linux_validation_dispatch"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=False, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == ["workflow_linux_validation_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=False, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == ["workflow_linux_validation_verdict_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=False, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == ["workflow_linux_validation_terminal_publish"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_final_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=False, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == ["workflow_linux_validation_final_verdict"] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_final_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=False, + skip_linux_validation_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_final_verdict_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_final_publish_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=False, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_final_publish_archive" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=False, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_verdict" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=False, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_verdict_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=False, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_execution_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=False, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_execution" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_trace_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=False, + skip_linux_validation_terminal_dispatch_completion=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_trace" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_completion_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=False, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_completion" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=False, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_final_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=False, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_final_verdict" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_final_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=False, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_final_publish_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=False, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_final_publish_archive" + ] + + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=False, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=False, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=False, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=False, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=False, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=True, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + ] + + +def test_build_ci_workflow_pipeline_commands_supports_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_only_stage(): + commands = _build_commands( + skip_plan=True, + skip_yaml=True, + skip_sync=True, + skip_command_guard=True, + skip_governance=True, + skip_governance_publish=True, + skip_decision=True, + skip_dispatch=True, + skip_dispatch_execution=True, + skip_dispatch_trace=True, + skip_dispatch_completion=True, + skip_dispatch_terminal_publish=True, + skip_dispatch_release_handoff=True, + skip_release_trigger=True, + skip_release_trace=True, + skip_release_completion=True, + skip_release_terminal_publish=True, + skip_release_finalization=True, + skip_release_closure=True, + skip_release_archive=True, + skip_release_verdict=True, + skip_release_incident=True, + skip_release_terminal_verdict=True, + skip_release_delivery=True, + skip_release_delivery_terminal_publish=True, + skip_release_delivery_final_verdict=True, + skip_release_follow_up_dispatch=True, + skip_release_follow_up_closure=True, + skip_release_follow_up_terminal_publish=True, + skip_release_follow_up_final_verdict=True, + skip_release_final_outcome=True, + skip_release_final_terminal_publish=True, + skip_release_final_handoff=True, + skip_release_final_closure=True, + skip_release_final_closure_publish=True, + skip_release_final_archive=True, + skip_release_final_verdict=True, + skip_release_final_verdict_publish=True, + skip_release_final_publish_archive=True, + skip_gate_manifest_drift=True, + skip_terminal_verdict=True, + skip_linux_validation_dispatch=True, + skip_linux_validation_verdict=True, + skip_linux_validation_verdict_publish=True, + skip_linux_validation_terminal_publish=True, + skip_linux_validation_final_verdict=True, + skip_linux_validation_final_verdict_publish=True, + skip_linux_validation_final_publish_archive=True, + skip_linux_validation_terminal_verdict=True, + skip_linux_validation_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch=True, + skip_linux_validation_terminal_dispatch_execution=True, + skip_linux_validation_terminal_dispatch_trace=True, + skip_linux_validation_terminal_dispatch_completion=True, + skip_linux_validation_terminal_dispatch_terminal_publish=True, + skip_linux_validation_terminal_dispatch_final_verdict=True, + skip_linux_validation_terminal_dispatch_final_verdict_publish=True, + skip_linux_validation_terminal_dispatch_final_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_publish_archive=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish=True, + skip_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive=False, + ) + assert [stage for stage, _ in commands] == [ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ] + diff --git a/tests/test_p2_linux_ci_workflow_plan_gate_runtime.py b/tests/test_p2_linux_ci_workflow_plan_gate_runtime.py new file mode 100644 index 0000000..3268687 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_plan_gate_runtime.py @@ -0,0 +1,161 @@ +"""Contract tests for P2-17 Linux CI workflow plan gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_plan_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_plan_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_ci_workflow_plan_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_ci_matrix() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_shard_plan": "/tmp/shard_plan.json", + "shard_total": 3, + "manifest_total_tests": 5, + "selected_shards": 3, + "skipped_empty_shards": False, + "matrix": { + "include": [ + { + "shard_index": 1, + "shard_total": 3, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 1", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "1", + ], + }, + { + "shard_index": 2, + "shard_total": 3, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "2", + ], + }, + { + "shard_index": 3, + "shard_total": 3, + "total_tests": 1, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-3", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-3/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 3", + "command_parts": [ + "python", + "scripts/run_p2_linux_unified_execution_gate.py", + "--shard-total", + "3", + "--shard-index", + "3", + ], + }, + ] + }, + "summary_paths": [ + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-3/summary.json", + ], + } + + +def test_build_ci_workflow_plan_payload_fan_out_and_fan_in_contract(): + gate = _load_ci_workflow_plan_gate_module() + payload = gate.build_ci_workflow_plan_payload( + _sample_ci_matrix(), + source_path=Path("reports/ci_matrix.json"), + python_executable="python3", + merged_summary_output=Path("reports/merged_summary.json"), + final_report_json=Path("reports/final_report.json"), + final_report_markdown=Path("reports/final_report.md"), + ) + + assert payload["selected_shards"] == 3 + assert len(payload["fan_out_jobs"]) == 3 + assert payload["fan_out_jobs"][0]["job_id"] == "linux-shard-1" + assert payload["fan_out_jobs"][2]["summary_path"].endswith("shard-3/summary.json") + + aggregation_parts = payload["fan_in"]["aggregation_command_parts"] + assert aggregation_parts[:2] == ["python3", "scripts/run_p2_linux_shard_aggregation_gate.py"] + assert "--summary" in aggregation_parts + publish_parts = payload["fan_in"]["publish_command_parts"] + assert publish_parts[:2] == ["python3", "scripts/run_p2_linux_report_publish_gate.py"] + assert "--merged-summary" in publish_parts + + +def test_build_github_output_values_contract(): + gate = _load_ci_workflow_plan_gate_module() + payload = gate.build_ci_workflow_plan_payload( + _sample_ci_matrix(), + source_path=Path("reports/ci_matrix.json"), + python_executable="python", + merged_summary_output=Path("reports/merged_summary.json"), + final_report_json=Path("reports/final_report.json"), + final_report_markdown=Path("reports/final_report.md"), + ) + + outputs = gate.build_github_output_values(payload) + assert "fan_out_matrix" in outputs + assert "fan_in_summary_paths" in outputs + assert "aggregation_command" in outputs + assert "publish_command" in outputs + assert outputs["selected_shards"] == "3" + assert outputs["manifest_total_tests"] == "5" + + +def test_load_ci_matrix_rejects_summary_order_mismatch(tmp_path: Path): + gate = _load_ci_workflow_plan_gate_module() + payload = _sample_ci_matrix() + payload["summary_paths"] = list(reversed(payload["summary_paths"])) + matrix_path = tmp_path / "ci_matrix_bad.json" + matrix_path.write_text(json.dumps(payload), encoding="utf-8") + + try: + gate.load_ci_matrix(matrix_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_load_ci_matrix_rejects_selected_shards_mismatch(tmp_path: Path): + gate = _load_ci_workflow_plan_gate_module() + payload = _sample_ci_matrix() + payload["selected_shards"] = 2 + matrix_path = tmp_path / "ci_matrix_bad_selected.json" + matrix_path.write_text(json.dumps(payload), encoding="utf-8") + + try: + gate.load_ci_matrix(matrix_path) + raised = False + except ValueError: + raised = True + + assert raised diff --git a/tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py new file mode 100644 index 0000000..2f05995 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_archive_gate_runtime.py @@ -0,0 +1,193 @@ +"""Contract tests for P2-37 Linux CI workflow release archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_archive_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_archive_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_closure_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_finalization_report": "/tmp/ci_workflow_release_finalization.json", + "source_release_terminal_publish_report": "/tmp/ci_workflow_release_terminal_publish.json", + "source_release_completion_report": "/tmp/ci_workflow_release_completion.json", + "source_release_trace_report": "/tmp/ci_workflow_release_trace.json", + "source_release_trigger_report": "/tmp/ci_workflow_release_trigger.json", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "release_closure_status": "closed", + "release_closure_decision": "ship", + "release_closure_exit_code": 0, + "should_close_release": True, + "should_notify": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "reason_codes": ["release_closed"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def _materialize_evidence_paths(base_dir: Path, payload: dict) -> None: + closure_file = base_dir / "ci_workflow_release_closure.json" + closure_file.write_text(json.dumps(payload, indent=2), encoding="utf-8") + for field in ( + "source_release_finalization_report", + "source_release_terminal_publish_report", + "source_release_completion_report", + "source_release_trace_report", + "source_release_trigger_report", + "source_release_handoff_report", + "source_terminal_publish_report", + "source_dispatch_completion_report", + "source_dispatch_trace_report", + "source_dispatch_execution_report", + "source_dispatch_report", + ): + evidence_path = base_dir / Path(payload[field]).name + evidence_path.write_text("{}", encoding="utf-8") + payload[field] = str(evidence_path) + + +def test_build_release_archive_payload_marks_ready_when_evidence_complete(tmp_path: Path): + gate = _load_ci_workflow_release_archive_gate_module() + payload = _sample_release_closure_payload() + _materialize_evidence_paths(tmp_path, payload) + report_path = tmp_path / "ci_workflow_release_closure.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_closure_report(report_path) + archive_payload = gate.build_release_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert archive_payload["release_archive_status"] == "ready" + assert archive_payload["release_archive_decision"] == "publish" + assert archive_payload["should_publish_archive"] is True + assert archive_payload["release_archive_exit_code"] == 0 + assert archive_payload["reason_codes"] == ["release_archive_ready"] + assert archive_payload["missing_artifacts"] == [] + + +def test_build_release_archive_payload_pending_hold_path(tmp_path: Path): + gate = _load_ci_workflow_release_archive_gate_module() + payload = _sample_release_closure_payload() + payload["release_closure_status"] = "pending" + payload["release_closure_decision"] = "hold" + payload["release_closure_exit_code"] = 1 + payload["should_close_release"] = False + payload["reason_codes"] = ["release_run_await_timeout"] + _materialize_evidence_paths(tmp_path, payload) + report_path = tmp_path / "ci_workflow_release_closure_pending.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_closure_report(report_path) + archive_payload = gate.build_release_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert archive_payload["release_archive_status"] == "pending" + assert archive_payload["release_archive_decision"] == "hold" + assert archive_payload["should_publish_archive"] is False + assert archive_payload["release_archive_exit_code"] == 1 + assert "release_run_await_timeout" in archive_payload["reason_codes"] + + +def test_build_release_archive_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_archive_gate_module() + payload = _sample_release_closure_payload() + payload["release_closure_status"] = "closed" + payload["release_closure_decision"] = "hold" + _materialize_evidence_paths(tmp_path, payload) + report_path = tmp_path / "ci_workflow_release_closure_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_closure_report(report_path) + archive_payload = gate.build_release_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert archive_payload["release_archive_status"] == "contract_failed" + assert archive_payload["release_archive_decision"] == "block" + assert archive_payload["release_archive_exit_code"] == 1 + assert "release_closure_decision_mismatch_closed" in archive_payload["structural_issues"] + + +def test_build_release_archive_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_archive_gate_module() + payload = _sample_release_closure_payload() + _materialize_evidence_paths(tmp_path, payload) + missing_path = tmp_path / "ci_workflow_release_trace.json" + if missing_path.exists(): + missing_path.unlink() + payload["source_release_trace_report"] = str(missing_path) + report_path = tmp_path / "ci_workflow_release_closure_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_closure_report(report_path) + archive_payload = gate.build_release_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert archive_payload["release_archive_status"] == "contract_failed" + assert archive_payload["release_archive_decision"] == "block" + assert archive_payload["release_archive_exit_code"] == 1 + assert str(missing_path) in archive_payload["missing_artifacts"] + assert "release_evidence_missing" in archive_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_archive_gate_module() + payload = _sample_release_closure_payload() + _materialize_evidence_paths(tmp_path, payload) + report_path = tmp_path / "ci_workflow_release_closure.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_closure_report(report_path) + archive_payload = gate.build_release_archive_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_archive.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_archive.md"), + ) + + assert outputs["workflow_release_archive_status"] == "ready" + assert outputs["workflow_release_archive_decision"] == "publish" + assert outputs["workflow_release_archive_should_publish"] == "true" + assert outputs["workflow_release_archive_exit_code"] == "0" + assert outputs["workflow_release_archive_run_id"] == "2233445566" + assert outputs["workflow_release_archive_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_archive_report_json"].endswith( + "ci_workflow_release_archive.json" + ) + assert outputs["workflow_release_archive_report_markdown"].endswith( + "ci_workflow_release_archive.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py new file mode 100644 index 0000000..f9c5027 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_closure_gate_runtime.py @@ -0,0 +1,170 @@ +"""Contract tests for P2-36 Linux CI workflow release closure gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_closure_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_closure_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_closure_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_finalization_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_terminal_publish_report": "/tmp/ci_workflow_release_terminal_publish.json", + "source_release_completion_report": "/tmp/ci_workflow_release_completion.json", + "source_release_trace_report": "/tmp/ci_workflow_release_trace.json", + "source_release_trigger_report": "/tmp/ci_workflow_release_trigger.json", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "release_trigger_status": "triggered", + "release_tracking_status": "release_run_completed_success", + "release_completion_status": "release_run_completed_success", + "release_completion_exit_code": 0, + "release_publish_status": "passed", + "release_publish_exit_code": 0, + "release_finalization_status": "finalized", + "release_finalization_decision": "finalize", + "release_finalization_exit_code": 0, + "should_finalize_release": True, + "on_hold_policy": "pass", + "allow_in_progress": False, + "should_poll_release_run": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "reason_codes": ["release_finalized"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_closure_payload_marks_closed(tmp_path: Path): + gate = _load_ci_workflow_release_closure_gate_module() + payload = _sample_release_finalization_payload() + report_path = tmp_path / "ci_workflow_release_finalization.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_finalization_report(report_path) + closure_payload = gate.build_release_closure_payload( + report, + source_path=report_path.resolve(), + ) + + assert closure_payload["release_closure_status"] == "closed" + assert closure_payload["release_closure_decision"] == "ship" + assert closure_payload["should_close_release"] is True + assert closure_payload["release_closure_exit_code"] == 0 + assert closure_payload["reason_codes"] == ["release_closed"] + + +def test_build_release_closure_payload_pending_hold_path(tmp_path: Path): + gate = _load_ci_workflow_release_closure_gate_module() + payload = _sample_release_finalization_payload() + payload["release_tracking_status"] = "release_run_in_progress" + payload["release_completion_status"] = "release_run_await_timeout" + payload["release_publish_status"] = "in_progress" + payload["release_publish_exit_code"] = 0 + payload["release_finalization_status"] = "awaiting_release" + payload["release_finalization_decision"] = "hold" + payload["release_finalization_exit_code"] = 1 + payload["should_finalize_release"] = False + payload["on_hold_policy"] = "fail" + payload["reason_codes"] = ["release_run_await_timeout", "on_hold_policy_fail"] + + report_path = tmp_path / "ci_workflow_release_finalization_hold.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_finalization_report(report_path) + closure_payload = gate.build_release_closure_payload( + report, + source_path=report_path.resolve(), + ) + + assert closure_payload["release_closure_status"] == "pending" + assert closure_payload["release_closure_decision"] == "hold" + assert closure_payload["should_close_release"] is False + assert closure_payload["release_closure_exit_code"] == 1 + assert "on_hold_policy_fail" in closure_payload["reason_codes"] + + +def test_build_release_closure_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_closure_gate_module() + payload = _sample_release_finalization_payload() + payload["release_finalization_status"] = "finalized" + payload["release_finalization_decision"] = "hold" + payload["release_finalization_exit_code"] = 0 + payload["should_finalize_release"] = True + + report_path = tmp_path / "ci_workflow_release_finalization_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_finalization_report(report_path) + closure_payload = gate.build_release_closure_payload( + report, + source_path=report_path.resolve(), + ) + + assert closure_payload["release_closure_status"] == "contract_failed" + assert closure_payload["release_closure_decision"] == "rollback" + assert closure_payload["release_closure_exit_code"] == 1 + assert "release_finalization_decision_mismatch_finalized" in closure_payload[ + "structural_issues" + ] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_closure_gate_module() + payload = _sample_release_finalization_payload() + payload["release_publish_status"] = "failed" + payload["release_publish_exit_code"] = 1 + payload["release_finalization_status"] = "failed" + payload["release_finalization_decision"] = "abort" + payload["release_finalization_exit_code"] = 1 + payload["should_finalize_release"] = False + payload["reason_codes"] = ["release_run_completed_with_failure"] + + report_path = tmp_path / "ci_workflow_release_finalization_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_finalization_report(report_path) + closure_payload = gate.build_release_closure_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + closure_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_closure.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_closure.md"), + ) + + assert outputs["workflow_release_closure_status"] == "failed" + assert outputs["workflow_release_closure_decision"] == "rollback" + assert outputs["workflow_release_closure_should_close_release"] == "false" + assert outputs["workflow_release_closure_should_notify"] == "true" + assert outputs["workflow_release_closure_exit_code"] == "1" + assert outputs["workflow_release_closure_finalization_status"] == "failed" + assert outputs["workflow_release_closure_run_id"] == "2233445566" + assert outputs["workflow_release_closure_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_closure_report_json"].endswith( + "ci_workflow_release_closure.json" + ) + assert outputs["workflow_release_closure_report_markdown"].endswith( + "ci_workflow_release_closure.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py new file mode 100644 index 0000000..30989dc --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_completion_gate_runtime.py @@ -0,0 +1,233 @@ +"""Contract tests for P2-33 Linux CI workflow release completion gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_completion_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_completion_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_completion_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_trace_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_trigger_report": "/tmp/ci_workflow_release_trigger.json", + "project_root": "/tmp", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "handoff_status": "ready_for_release", + "release_trigger_status": "triggered", + "release_trigger_exit_code": 0, + "should_trigger_release": True, + "release_trigger_attempted": True, + "target_environment": "production", + "release_channel": "stable", + "release_workflow_path": ".github/workflows/release.yml", + "release_workflow_ref": "main", + "release_tracking_status": "release_run_tracking_ready", + "release_trace_exit_code": 0, + "poll_now": False, + "dry_run": False, + "should_poll_release_run": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "repo_owner": "acme", + "repo_name": "demo", + "release_poll_command": ( + "gh run view 2233445566 --json " + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt --repo acme/demo" + ), + "release_poll_command_parts": [ + "gh", + "run", + "view", + "2233445566", + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + "--repo", + "acme/demo", + ], + "release_poll_attempted": False, + "release_poll_returncode": None, + "release_poll_status": "", + "release_poll_conclusion": "", + "release_poll_url": "https://github.com/acme/demo/actions/runs/2233445566", + "release_poll_error_type": "", + "release_poll_error_message": "", + "release_poll_stdout_tail": "", + "release_poll_stderr_tail": "", + "release_trigger_reason_codes": ["release_trigger_command_succeeded"], + "reason_codes": ["release_trigger_command_succeeded"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def _success_poll_result() -> dict: + return { + "returncode": 0, + "stdout_tail": ( + '{"status":"completed","conclusion":"success",' + '"url":"https://github.com/acme/demo/actions/runs/2233445566"}' + ), + "stderr_tail": "", + "payload": { + "status": "completed", + "conclusion": "success", + "url": "https://github.com/acme/demo/actions/runs/2233445566", + }, + "error_type": "", + "error_message": "", + } + + +def test_build_release_completion_payload_reaches_success(tmp_path: Path): + gate = _load_ci_workflow_release_completion_gate_module() + payload = _sample_release_trace_payload() + report_path = tmp_path / "ci_workflow_release_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_trace_report(report_path) + completion_payload = gate.build_release_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=3, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=lambda command_parts, cwd, timeout_seconds: _success_poll_result(), + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["release_completion_status"] == "release_run_completed_success" + assert completion_payload["release_completion_exit_code"] == 0 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 1 + assert completion_payload["poll_status"] == "completed" + assert completion_payload["poll_conclusion"] == "success" + assert completion_payload["reason_codes"] == ["release_run_completed_success"] + + +def test_build_release_completion_payload_timeout_without_allow_in_progress(tmp_path: Path): + gate = _load_ci_workflow_release_completion_gate_module() + payload = _sample_release_trace_payload() + report_path = tmp_path / "ci_workflow_release_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_trace_report(report_path) + + def _in_progress_result(command_parts, cwd, timeout_seconds): + return { + "returncode": 0, + "stdout_tail": '{"status":"in_progress","conclusion":null}', + "stderr_tail": "", + "payload": {"status": "in_progress", "conclusion": None}, + "error_type": "", + "error_message": "", + } + + completion_payload = gate.build_release_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=2, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=_in_progress_result, + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["release_completion_status"] == "release_run_await_timeout" + assert completion_payload["release_completion_exit_code"] == 1 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 2 + assert "release_run_await_timeout" in completion_payload["reason_codes"] + + +def test_load_release_trace_report_rejects_command_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_completion_gate_module() + payload = _sample_release_trace_payload() + payload["release_poll_command_parts"] = [ + "gh", + "run", + "view", + "2233445566", + "--json", + "databaseId,status", + ] + report_path = tmp_path / "ci_workflow_release_trace_bad.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_release_trace_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_completion_gate_module() + payload = _sample_release_trace_payload() + payload["release_tracking_status"] = "release_run_completed_failure" + payload["release_trace_exit_code"] = 1 + payload["should_poll_release_run"] = False + payload["release_run_id"] = None + payload["release_poll_command"] = "" + payload["release_poll_command_parts"] = [] + payload["reason_codes"] = ["release_run_completed_with_failure"] + report_path = tmp_path / "ci_workflow_release_trace_terminal.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_trace_report(report_path) + completion_payload = gate.build_release_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=1, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + sleep_fn=lambda seconds: None, + ) + outputs = gate.build_github_output_values( + completion_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_completion.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_completion.md"), + ) + + assert outputs["workflow_release_completion_status"] == "release_run_completed_failure" + assert outputs["workflow_release_completion_exit_code"] == "1" + assert outputs["workflow_release_completion_should_poll"] == "false" + assert outputs["workflow_release_completion_poll_attempted"] == "false" + assert outputs["workflow_release_completion_run_id"] == "" + assert outputs["workflow_release_completion_report_json"].endswith( + "ci_workflow_release_completion.json" + ) + assert outputs["workflow_release_completion_report_markdown"].endswith( + "ci_workflow_release_completion.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py new file mode 100644 index 0000000..42a2c79 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_delivery_final_verdict_gate_runtime.py @@ -0,0 +1,277 @@ +"""Contract tests for P2-43 Linux CI workflow release delivery final verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_delivery_final_verdict_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_delivery_final_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_delivery_final_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_delivery_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_delivery_report": "/tmp/ci_workflow_release_delivery.json", + "source_release_terminal_verdict_report": "/tmp/ci_workflow_release_terminal_verdict.json", + "source_release_incident_report": "/tmp/ci_workflow_release_incident.json", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_publish_archive": True, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_url": "", + "release_terminal_verdict_status": "released", + "release_terminal_verdict_decision": "ship", + "release_terminal_verdict_exit_code": 0, + "terminal_should_ship_release": True, + "terminal_requires_follow_up": False, + "terminal_incident_linked": False, + "release_delivery_status": "shipped", + "release_delivery_decision": "deliver", + "release_delivery_exit_code": 0, + "delivery_should_ship_release": True, + "delivery_requires_human_action": False, + "delivery_should_announce_blocker": False, + "release_delivery_terminal_publish_status": "published", + "release_delivery_terminal_publish_decision": "announce_release", + "release_delivery_terminal_publish_exit_code": 0, + "terminal_publish_should_notify": True, + "terminal_publish_should_create_follow_up": False, + "terminal_publish_channel": "release", + "release_run_id": 3344556677, + "release_run_url": "https://github.com/acme/demo/actions/runs/3344556677", + "release_delivery_terminal_publish_summary": ( + "release_delivery_status=shipped " + "release_delivery_terminal_publish_status=published " + "release_delivery_terminal_publish_decision=announce_release" + ), + "reason_codes": ["release_delivery_terminal_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_delivery.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_terminal_verdict.json", "exists": True}, + ], + } + + +def test_build_release_delivery_final_verdict_payload_completed(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_final_verdict_gate_module() + payload = _sample_release_delivery_terminal_publish_payload() + report_path = tmp_path / "ci_workflow_release_delivery_terminal_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_terminal_publish_report(report_path) + final_payload = gate.build_release_delivery_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_delivery_final_verdict_status"] == "completed" + assert final_payload["release_delivery_final_verdict_decision"] == "close_release" + assert final_payload["release_delivery_final_verdict_exit_code"] == 0 + assert final_payload["final_should_close_release"] is True + assert final_payload["final_should_open_follow_up"] is False + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_announcement_target"] == "release" + assert final_payload["reason_codes"] == ["release_delivery_final_completed"] + + +def test_build_release_delivery_final_verdict_payload_requires_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_final_verdict_gate_module() + payload = _sample_release_delivery_terminal_publish_payload() + payload["release_archive_status"] = "pending" + payload["release_archive_decision"] = "hold" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "awaiting_archive" + payload["release_verdict_decision"] = "hold" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["release_terminal_verdict_status"] = "awaiting_archive" + payload["release_terminal_verdict_decision"] = "hold" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["release_delivery_status"] = "pending_follow_up" + payload["release_delivery_decision"] = "hold" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "pending_follow_up" + payload["release_delivery_terminal_publish_decision"] = "announce_hold" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "hold" + payload["reason_codes"] = ["release_delivery_terminal_pending_follow_up"] + report_path = tmp_path / "ci_workflow_release_delivery_terminal_publish_hold.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_terminal_publish_report(report_path) + final_payload = gate.build_release_delivery_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_delivery_final_verdict_status"] == "requires_follow_up" + assert final_payload["release_delivery_final_verdict_decision"] == "open_follow_up" + assert final_payload["release_delivery_final_verdict_exit_code"] == 1 + assert final_payload["final_should_close_release"] is False + assert final_payload["final_should_open_follow_up"] is True + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_announcement_target"] == "hold" + assert "release_delivery_final_follow_up_required" in final_payload["reason_codes"] + + +def test_build_release_delivery_final_verdict_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_final_verdict_gate_module() + payload = _sample_release_delivery_terminal_publish_payload() + payload["release_delivery_terminal_publish_status"] = "published" + payload["release_delivery_terminal_publish_decision"] = "announce_release" + payload["release_delivery_terminal_publish_exit_code"] = 0 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = False + payload["terminal_publish_channel"] = "release" + payload["release_delivery_status"] = "shipped" + payload["release_delivery_decision"] = "deliver" + payload["release_delivery_exit_code"] = 0 + payload["delivery_should_ship_release"] = True + payload["delivery_requires_human_action"] = False + payload["incident_dispatch_status"] = "ready_dry_run" + report_path = tmp_path / "ci_workflow_release_delivery_terminal_publish_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_terminal_publish_report(report_path) + final_payload = gate.build_release_delivery_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_delivery_final_verdict_status"] == "contract_failed" + assert final_payload["release_delivery_final_verdict_decision"] == "abort_close" + assert final_payload["release_delivery_final_verdict_exit_code"] == 1 + assert "incident_status_mismatch_published" in final_payload["structural_issues"] + + +def test_build_release_delivery_final_verdict_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_final_verdict_gate_module() + payload = _sample_release_delivery_terminal_publish_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_delivery.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_terminal_verdict.json", "exists": False}, + ] + report_path = tmp_path / "ci_workflow_release_delivery_terminal_publish_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_terminal_publish_report(report_path) + final_payload = gate.build_release_delivery_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_delivery_final_verdict_status"] == "contract_failed" + assert final_payload["release_delivery_final_verdict_decision"] == "abort_close" + assert final_payload["release_delivery_final_verdict_exit_code"] == 1 + assert "/tmp/ci_workflow_release_terminal_verdict.json" in final_payload["missing_artifacts"] + assert "release_delivery_final_evidence_missing" in final_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_final_verdict_gate_module() + payload = _sample_release_delivery_terminal_publish_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/88" + payload["release_terminal_verdict_status"] = "blocked_incident_dispatched" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["release_delivery_status"] = "blocked_incident" + payload["release_delivery_decision"] = "escalate" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "blocked" + payload["release_delivery_terminal_publish_decision"] = "announce_blocker" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "blocker" + payload["reason_codes"] = ["release_delivery_terminal_blocked"] + report_path = tmp_path / "ci_workflow_release_delivery_terminal_publish_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_terminal_publish_report(report_path) + final_payload = gate.build_release_delivery_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_final_verdict.md" + ), + ) + + assert outputs["workflow_release_delivery_final_verdict_status"] == "blocked" + assert outputs["workflow_release_delivery_final_verdict_decision"] == "escalate_blocker" + assert outputs["workflow_release_delivery_final_verdict_exit_code"] == "1" + assert outputs["workflow_release_delivery_final_should_close_release"] == "false" + assert outputs["workflow_release_delivery_final_should_open_follow_up"] == "true" + assert outputs["workflow_release_delivery_final_should_page_owner"] == "true" + assert outputs["workflow_release_delivery_final_announcement_target"] == "blocker" + assert outputs["workflow_release_delivery_final_terminal_publish_status"] == "blocked" + assert outputs["workflow_release_delivery_final_incident_status"] == "dispatched" + assert outputs["workflow_release_delivery_final_incident_url"].endswith("/issues/88") + assert outputs["workflow_release_delivery_final_run_id"] == "3344556677" + assert outputs["workflow_release_delivery_final_run_url"].endswith( + "/actions/runs/3344556677" + ) + assert outputs["workflow_release_delivery_final_report_json"].endswith( + "ci_workflow_release_delivery_final_verdict.json" + ) + assert outputs["workflow_release_delivery_final_report_markdown"].endswith( + "ci_workflow_release_delivery_final_verdict.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py new file mode 100644 index 0000000..a1b49b9 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_delivery_gate_runtime.py @@ -0,0 +1,228 @@ +"""Contract tests for P2-41 Linux CI workflow release delivery gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_delivery_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_delivery_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_delivery_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_terminal_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_incident_report": "/tmp/ci_workflow_release_incident.json", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_publish_archive": True, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_url": "", + "release_terminal_verdict_status": "released", + "release_terminal_verdict_decision": "ship", + "release_terminal_verdict_exit_code": 0, + "terminal_should_ship_release": True, + "terminal_requires_follow_up": False, + "terminal_incident_linked": False, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "release_terminal_verdict_summary": ( + "release_verdict_status=published incident_dispatch_status=not_required " + "release_terminal_verdict_status=released release_terminal_verdict_decision=ship" + ), + "reason_codes": ["release_terminal_released"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": True}, + ], + } + + +def test_build_release_delivery_payload_shipped(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_gate_module() + payload = _sample_release_terminal_verdict_payload() + report_path = tmp_path / "ci_workflow_release_terminal_verdict.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_verdict_report(report_path) + delivery_payload = gate.build_release_delivery_payload( + report, + source_path=report_path.resolve(), + ) + + assert delivery_payload["release_delivery_status"] == "shipped" + assert delivery_payload["release_delivery_decision"] == "deliver" + assert delivery_payload["release_delivery_exit_code"] == 0 + assert delivery_payload["delivery_should_ship_release"] is True + assert delivery_payload["delivery_requires_human_action"] is False + assert delivery_payload["reason_codes"] == ["release_delivery_shipped"] + + +def test_build_release_delivery_payload_blocked_incident(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_gate_module() + payload = _sample_release_terminal_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "ready_dry_run" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = False + payload["release_terminal_verdict_status"] = "blocked_incident_ready_dry_run" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["reason_codes"] = ["release_terminal_blocked_incident_ready_dry_run"] + report_path = tmp_path / "ci_workflow_release_terminal_verdict_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_verdict_report(report_path) + delivery_payload = gate.build_release_delivery_payload( + report, + source_path=report_path.resolve(), + ) + + assert delivery_payload["release_delivery_status"] == "blocked_incident" + assert delivery_payload["release_delivery_decision"] == "escalate" + assert delivery_payload["release_delivery_exit_code"] == 1 + assert delivery_payload["delivery_should_ship_release"] is False + assert delivery_payload["delivery_requires_human_action"] is True + assert "release_delivery_incident_escalation_required" in delivery_payload["reason_codes"] + + +def test_build_release_delivery_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_gate_module() + payload = _sample_release_terminal_verdict_payload() + payload["release_terminal_verdict_status"] = "released" + payload["release_terminal_verdict_decision"] = "ship" + payload["release_terminal_verdict_exit_code"] = 0 + payload["terminal_should_ship_release"] = True + payload["terminal_requires_follow_up"] = False + payload["incident_dispatch_status"] = "ready_dry_run" + payload["should_dispatch_incident"] = True + report_path = tmp_path / "ci_workflow_release_terminal_verdict_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_verdict_report(report_path) + delivery_payload = gate.build_release_delivery_payload( + report, + source_path=report_path.resolve(), + ) + + assert delivery_payload["release_delivery_status"] == "contract_failed" + assert delivery_payload["release_delivery_decision"] == "block" + assert delivery_payload["release_delivery_exit_code"] == 1 + assert "incident_status_mismatch_released" in delivery_payload["structural_issues"] + + +def test_build_release_delivery_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_gate_module() + payload = _sample_release_terminal_verdict_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": False}, + ] + report_path = tmp_path / "ci_workflow_release_terminal_verdict_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_verdict_report(report_path) + delivery_payload = gate.build_release_delivery_payload( + report, + source_path=report_path.resolve(), + ) + + assert delivery_payload["release_delivery_status"] == "contract_failed" + assert delivery_payload["release_delivery_decision"] == "block" + assert delivery_payload["release_delivery_exit_code"] == 1 + assert "/tmp/ci_workflow_release_finalization.json" in delivery_payload["missing_artifacts"] + assert "release_delivery_evidence_missing" in delivery_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_gate_module() + payload = _sample_release_terminal_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/77" + payload["release_terminal_verdict_status"] = "blocked_incident_dispatched" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["reason_codes"] = ["release_terminal_blocked_incident_dispatched"] + report_path = tmp_path / "ci_workflow_release_terminal_verdict_dispatched.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_verdict_report(report_path) + delivery_payload = gate.build_release_delivery_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + delivery_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_delivery.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_delivery.md"), + ) + + assert outputs["workflow_release_delivery_status"] == "blocked_incident" + assert outputs["workflow_release_delivery_decision"] == "escalate" + assert outputs["workflow_release_delivery_exit_code"] == "1" + assert outputs["workflow_release_delivery_should_ship_release"] == "false" + assert outputs["workflow_release_delivery_requires_human_action"] == "true" + assert outputs["workflow_release_delivery_should_announce_blocker"] == "true" + assert outputs["workflow_release_delivery_terminal_verdict_status"] == "blocked_incident_dispatched" + assert outputs["workflow_release_delivery_incident_status"] == "dispatched" + assert outputs["workflow_release_delivery_incident_url"].endswith("/issues/77") + assert outputs["workflow_release_delivery_run_id"] == "2233445566" + assert outputs["workflow_release_delivery_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_delivery_report_json"].endswith( + "ci_workflow_release_delivery.json" + ) + assert outputs["workflow_release_delivery_report_markdown"].endswith( + "ci_workflow_release_delivery.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py new file mode 100644 index 0000000..73d22a9 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_delivery_terminal_publish_gate_runtime.py @@ -0,0 +1,259 @@ +"""Contract tests for P2-42 Linux CI workflow release delivery terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_delivery_terminal_publish_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_delivery_terminal_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_delivery_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_delivery_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_terminal_verdict_report": "/tmp/ci_workflow_release_terminal_verdict.json", + "source_release_incident_report": "/tmp/ci_workflow_release_incident.json", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_publish_archive": True, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_url": "", + "release_terminal_verdict_status": "released", + "release_terminal_verdict_decision": "ship", + "release_terminal_verdict_exit_code": 0, + "terminal_should_ship_release": True, + "terminal_requires_follow_up": False, + "terminal_incident_linked": False, + "release_delivery_status": "shipped", + "release_delivery_decision": "deliver", + "release_delivery_exit_code": 0, + "delivery_should_ship_release": True, + "delivery_requires_human_action": False, + "delivery_should_announce_blocker": False, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "release_delivery_summary": ( + "release_terminal_verdict_status=released " + "release_delivery_status=shipped " + "release_delivery_decision=deliver" + ), + "reason_codes": ["release_delivery_shipped"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": True}, + ], + } + + +def test_build_release_delivery_terminal_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_terminal_publish_gate_module() + payload = _sample_release_delivery_payload() + report_path = tmp_path / "ci_workflow_release_delivery.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_report(report_path) + terminal_payload = gate.build_release_delivery_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_delivery_terminal_publish_status"] == "published" + assert terminal_payload["release_delivery_terminal_publish_decision"] == "announce_release" + assert terminal_payload["release_delivery_terminal_publish_exit_code"] == 0 + assert terminal_payload["terminal_publish_should_notify"] is True + assert terminal_payload["terminal_publish_should_create_follow_up"] is False + assert terminal_payload["terminal_publish_channel"] == "release" + assert terminal_payload["reason_codes"] == ["release_delivery_terminal_published"] + + +def test_build_release_delivery_terminal_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_terminal_publish_gate_module() + payload = _sample_release_delivery_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "ready_dry_run" + payload["release_terminal_verdict_status"] = "blocked_incident_ready_dry_run" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["release_delivery_status"] = "blocked_incident" + payload["release_delivery_decision"] = "escalate" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["reason_codes"] = ["release_delivery_incident_escalation_required"] + report_path = tmp_path / "ci_workflow_release_delivery_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_report(report_path) + terminal_payload = gate.build_release_delivery_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_delivery_terminal_publish_status"] == "blocked" + assert terminal_payload["release_delivery_terminal_publish_decision"] == "announce_blocker" + assert terminal_payload["release_delivery_terminal_publish_exit_code"] == 1 + assert terminal_payload["terminal_publish_should_notify"] is True + assert terminal_payload["terminal_publish_should_create_follow_up"] is True + assert terminal_payload["terminal_publish_channel"] == "blocker" + assert "release_delivery_terminal_blocked" in terminal_payload["reason_codes"] + + +def test_build_release_delivery_terminal_publish_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_terminal_publish_gate_module() + payload = _sample_release_delivery_payload() + payload["release_delivery_status"] = "shipped" + payload["release_delivery_decision"] = "deliver" + payload["release_delivery_exit_code"] = 0 + payload["delivery_should_ship_release"] = True + payload["delivery_requires_human_action"] = False + payload["delivery_should_announce_blocker"] = False + payload["incident_dispatch_status"] = "ready_dry_run" + payload["should_dispatch_incident"] = True + report_path = tmp_path / "ci_workflow_release_delivery_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_report(report_path) + terminal_payload = gate.build_release_delivery_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_delivery_terminal_publish_status"] == "contract_failed" + assert terminal_payload["release_delivery_terminal_publish_decision"] == "abort_publish" + assert terminal_payload["release_delivery_terminal_publish_exit_code"] == 1 + assert "incident_status_mismatch_shipped" in terminal_payload["structural_issues"] + + +def test_build_release_delivery_terminal_publish_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_terminal_publish_gate_module() + payload = _sample_release_delivery_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": False}, + ] + report_path = tmp_path / "ci_workflow_release_delivery_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_report(report_path) + terminal_payload = gate.build_release_delivery_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_delivery_terminal_publish_status"] == "contract_failed" + assert terminal_payload["release_delivery_terminal_publish_decision"] == "abort_publish" + assert terminal_payload["release_delivery_terminal_publish_exit_code"] == 1 + assert "/tmp/ci_workflow_release_finalization.json" in terminal_payload["missing_artifacts"] + assert "release_delivery_terminal_evidence_missing" in terminal_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_delivery_terminal_publish_gate_module() + payload = _sample_release_delivery_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/77" + payload["release_terminal_verdict_status"] = "blocked_incident_dispatched" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["release_delivery_status"] = "blocked_incident" + payload["release_delivery_decision"] = "escalate" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["reason_codes"] = ["release_delivery_incident_escalation_required"] + report_path = tmp_path / "ci_workflow_release_delivery_incident.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_report(report_path) + terminal_payload = gate.build_release_delivery_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + terminal_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_delivery_terminal_publish.md" + ), + ) + + assert outputs["workflow_release_delivery_terminal_publish_status"] == "blocked" + assert outputs["workflow_release_delivery_terminal_publish_decision"] == "announce_blocker" + assert outputs["workflow_release_delivery_terminal_publish_exit_code"] == "1" + assert outputs["workflow_release_delivery_terminal_publish_should_notify"] == "true" + assert outputs["workflow_release_delivery_terminal_publish_should_create_follow_up"] == "true" + assert outputs["workflow_release_delivery_terminal_publish_channel"] == "blocker" + assert outputs["workflow_release_delivery_terminal_publish_delivery_status"] == "blocked_incident" + assert outputs["workflow_release_delivery_terminal_publish_incident_status"] == "dispatched" + assert outputs["workflow_release_delivery_terminal_publish_incident_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_release_delivery_terminal_publish_run_id"] == "2233445566" + assert outputs["workflow_release_delivery_terminal_publish_run_url"].endswith( + "/actions/runs/2233445566" + ) + assert outputs["workflow_release_delivery_terminal_publish_report_json"].endswith( + "ci_workflow_release_delivery_terminal_publish.json" + ) + assert outputs["workflow_release_delivery_terminal_publish_report_markdown"].endswith( + "ci_workflow_release_delivery_terminal_publish.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py new file mode 100644 index 0000000..9cc7eb0 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_archive_gate_runtime.py @@ -0,0 +1,293 @@ +"""Contract tests for P2-53 Linux CI workflow release final archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_archive_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_archive_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_closure_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_closure_publish_should_notify": True, + "final_closure_publish_requires_manual_action": False, + "final_closure_publish_channel": "release", + "release_final_closure_publish_summary": ( + "release_final_closure_status=closed " + "release_final_closure_publish_status=published " + "release_final_closure_publish_decision=announce_release_closed" + ), + "reason_codes": ["release_final_closure_publish_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_closure", + "path": "/tmp/ci_workflow_release_final_closure.json", + "exists": True, + }, + { + "source": "final_handoff", + "path": "/tmp/ci_workflow_release_final_handoff.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_archive_payload_archived(tmp_path: Path): + gate = _load_ci_workflow_release_final_archive_gate_module() + publish_payload = _sample_release_final_closure_publish_payload() + + publish_path = tmp_path / "ci_workflow_release_final_closure_publish.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_closure_publish_report(publish_path) + archive_payload = gate.build_release_final_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_archive_status"] == "archived" + assert archive_payload["release_final_archive_decision"] == "archive_release_closed" + assert archive_payload["release_final_archive_exit_code"] == 0 + assert archive_payload["final_archive_should_publish"] is True + assert archive_payload["final_archive_requires_manual_action"] is False + assert archive_payload["final_archive_channel"] == "release" + assert "release_final_archive_archived" in archive_payload["reason_codes"] + + +def test_build_release_final_archive_payload_archived_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_archive_gate_module() + publish_payload = _sample_release_final_closure_publish_payload() + publish_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["final_closure_publish_should_notify"] = True + publish_payload["final_closure_publish_requires_manual_action"] = False + publish_payload["final_closure_publish_channel"] = "follow_up" + publish_payload["reason_codes"] = ["release_final_closure_publish_with_follow_up"] + + publish_path = tmp_path / "release_final_closure_publish_with_follow_up.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_closure_publish_report(publish_path) + archive_payload = gate.build_release_final_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_archive_status"] == "archived_with_follow_up" + assert ( + archive_payload["release_final_archive_decision"] + == "archive_release_closed_with_follow_up" + ) + assert archive_payload["release_final_archive_exit_code"] == 1 + assert archive_payload["final_archive_should_publish"] is True + assert archive_payload["final_archive_requires_manual_action"] is False + assert archive_payload["final_archive_channel"] == "follow_up" + assert "release_final_archive_archived_with_follow_up" in archive_payload["reason_codes"] + + +def test_build_release_final_archive_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_archive_gate_module() + publish_payload = _sample_release_final_closure_publish_payload() + publish_payload["release_delivery_final_verdict_status"] = "blocked" + publish_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + publish_payload["release_delivery_final_verdict_exit_code"] = 1 + publish_payload["release_follow_up_final_verdict_status"] = "blocked" + publish_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "blocked" + publish_payload["release_final_outcome_decision"] = "escalate_blocker" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "blocked" + publish_payload["release_final_terminal_publish_decision"] = "announce_blocker" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "blocked" + publish_payload["release_final_handoff_decision"] = "handoff_blocker" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "blocked" + publish_payload["release_final_closure_decision"] = "close_blocker" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "blocked" + publish_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["final_closure_publish_should_notify"] = True + publish_payload["final_closure_publish_requires_manual_action"] = True + publish_payload["final_closure_publish_channel"] = "blocker" + publish_payload["reason_codes"] = ["release_final_closure_publish_blocked"] + + publish_path = tmp_path / "release_final_closure_publish_blocked.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_closure_publish_report(publish_path) + archive_payload = gate.build_release_final_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_archive_status"] == "blocked" + assert archive_payload["release_final_archive_decision"] == "archive_release_blocker" + assert archive_payload["release_final_archive_exit_code"] == 1 + assert archive_payload["final_archive_should_publish"] is False + assert archive_payload["final_archive_requires_manual_action"] is True + assert archive_payload["final_archive_channel"] == "blocker" + assert "release_final_archive_blocked" in archive_payload["reason_codes"] + + +def test_build_release_final_archive_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_archive_gate_module() + publish_payload = _sample_release_final_closure_publish_payload() + publish_payload["release_final_closure_publish_status"] = "published" + publish_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_payload["final_closure_publish_requires_manual_action"] = True + publish_payload["final_closure_publish_channel"] = "follow_up" + + publish_path = tmp_path / "release_final_closure_publish_mismatch.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_closure_publish_report(publish_path) + archive_payload = gate.build_release_final_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_archive_status"] == "contract_failed" + assert archive_payload["release_final_archive_decision"] == "abort_archive" + assert archive_payload["release_final_archive_exit_code"] == 1 + assert "final_closure_publish_decision_mismatch" in archive_payload["structural_issues"] + assert ( + "final_closure_publish_requires_manual_action_mismatch" + in archive_payload["structural_issues"] + ) + assert "final_closure_publish_channel_mismatch" in archive_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_archive_gate_module() + publish_payload = _sample_release_final_closure_publish_payload() + publish_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["final_closure_publish_should_notify"] = True + publish_payload["final_closure_publish_requires_manual_action"] = False + publish_payload["final_closure_publish_channel"] = "follow_up" + publish_payload["reason_codes"] = ["release_final_closure_publish_with_follow_up"] + + publish_path = tmp_path / "release_final_closure_publish_output.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_closure_publish_report(publish_path) + archive_payload = gate.build_release_final_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_archive.md"), + ) + + assert outputs["workflow_release_final_archive_status"] == "archived_with_follow_up" + assert ( + outputs["workflow_release_final_archive_decision"] + == "archive_release_closed_with_follow_up" + ) + assert outputs["workflow_release_final_archive_exit_code"] == "1" + assert outputs["workflow_release_final_archive_should_publish"] == "true" + assert outputs["workflow_release_final_archive_requires_manual_action"] == "false" + assert outputs["workflow_release_final_archive_channel"] == "follow_up" + assert outputs["workflow_release_final_archive_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_archive_run_id"] == "1234567890" + assert outputs["workflow_release_final_archive_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_archive_report_json"].endswith( + "ci_workflow_release_final_archive.json" + ) + assert outputs["workflow_release_final_archive_report_markdown"].endswith( + "ci_workflow_release_final_archive.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py new file mode 100644 index 0000000..82cc675 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_closure_gate_runtime.py @@ -0,0 +1,258 @@ +"""Contract tests for P2-51 Linux CI workflow release final closure gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_closure_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_closure_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_closure_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_handoff_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_handoff_should_archive_release": True, + "final_handoff_keep_follow_up_open": False, + "final_handoff_should_page_owner": False, + "final_handoff_target": "release", + "release_final_handoff_summary": ( + "release_final_terminal_publish_status=published " + "release_final_handoff_status=completed " + "release_final_handoff_decision=handoff_release_closure" + ), + "reason_codes": ["release_final_handoff_completed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "terminal_publish", + "path": "/tmp/ci_workflow_release_final_terminal_publish.json", + "exists": True, + }, + { + "source": "final_outcome", + "path": "/tmp/ci_workflow_release_final_outcome.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_closure_payload_closed(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_gate_module() + handoff_payload = _sample_release_final_handoff_payload() + + handoff_path = tmp_path / "ci_workflow_release_final_handoff.json" + handoff_path.write_text(json.dumps(handoff_payload, indent=2), encoding="utf-8") + + handoff_report = gate.load_release_final_handoff_report(handoff_path) + closure_payload = gate.build_release_final_closure_payload( + handoff_report, + source_path=handoff_path.resolve(), + ) + + assert closure_payload["release_final_closure_status"] == "closed" + assert closure_payload["release_final_closure_decision"] == "close_release" + assert closure_payload["release_final_closure_exit_code"] == 0 + assert closure_payload["final_closure_is_closed"] is True + assert closure_payload["final_closure_has_open_follow_up"] is False + assert closure_payload["final_closure_should_page_owner"] is False + assert closure_payload["final_closure_target"] == "release" + assert "release_final_closure_closed" in closure_payload["reason_codes"] + + +def test_build_release_final_closure_payload_closed_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_gate_module() + handoff_payload = _sample_release_final_handoff_payload() + handoff_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + handoff_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + handoff_payload["release_follow_up_final_verdict_exit_code"] = 1 + handoff_payload["release_final_outcome_status"] = "released_with_follow_up" + handoff_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + handoff_payload["release_final_outcome_exit_code"] = 1 + handoff_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + handoff_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + handoff_payload["release_final_terminal_publish_exit_code"] = 1 + handoff_payload["release_final_handoff_status"] = "completed_with_follow_up" + handoff_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + handoff_payload["release_final_handoff_exit_code"] = 1 + handoff_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + handoff_payload["final_handoff_should_archive_release"] = True + handoff_payload["final_handoff_keep_follow_up_open"] = True + handoff_payload["final_handoff_should_page_owner"] = False + handoff_payload["final_handoff_target"] = "follow_up" + handoff_payload["reason_codes"] = ["release_final_handoff_completed_with_follow_up"] + + handoff_path = tmp_path / "release_final_handoff_with_follow_up.json" + handoff_path.write_text(json.dumps(handoff_payload, indent=2), encoding="utf-8") + + handoff_report = gate.load_release_final_handoff_report(handoff_path) + closure_payload = gate.build_release_final_closure_payload( + handoff_report, + source_path=handoff_path.resolve(), + ) + + assert closure_payload["release_final_closure_status"] == "closed_with_follow_up" + assert closure_payload["release_final_closure_decision"] == "close_with_follow_up" + assert closure_payload["release_final_closure_exit_code"] == 1 + assert closure_payload["final_closure_is_closed"] is True + assert closure_payload["final_closure_has_open_follow_up"] is True + assert closure_payload["final_closure_should_page_owner"] is False + assert closure_payload["final_closure_target"] == "follow_up" + assert "release_final_closure_closed_with_follow_up" in closure_payload["reason_codes"] + + +def test_build_release_final_closure_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_gate_module() + handoff_payload = _sample_release_final_handoff_payload() + handoff_payload["release_delivery_final_verdict_status"] = "blocked" + handoff_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + handoff_payload["release_delivery_final_verdict_exit_code"] = 1 + handoff_payload["release_follow_up_final_verdict_status"] = "blocked" + handoff_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + handoff_payload["release_follow_up_final_verdict_exit_code"] = 1 + handoff_payload["release_final_outcome_status"] = "blocked" + handoff_payload["release_final_outcome_decision"] = "escalate_blocker" + handoff_payload["release_final_outcome_exit_code"] = 1 + handoff_payload["release_final_terminal_publish_status"] = "blocked" + handoff_payload["release_final_terminal_publish_decision"] = "announce_blocker" + handoff_payload["release_final_terminal_publish_exit_code"] = 1 + handoff_payload["release_final_handoff_status"] = "blocked" + handoff_payload["release_final_handoff_decision"] = "handoff_blocker" + handoff_payload["release_final_handoff_exit_code"] = 1 + handoff_payload["final_handoff_should_archive_release"] = False + handoff_payload["final_handoff_keep_follow_up_open"] = True + handoff_payload["final_handoff_should_page_owner"] = True + handoff_payload["final_handoff_target"] = "blocker" + handoff_payload["reason_codes"] = ["release_final_handoff_blocked"] + + handoff_path = tmp_path / "release_final_handoff_blocked.json" + handoff_path.write_text(json.dumps(handoff_payload, indent=2), encoding="utf-8") + + handoff_report = gate.load_release_final_handoff_report(handoff_path) + closure_payload = gate.build_release_final_closure_payload( + handoff_report, + source_path=handoff_path.resolve(), + ) + + assert closure_payload["release_final_closure_status"] == "blocked" + assert closure_payload["release_final_closure_decision"] == "close_blocker" + assert closure_payload["release_final_closure_exit_code"] == 1 + assert closure_payload["final_closure_is_closed"] is False + assert closure_payload["final_closure_has_open_follow_up"] is True + assert closure_payload["final_closure_should_page_owner"] is True + assert closure_payload["final_closure_target"] == "blocker" + assert "release_final_closure_blocked" in closure_payload["reason_codes"] + + +def test_build_release_final_closure_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_gate_module() + handoff_payload = _sample_release_final_handoff_payload() + handoff_payload["release_final_handoff_status"] = "completed" + handoff_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + handoff_payload["final_handoff_keep_follow_up_open"] = True + handoff_payload["final_handoff_target"] = "follow_up" + + handoff_path = tmp_path / "release_final_handoff_mismatch.json" + handoff_path.write_text(json.dumps(handoff_payload, indent=2), encoding="utf-8") + + handoff_report = gate.load_release_final_handoff_report(handoff_path) + closure_payload = gate.build_release_final_closure_payload( + handoff_report, + source_path=handoff_path.resolve(), + ) + + assert closure_payload["release_final_closure_status"] == "contract_failed" + assert closure_payload["release_final_closure_decision"] == "abort_close" + assert closure_payload["release_final_closure_exit_code"] == 1 + assert "final_handoff_decision_mismatch" in closure_payload["structural_issues"] + assert "final_handoff_keep_follow_up_open_mismatch" in closure_payload["structural_issues"] + assert "final_handoff_target_mismatch" in closure_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_gate_module() + handoff_payload = _sample_release_final_handoff_payload() + handoff_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + handoff_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + handoff_payload["release_follow_up_final_verdict_exit_code"] = 1 + handoff_payload["release_final_outcome_status"] = "released_with_follow_up" + handoff_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + handoff_payload["release_final_outcome_exit_code"] = 1 + handoff_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + handoff_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + handoff_payload["release_final_terminal_publish_exit_code"] = 1 + handoff_payload["release_final_handoff_status"] = "completed_with_follow_up" + handoff_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + handoff_payload["release_final_handoff_exit_code"] = 1 + handoff_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + handoff_payload["final_handoff_should_archive_release"] = True + handoff_payload["final_handoff_keep_follow_up_open"] = True + handoff_payload["final_handoff_should_page_owner"] = False + handoff_payload["final_handoff_target"] = "follow_up" + handoff_payload["reason_codes"] = ["release_final_handoff_completed_with_follow_up"] + + handoff_path = tmp_path / "release_final_handoff_output.json" + handoff_path.write_text(json.dumps(handoff_payload, indent=2), encoding="utf-8") + + handoff_report = gate.load_release_final_handoff_report(handoff_path) + closure_payload = gate.build_release_final_closure_payload( + handoff_report, + source_path=handoff_path.resolve(), + ) + outputs = gate.build_github_output_values( + closure_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure.md"), + ) + + assert outputs["workflow_release_final_closure_status"] == "closed_with_follow_up" + assert outputs["workflow_release_final_closure_decision"] == "close_with_follow_up" + assert outputs["workflow_release_final_closure_exit_code"] == "1" + assert outputs["workflow_release_final_closure_is_closed"] == "true" + assert outputs["workflow_release_final_closure_has_open_follow_up"] == "true" + assert outputs["workflow_release_final_closure_should_page_owner"] == "false" + assert outputs["workflow_release_final_closure_target"] == "follow_up" + assert outputs["workflow_release_final_closure_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_closure_run_id"] == "1234567890" + assert outputs["workflow_release_final_closure_run_url"].endswith("/actions/runs/1234567890") + assert outputs["workflow_release_final_closure_report_json"].endswith( + "ci_workflow_release_final_closure.json" + ) + assert outputs["workflow_release_final_closure_report_markdown"].endswith( + "ci_workflow_release_final_closure.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py new file mode 100644 index 0000000..76cff4a --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_closure_publish_gate_runtime.py @@ -0,0 +1,275 @@ +"""Contract tests for P2-52 Linux CI workflow release final closure publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_closure_publish_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_closure_publish_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_closure_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_closure_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_closure_is_closed": True, + "final_closure_has_open_follow_up": False, + "final_closure_should_page_owner": False, + "final_closure_target": "release", + "release_final_closure_summary": ( + "release_final_handoff_status=completed " + "release_final_closure_status=closed " + "release_final_closure_decision=close_release" + ), + "reason_codes": ["release_final_closure_closed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_handoff", + "path": "/tmp/ci_workflow_release_final_handoff.json", + "exists": True, + }, + { + "source": "final_terminal_publish", + "path": "/tmp/ci_workflow_release_final_terminal_publish.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_closure_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_publish_gate_module() + closure_payload = _sample_release_final_closure_payload() + + closure_path = tmp_path / "ci_workflow_release_final_closure.json" + closure_path.write_text(json.dumps(closure_payload, indent=2), encoding="utf-8") + + closure_report = gate.load_release_final_closure_report(closure_path) + publish_payload = gate.build_release_final_closure_publish_payload( + closure_report, + source_path=closure_path.resolve(), + ) + + assert publish_payload["release_final_closure_publish_status"] == "published" + assert publish_payload["release_final_closure_publish_decision"] == "announce_release_closed" + assert publish_payload["release_final_closure_publish_exit_code"] == 0 + assert publish_payload["final_closure_publish_should_notify"] is True + assert publish_payload["final_closure_publish_requires_manual_action"] is False + assert publish_payload["final_closure_publish_channel"] == "release" + assert "release_final_closure_publish_published" in publish_payload["reason_codes"] + + +def test_build_release_final_closure_publish_payload_published_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_publish_gate_module() + closure_payload = _sample_release_final_closure_payload() + closure_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + closure_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + closure_payload["release_follow_up_final_verdict_exit_code"] = 1 + closure_payload["release_final_outcome_status"] = "released_with_follow_up" + closure_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + closure_payload["release_final_outcome_exit_code"] = 1 + closure_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + closure_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + closure_payload["release_final_terminal_publish_exit_code"] = 1 + closure_payload["release_final_handoff_status"] = "completed_with_follow_up" + closure_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + closure_payload["release_final_handoff_exit_code"] = 1 + closure_payload["release_final_closure_status"] = "closed_with_follow_up" + closure_payload["release_final_closure_decision"] = "close_with_follow_up" + closure_payload["release_final_closure_exit_code"] = 1 + closure_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + closure_payload["final_closure_is_closed"] = True + closure_payload["final_closure_has_open_follow_up"] = True + closure_payload["final_closure_should_page_owner"] = False + closure_payload["final_closure_target"] = "follow_up" + closure_payload["reason_codes"] = ["release_final_closure_closed_with_follow_up"] + + closure_path = tmp_path / "release_final_closure_with_follow_up.json" + closure_path.write_text(json.dumps(closure_payload, indent=2), encoding="utf-8") + + closure_report = gate.load_release_final_closure_report(closure_path) + publish_payload = gate.build_release_final_closure_publish_payload( + closure_report, + source_path=closure_path.resolve(), + ) + + assert publish_payload["release_final_closure_publish_status"] == "published_with_follow_up" + assert ( + publish_payload["release_final_closure_publish_decision"] + == "announce_release_closed_with_follow_up" + ) + assert publish_payload["release_final_closure_publish_exit_code"] == 1 + assert publish_payload["final_closure_publish_should_notify"] is True + assert publish_payload["final_closure_publish_requires_manual_action"] is False + assert publish_payload["final_closure_publish_channel"] == "follow_up" + assert "release_final_closure_publish_with_follow_up" in publish_payload["reason_codes"] + + +def test_build_release_final_closure_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_publish_gate_module() + closure_payload = _sample_release_final_closure_payload() + closure_payload["release_delivery_final_verdict_status"] = "blocked" + closure_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + closure_payload["release_delivery_final_verdict_exit_code"] = 1 + closure_payload["release_follow_up_final_verdict_status"] = "blocked" + closure_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + closure_payload["release_follow_up_final_verdict_exit_code"] = 1 + closure_payload["release_final_outcome_status"] = "blocked" + closure_payload["release_final_outcome_decision"] = "escalate_blocker" + closure_payload["release_final_outcome_exit_code"] = 1 + closure_payload["release_final_terminal_publish_status"] = "blocked" + closure_payload["release_final_terminal_publish_decision"] = "announce_blocker" + closure_payload["release_final_terminal_publish_exit_code"] = 1 + closure_payload["release_final_handoff_status"] = "blocked" + closure_payload["release_final_handoff_decision"] = "handoff_blocker" + closure_payload["release_final_handoff_exit_code"] = 1 + closure_payload["release_final_closure_status"] = "blocked" + closure_payload["release_final_closure_decision"] = "close_blocker" + closure_payload["release_final_closure_exit_code"] = 1 + closure_payload["final_closure_is_closed"] = False + closure_payload["final_closure_has_open_follow_up"] = True + closure_payload["final_closure_should_page_owner"] = True + closure_payload["final_closure_target"] = "blocker" + closure_payload["reason_codes"] = ["release_final_closure_blocked"] + + closure_path = tmp_path / "release_final_closure_blocked.json" + closure_path.write_text(json.dumps(closure_payload, indent=2), encoding="utf-8") + + closure_report = gate.load_release_final_closure_report(closure_path) + publish_payload = gate.build_release_final_closure_publish_payload( + closure_report, + source_path=closure_path.resolve(), + ) + + assert publish_payload["release_final_closure_publish_status"] == "blocked" + assert publish_payload["release_final_closure_publish_decision"] == "announce_release_blocker" + assert publish_payload["release_final_closure_publish_exit_code"] == 1 + assert publish_payload["final_closure_publish_should_notify"] is True + assert publish_payload["final_closure_publish_requires_manual_action"] is True + assert publish_payload["final_closure_publish_channel"] == "blocker" + assert "release_final_closure_publish_blocked" in publish_payload["reason_codes"] + + +def test_build_release_final_closure_publish_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_publish_gate_module() + closure_payload = _sample_release_final_closure_payload() + closure_payload["release_final_closure_status"] = "closed" + closure_payload["release_final_closure_decision"] = "close_with_follow_up" + closure_payload["final_closure_has_open_follow_up"] = True + closure_payload["final_closure_target"] = "follow_up" + + closure_path = tmp_path / "release_final_closure_mismatch.json" + closure_path.write_text(json.dumps(closure_payload, indent=2), encoding="utf-8") + + closure_report = gate.load_release_final_closure_report(closure_path) + publish_payload = gate.build_release_final_closure_publish_payload( + closure_report, + source_path=closure_path.resolve(), + ) + + assert publish_payload["release_final_closure_publish_status"] == "contract_failed" + assert publish_payload["release_final_closure_publish_decision"] == "abort_publish" + assert publish_payload["release_final_closure_publish_exit_code"] == 1 + assert "final_closure_decision_mismatch" in publish_payload["structural_issues"] + assert "final_closure_has_open_follow_up_mismatch" in publish_payload["structural_issues"] + assert "final_closure_target_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_closure_publish_gate_module() + closure_payload = _sample_release_final_closure_payload() + closure_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + closure_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + closure_payload["release_follow_up_final_verdict_exit_code"] = 1 + closure_payload["release_final_outcome_status"] = "released_with_follow_up" + closure_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + closure_payload["release_final_outcome_exit_code"] = 1 + closure_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + closure_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + closure_payload["release_final_terminal_publish_exit_code"] = 1 + closure_payload["release_final_handoff_status"] = "completed_with_follow_up" + closure_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + closure_payload["release_final_handoff_exit_code"] = 1 + closure_payload["release_final_closure_status"] = "closed_with_follow_up" + closure_payload["release_final_closure_decision"] = "close_with_follow_up" + closure_payload["release_final_closure_exit_code"] = 1 + closure_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + closure_payload["final_closure_is_closed"] = True + closure_payload["final_closure_has_open_follow_up"] = True + closure_payload["final_closure_should_page_owner"] = False + closure_payload["final_closure_target"] = "follow_up" + closure_payload["reason_codes"] = ["release_final_closure_closed_with_follow_up"] + + closure_path = tmp_path / "release_final_closure_output.json" + closure_path.write_text(json.dumps(closure_payload, indent=2), encoding="utf-8") + + closure_report = gate.load_release_final_closure_report(closure_path) + publish_payload = gate.build_release_final_closure_publish_payload( + closure_report, + source_path=closure_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_closure_publish.md"), + ) + + assert outputs["workflow_release_final_closure_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_release_final_closure_publish_decision"] + == "announce_release_closed_with_follow_up" + ) + assert outputs["workflow_release_final_closure_publish_exit_code"] == "1" + assert outputs["workflow_release_final_closure_publish_should_notify"] == "true" + assert outputs["workflow_release_final_closure_publish_requires_manual_action"] == "false" + assert outputs["workflow_release_final_closure_publish_channel"] == "follow_up" + assert outputs["workflow_release_final_closure_publish_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_closure_publish_run_id"] == "1234567890" + assert outputs["workflow_release_final_closure_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_closure_publish_report_json"].endswith( + "ci_workflow_release_final_closure_publish.json" + ) + assert outputs["workflow_release_final_closure_publish_report_markdown"].endswith( + "ci_workflow_release_final_closure_publish.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py new file mode 100644 index 0000000..c0231f0 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_handoff_gate_runtime.py @@ -0,0 +1,252 @@ +"""Contract tests for P2-50 Linux CI workflow release final handoff gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_handoff_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_handoff_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_handoff_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_should_ship_release": True, + "final_follow_up_open": False, + "final_should_page_owner": False, + "final_outcome_target": "release", + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "final_terminal_should_notify": True, + "final_terminal_requires_manual_action": False, + "final_terminal_channel": "release", + "release_final_terminal_publish_summary": ( + "release_final_outcome_status=released " + "release_final_terminal_publish_status=published " + "release_final_terminal_publish_decision=announce_release_closure" + ), + "reason_codes": ["release_final_terminal_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"source": "delivery", "path": "/tmp/ci_workflow_release_delivery_final_verdict.json", "exists": True}, + {"source": "follow_up", "path": "/tmp/ci_workflow_release_follow_up_final_verdict.json", "exists": True}, + ], + } + + +def test_build_release_final_handoff_payload_completed(tmp_path: Path): + gate = _load_ci_workflow_release_final_handoff_gate_module() + final_terminal_payload = _sample_release_final_terminal_publish_payload() + + final_terminal_path = tmp_path / "ci_workflow_release_final_terminal_publish.json" + final_terminal_path.write_text(json.dumps(final_terminal_payload, indent=2), encoding="utf-8") + + final_terminal_report = gate.load_release_final_terminal_publish_report(final_terminal_path) + handoff_payload = gate.build_release_final_handoff_payload( + final_terminal_report, + source_path=final_terminal_path.resolve(), + ) + + assert handoff_payload["release_final_handoff_status"] == "completed" + assert handoff_payload["release_final_handoff_decision"] == "handoff_release_closure" + assert handoff_payload["release_final_handoff_exit_code"] == 0 + assert handoff_payload["final_handoff_should_archive_release"] is True + assert handoff_payload["final_handoff_keep_follow_up_open"] is False + assert handoff_payload["final_handoff_should_page_owner"] is False + assert handoff_payload["final_handoff_target"] == "release" + assert "release_final_handoff_completed" in handoff_payload["reason_codes"] + + +def test_build_release_final_handoff_payload_completed_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_handoff_gate_module() + final_terminal_payload = _sample_release_final_terminal_publish_payload() + final_terminal_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + final_terminal_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + final_terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + final_terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_terminal_payload["release_final_outcome_status"] = "released_with_follow_up" + final_terminal_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + final_terminal_payload["release_final_outcome_exit_code"] = 1 + final_terminal_payload["final_should_ship_release"] = True + final_terminal_payload["final_follow_up_open"] = True + final_terminal_payload["final_should_page_owner"] = False + final_terminal_payload["final_outcome_target"] = "follow_up" + final_terminal_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + final_terminal_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + final_terminal_payload["release_final_terminal_publish_exit_code"] = 1 + final_terminal_payload["final_terminal_should_notify"] = True + final_terminal_payload["final_terminal_requires_manual_action"] = False + final_terminal_payload["final_terminal_channel"] = "follow_up" + final_terminal_payload["reason_codes"] = ["release_final_terminal_published_with_follow_up"] + + final_terminal_path = tmp_path / "release_final_terminal_publish_with_follow_up.json" + final_terminal_path.write_text(json.dumps(final_terminal_payload, indent=2), encoding="utf-8") + + final_terminal_report = gate.load_release_final_terminal_publish_report(final_terminal_path) + handoff_payload = gate.build_release_final_handoff_payload( + final_terminal_report, + source_path=final_terminal_path.resolve(), + ) + + assert handoff_payload["release_final_handoff_status"] == "completed_with_follow_up" + assert handoff_payload["release_final_handoff_decision"] == "handoff_release_with_follow_up" + assert handoff_payload["release_final_handoff_exit_code"] == 1 + assert handoff_payload["final_handoff_should_archive_release"] is True + assert handoff_payload["final_handoff_keep_follow_up_open"] is True + assert handoff_payload["final_handoff_should_page_owner"] is False + assert handoff_payload["final_handoff_target"] == "follow_up" + assert "release_final_handoff_completed_with_follow_up" in handoff_payload["reason_codes"] + + +def test_build_release_final_handoff_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_handoff_gate_module() + final_terminal_payload = _sample_release_final_terminal_publish_payload() + final_terminal_payload["release_delivery_final_verdict_status"] = "blocked" + final_terminal_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + final_terminal_payload["release_delivery_final_verdict_exit_code"] = 1 + final_terminal_payload["release_follow_up_final_verdict_status"] = "blocked" + final_terminal_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + final_terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + final_terminal_payload["release_final_outcome_status"] = "blocked" + final_terminal_payload["release_final_outcome_decision"] = "escalate_blocker" + final_terminal_payload["release_final_outcome_exit_code"] = 1 + final_terminal_payload["final_should_ship_release"] = False + final_terminal_payload["final_follow_up_open"] = True + final_terminal_payload["final_should_page_owner"] = True + final_terminal_payload["final_outcome_target"] = "blocker" + final_terminal_payload["release_final_terminal_publish_status"] = "blocked" + final_terminal_payload["release_final_terminal_publish_decision"] = "announce_blocker" + final_terminal_payload["release_final_terminal_publish_exit_code"] = 1 + final_terminal_payload["final_terminal_should_notify"] = True + final_terminal_payload["final_terminal_requires_manual_action"] = True + final_terminal_payload["final_terminal_channel"] = "blocker" + final_terminal_payload["reason_codes"] = ["release_final_terminal_blocked"] + + final_terminal_path = tmp_path / "release_final_terminal_publish_blocked.json" + final_terminal_path.write_text(json.dumps(final_terminal_payload, indent=2), encoding="utf-8") + + final_terminal_report = gate.load_release_final_terminal_publish_report(final_terminal_path) + handoff_payload = gate.build_release_final_handoff_payload( + final_terminal_report, + source_path=final_terminal_path.resolve(), + ) + + assert handoff_payload["release_final_handoff_status"] == "blocked" + assert handoff_payload["release_final_handoff_decision"] == "handoff_blocker" + assert handoff_payload["release_final_handoff_exit_code"] == 1 + assert handoff_payload["final_handoff_should_archive_release"] is False + assert handoff_payload["final_handoff_keep_follow_up_open"] is True + assert handoff_payload["final_handoff_should_page_owner"] is True + assert handoff_payload["final_handoff_target"] == "blocker" + assert "release_final_handoff_blocked" in handoff_payload["reason_codes"] + + +def test_build_release_final_handoff_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_handoff_gate_module() + final_terminal_payload = _sample_release_final_terminal_publish_payload() + final_terminal_payload["release_final_terminal_publish_status"] = "published" + final_terminal_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + final_terminal_payload["final_follow_up_open"] = True + final_terminal_payload["final_terminal_channel"] = "follow_up" + + final_terminal_path = tmp_path / "release_final_terminal_publish_mismatch.json" + final_terminal_path.write_text(json.dumps(final_terminal_payload, indent=2), encoding="utf-8") + + final_terminal_report = gate.load_release_final_terminal_publish_report(final_terminal_path) + handoff_payload = gate.build_release_final_handoff_payload( + final_terminal_report, + source_path=final_terminal_path.resolve(), + ) + + assert handoff_payload["release_final_handoff_status"] == "contract_failed" + assert handoff_payload["release_final_handoff_decision"] == "abort_handoff" + assert handoff_payload["release_final_handoff_exit_code"] == 1 + assert "final_terminal_decision_mismatch" in handoff_payload["structural_issues"] + assert "final_follow_up_open_mismatch" in handoff_payload["structural_issues"] + assert "final_terminal_channel_mismatch" in handoff_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_handoff_gate_module() + final_terminal_payload = _sample_release_final_terminal_publish_payload() + final_terminal_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + final_terminal_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + final_terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + final_terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_terminal_payload["release_final_outcome_status"] = "released_with_follow_up" + final_terminal_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + final_terminal_payload["release_final_outcome_exit_code"] = 1 + final_terminal_payload["final_should_ship_release"] = True + final_terminal_payload["final_follow_up_open"] = True + final_terminal_payload["final_should_page_owner"] = False + final_terminal_payload["final_outcome_target"] = "follow_up" + final_terminal_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + final_terminal_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + final_terminal_payload["release_final_terminal_publish_exit_code"] = 1 + final_terminal_payload["final_terminal_should_notify"] = True + final_terminal_payload["final_terminal_requires_manual_action"] = False + final_terminal_payload["final_terminal_channel"] = "follow_up" + final_terminal_payload["reason_codes"] = ["release_final_terminal_published_with_follow_up"] + + final_terminal_path = tmp_path / "release_final_terminal_publish_output.json" + final_terminal_path.write_text(json.dumps(final_terminal_payload, indent=2), encoding="utf-8") + + final_terminal_report = gate.load_release_final_terminal_publish_report(final_terminal_path) + handoff_payload = gate.build_release_final_handoff_payload( + final_terminal_report, + source_path=final_terminal_path.resolve(), + ) + outputs = gate.build_github_output_values( + handoff_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_handoff.md"), + ) + + assert outputs["workflow_release_final_handoff_status"] == "completed_with_follow_up" + assert ( + outputs["workflow_release_final_handoff_decision"] + == "handoff_release_with_follow_up" + ) + assert outputs["workflow_release_final_handoff_exit_code"] == "1" + assert outputs["workflow_release_final_handoff_should_archive_release"] == "true" + assert outputs["workflow_release_final_handoff_keep_follow_up_open"] == "true" + assert outputs["workflow_release_final_handoff_should_page_owner"] == "false" + assert outputs["workflow_release_final_handoff_target"] == "follow_up" + assert outputs["workflow_release_final_handoff_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_handoff_run_id"] == "1234567890" + assert outputs["workflow_release_final_handoff_run_url"].endswith("/actions/runs/1234567890") + assert outputs["workflow_release_final_handoff_report_json"].endswith( + "ci_workflow_release_final_handoff.json" + ) + assert outputs["workflow_release_final_handoff_report_markdown"].endswith( + "ci_workflow_release_final_handoff.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py new file mode 100644 index 0000000..0001d3b --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_outcome_gate_runtime.py @@ -0,0 +1,266 @@ +"""Contract tests for P2-48 Linux CI workflow release final outcome gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_outcome_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_outcome_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_outcome_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_delivery_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_delivery_terminal_publish_report": "/tmp/ci_workflow_release_delivery_terminal_publish.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "final_should_close_release": True, + "final_should_open_follow_up": False, + "final_should_page_owner": False, + "final_announcement_target": "release", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "reason_codes": ["release_delivery_final_completed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_delivery_terminal_publish.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_delivery.json", "exists": True}, + ], + } + + +def _sample_release_follow_up_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_follow_up_terminal_publish_report": "/tmp/ci_workflow_release_follow_up_terminal_publish.json", + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "final_should_close_follow_up": True, + "final_should_open_follow_up": False, + "final_should_page_owner": False, + "final_announcement_target": "release", + "follow_up_queue_url": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "reason_codes": ["release_follow_up_final_completed_closed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_follow_up_terminal_publish.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_follow_up_closure.json", "exists": True}, + ], + } + + +def test_build_release_final_outcome_payload_released(tmp_path: Path): + gate = _load_ci_workflow_release_final_outcome_gate_module() + delivery_payload = _sample_release_delivery_final_verdict_payload() + follow_payload = _sample_release_follow_up_final_verdict_payload() + + delivery_path = tmp_path / "ci_workflow_release_delivery_final_verdict.json" + follow_path = tmp_path / "ci_workflow_release_follow_up_final_verdict.json" + delivery_path.write_text(json.dumps(delivery_payload, indent=2), encoding="utf-8") + follow_path.write_text(json.dumps(follow_payload, indent=2), encoding="utf-8") + + delivery_report = gate.load_release_delivery_final_verdict_report(delivery_path) + follow_report = gate.load_release_follow_up_final_verdict_report(follow_path) + final_payload = gate.build_release_final_outcome_payload( + delivery_report, + follow_report, + delivery_source_path=delivery_path.resolve(), + follow_up_source_path=follow_path.resolve(), + ) + + assert final_payload["release_final_outcome_status"] == "released" + assert final_payload["release_final_outcome_decision"] == "ship_and_close" + assert final_payload["release_final_outcome_exit_code"] == 0 + assert final_payload["final_should_ship_release"] is True + assert final_payload["final_follow_up_open"] is False + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_outcome_target"] == "release" + assert "release_final_outcome_released" in final_payload["reason_codes"] + + +def test_build_release_final_outcome_payload_released_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_outcome_gate_module() + delivery_payload = _sample_release_delivery_final_verdict_payload() + follow_payload = _sample_release_follow_up_final_verdict_payload() + follow_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + follow_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + follow_payload["release_follow_up_final_verdict_exit_code"] = 1 + follow_payload["final_should_close_follow_up"] = False + follow_payload["final_should_open_follow_up"] = True + follow_payload["final_should_page_owner"] = False + follow_payload["final_announcement_target"] = "follow_up" + follow_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + follow_payload["reason_codes"] = ["release_follow_up_final_pending_queue"] + + delivery_path = tmp_path / "delivery_requires_follow_up.json" + follow_path = tmp_path / "follow_up_requires_follow_up.json" + delivery_path.write_text(json.dumps(delivery_payload, indent=2), encoding="utf-8") + follow_path.write_text(json.dumps(follow_payload, indent=2), encoding="utf-8") + + delivery_report = gate.load_release_delivery_final_verdict_report(delivery_path) + follow_report = gate.load_release_follow_up_final_verdict_report(follow_path) + final_payload = gate.build_release_final_outcome_payload( + delivery_report, + follow_report, + delivery_source_path=delivery_path.resolve(), + follow_up_source_path=follow_path.resolve(), + ) + + assert final_payload["release_final_outcome_status"] == "released_with_follow_up" + assert final_payload["release_final_outcome_decision"] == "ship_with_follow_up_open" + assert final_payload["release_final_outcome_exit_code"] == 1 + assert final_payload["final_should_ship_release"] is True + assert final_payload["final_follow_up_open"] is True + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_outcome_target"] == "follow_up" + assert "release_final_outcome_released_with_follow_up" in final_payload["reason_codes"] + + +def test_build_release_final_outcome_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_outcome_gate_module() + delivery_payload = _sample_release_delivery_final_verdict_payload() + follow_payload = _sample_release_follow_up_final_verdict_payload() + delivery_payload["release_delivery_final_verdict_status"] = "blocked" + delivery_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + delivery_payload["release_delivery_final_verdict_exit_code"] = 1 + delivery_payload["final_should_close_release"] = False + delivery_payload["final_should_open_follow_up"] = True + delivery_payload["final_should_page_owner"] = True + delivery_payload["final_announcement_target"] = "blocker" + delivery_payload["reason_codes"] = ["release_delivery_final_blocked"] + follow_payload["release_follow_up_final_verdict_status"] = "blocked" + follow_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + follow_payload["release_follow_up_final_verdict_exit_code"] = 1 + follow_payload["final_should_close_follow_up"] = False + follow_payload["final_should_open_follow_up"] = True + follow_payload["final_should_page_owner"] = True + follow_payload["final_announcement_target"] = "blocker" + follow_payload["reason_codes"] = ["release_follow_up_final_queue_failed"] + + delivery_path = tmp_path / "delivery_blocked.json" + follow_path = tmp_path / "follow_blocked.json" + delivery_path.write_text(json.dumps(delivery_payload, indent=2), encoding="utf-8") + follow_path.write_text(json.dumps(follow_payload, indent=2), encoding="utf-8") + + delivery_report = gate.load_release_delivery_final_verdict_report(delivery_path) + follow_report = gate.load_release_follow_up_final_verdict_report(follow_path) + final_payload = gate.build_release_final_outcome_payload( + delivery_report, + follow_report, + delivery_source_path=delivery_path.resolve(), + follow_up_source_path=follow_path.resolve(), + ) + + assert final_payload["release_final_outcome_status"] == "blocked" + assert final_payload["release_final_outcome_decision"] == "escalate_blocker" + assert final_payload["release_final_outcome_exit_code"] == 1 + assert final_payload["final_should_ship_release"] is False + assert final_payload["final_follow_up_open"] is True + assert final_payload["final_should_page_owner"] is True + assert final_payload["final_outcome_target"] == "blocker" + assert "release_final_outcome_blocked" in final_payload["reason_codes"] + + +def test_build_release_final_outcome_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_outcome_gate_module() + delivery_payload = _sample_release_delivery_final_verdict_payload() + follow_payload = _sample_release_follow_up_final_verdict_payload() + delivery_payload["release_delivery_final_verdict_status"] = "completed" + delivery_payload["release_delivery_final_verdict_decision"] = "open_follow_up" + delivery_payload["final_should_close_release"] = False + follow_payload["release_follow_up_final_verdict_status"] = "completed" + follow_payload["release_follow_up_final_verdict_decision"] = "close_follow_up" + follow_payload["release_run_id"] = 99887766 + follow_payload["release_run_url"] = "https://github.com/acme/demo/actions/runs/99887766" + + delivery_path = tmp_path / "delivery_mismatch.json" + follow_path = tmp_path / "follow_mismatch.json" + delivery_path.write_text(json.dumps(delivery_payload, indent=2), encoding="utf-8") + follow_path.write_text(json.dumps(follow_payload, indent=2), encoding="utf-8") + + delivery_report = gate.load_release_delivery_final_verdict_report(delivery_path) + follow_report = gate.load_release_follow_up_final_verdict_report(follow_path) + final_payload = gate.build_release_final_outcome_payload( + delivery_report, + follow_report, + delivery_source_path=delivery_path.resolve(), + follow_up_source_path=follow_path.resolve(), + ) + + assert final_payload["release_final_outcome_status"] == "contract_failed" + assert final_payload["release_final_outcome_decision"] == "abort_outcome" + assert final_payload["release_final_outcome_exit_code"] == 1 + assert "delivery_decision_mismatch" in final_payload["structural_issues"] + assert "release_run_id_mismatch" in final_payload["structural_issues"] + assert "release_run_url_mismatch" in final_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_outcome_gate_module() + delivery_payload = _sample_release_delivery_final_verdict_payload() + follow_payload = _sample_release_follow_up_final_verdict_payload() + follow_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + follow_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + follow_payload["release_follow_up_final_verdict_exit_code"] = 1 + follow_payload["final_should_close_follow_up"] = False + follow_payload["final_should_open_follow_up"] = True + follow_payload["final_should_page_owner"] = False + follow_payload["final_announcement_target"] = "follow_up" + follow_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + follow_payload["reason_codes"] = ["release_follow_up_final_pending_queue"] + + delivery_path = tmp_path / "delivery_output.json" + follow_path = tmp_path / "follow_output.json" + delivery_path.write_text(json.dumps(delivery_payload, indent=2), encoding="utf-8") + follow_path.write_text(json.dumps(follow_payload, indent=2), encoding="utf-8") + + delivery_report = gate.load_release_delivery_final_verdict_report(delivery_path) + follow_report = gate.load_release_follow_up_final_verdict_report(follow_path) + final_payload = gate.build_release_final_outcome_payload( + delivery_report, + follow_report, + delivery_source_path=delivery_path.resolve(), + follow_up_source_path=follow_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_outcome.md"), + ) + + assert outputs["workflow_release_final_outcome_status"] == "released_with_follow_up" + assert outputs["workflow_release_final_outcome_decision"] == "ship_with_follow_up_open" + assert outputs["workflow_release_final_outcome_exit_code"] == "1" + assert outputs["workflow_release_final_should_ship_release"] == "true" + assert outputs["workflow_release_final_follow_up_open"] == "true" + assert outputs["workflow_release_final_should_page_owner"] == "false" + assert outputs["workflow_release_final_target"] == "follow_up" + assert outputs["workflow_release_final_delivery_status"] == "completed" + assert outputs["workflow_release_final_follow_up_status"] == "requires_follow_up" + assert outputs["workflow_release_final_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_run_id"] == "1234567890" + assert outputs["workflow_release_final_run_url"].endswith("/actions/runs/1234567890") + assert outputs["workflow_release_final_report_json"].endswith( + "ci_workflow_release_final_outcome.json" + ) + assert outputs["workflow_release_final_report_markdown"].endswith( + "ci_workflow_release_final_outcome.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py new file mode 100644 index 0000000..149af11 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_publish_archive_gate_runtime.py @@ -0,0 +1,346 @@ +"""Contract tests for P2-56 Linux CI workflow release final publish archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_publish_archive_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_final_publish_archive_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_publish_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_verdict_report": "/tmp/ci_workflow_release_final_verdict.json", + "source_release_final_archive_report": "/tmp/ci_workflow_release_final_archive.json", + "source_release_final_closure_publish_report": "/tmp/ci_workflow_release_final_closure_publish.json", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_final_archive_status": "archived", + "release_final_archive_decision": "archive_release_closed", + "release_final_archive_exit_code": 0, + "release_final_verdict_status": "released", + "release_final_verdict_decision": "ship_release", + "release_final_verdict_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_verdict_publish_should_notify": True, + "final_verdict_publish_requires_manual_action": False, + "final_verdict_publish_channel": "release", + "release_final_verdict_publish_status": "published", + "release_final_verdict_publish_decision": "announce_release_shipped", + "release_final_verdict_publish_exit_code": 0, + "release_final_verdict_publish_summary": ( + "release_final_verdict_status=released " + "release_final_verdict_publish_status=published " + "release_final_verdict_publish_decision=announce_release_shipped" + ), + "reason_codes": ["release_final_verdict_publish_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_archive", + "path": "/tmp/ci_workflow_release_final_archive.json", + "exists": True, + }, + { + "source": "final_closure_publish", + "path": "/tmp/ci_workflow_release_final_closure_publish.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_publish_archive_payload_archived(tmp_path: Path): + gate = _load_ci_workflow_release_final_publish_archive_gate_module() + publish_payload = _sample_release_final_verdict_publish_payload() + + publish_path = tmp_path / "ci_workflow_release_final_verdict_publish.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_verdict_publish_report(publish_path) + archive_payload = gate.build_release_final_publish_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_publish_archive_status"] == "archived" + assert archive_payload["release_final_publish_archive_decision"] == "archive_release_shipped" + assert archive_payload["release_final_publish_archive_exit_code"] == 0 + assert archive_payload["final_publish_archive_should_archive"] is True + assert archive_payload["final_publish_archive_requires_manual_action"] is False + assert archive_payload["final_publish_archive_channel"] == "release" + assert "release_final_publish_archive_archived" in archive_payload["reason_codes"] + + +def test_build_release_final_publish_archive_payload_archived_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_publish_archive_gate_module() + publish_payload = _sample_release_final_verdict_publish_payload() + publish_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["release_final_archive_status"] = "archived_with_follow_up" + publish_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + publish_payload["release_final_archive_exit_code"] = 1 + publish_payload["release_final_verdict_status"] = "released_with_follow_up" + publish_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + publish_payload["release_final_verdict_exit_code"] = 1 + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["final_verdict_publish_should_notify"] = True + publish_payload["final_verdict_publish_requires_manual_action"] = False + publish_payload["final_verdict_publish_channel"] = "follow_up" + publish_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + publish_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + publish_payload["release_final_verdict_publish_exit_code"] = 1 + publish_payload["reason_codes"] = ["release_final_verdict_publish_with_follow_up"] + + publish_path = tmp_path / "release_final_verdict_publish_with_follow_up.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_verdict_publish_report(publish_path) + archive_payload = gate.build_release_final_publish_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_publish_archive_status"] == "archived_with_follow_up" + assert ( + archive_payload["release_final_publish_archive_decision"] + == "archive_release_shipped_with_follow_up" + ) + assert archive_payload["release_final_publish_archive_exit_code"] == 1 + assert archive_payload["final_publish_archive_should_archive"] is True + assert archive_payload["final_publish_archive_requires_manual_action"] is False + assert archive_payload["final_publish_archive_channel"] == "follow_up" + assert "release_final_publish_archive_archived_with_follow_up" in archive_payload["reason_codes"] + + +def test_build_release_final_publish_archive_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_publish_archive_gate_module() + publish_payload = _sample_release_final_verdict_publish_payload() + publish_payload["release_delivery_final_verdict_status"] = "blocked" + publish_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + publish_payload["release_delivery_final_verdict_exit_code"] = 1 + publish_payload["release_follow_up_final_verdict_status"] = "blocked" + publish_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "blocked" + publish_payload["release_final_outcome_decision"] = "escalate_blocker" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "blocked" + publish_payload["release_final_terminal_publish_decision"] = "announce_blocker" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "blocked" + publish_payload["release_final_handoff_decision"] = "handoff_blocker" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "blocked" + publish_payload["release_final_closure_decision"] = "close_blocker" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "blocked" + publish_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["release_final_archive_status"] = "blocked" + publish_payload["release_final_archive_decision"] = "archive_release_blocker" + publish_payload["release_final_archive_exit_code"] = 1 + publish_payload["release_final_verdict_status"] = "blocked" + publish_payload["release_final_verdict_decision"] = "escalate_release_blocker" + publish_payload["release_final_verdict_exit_code"] = 1 + publish_payload["final_verdict_publish_should_notify"] = True + publish_payload["final_verdict_publish_requires_manual_action"] = True + publish_payload["final_verdict_publish_channel"] = "blocker" + publish_payload["release_final_verdict_publish_status"] = "blocked" + publish_payload["release_final_verdict_publish_decision"] = "announce_release_blocker" + publish_payload["release_final_verdict_publish_exit_code"] = 1 + publish_payload["reason_codes"] = ["release_final_verdict_publish_blocked"] + + publish_path = tmp_path / "release_final_verdict_publish_blocked.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_verdict_publish_report(publish_path) + archive_payload = gate.build_release_final_publish_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_publish_archive_status"] == "blocked" + assert archive_payload["release_final_publish_archive_decision"] == "archive_release_blocker" + assert archive_payload["release_final_publish_archive_exit_code"] == 1 + assert archive_payload["final_publish_archive_should_archive"] is False + assert archive_payload["final_publish_archive_requires_manual_action"] is True + assert archive_payload["final_publish_archive_channel"] == "blocker" + assert "release_final_publish_archive_blocked" in archive_payload["reason_codes"] + + +def test_build_release_final_publish_archive_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_release_final_publish_archive_gate_module() + publish_payload = _sample_release_final_verdict_publish_payload() + publish_payload["release_final_verdict_publish_status"] = "published" + publish_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + publish_payload["final_verdict_publish_requires_manual_action"] = True + publish_payload["final_verdict_publish_channel"] = "follow_up" + + publish_path = tmp_path / "release_final_verdict_publish_mismatch.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_verdict_publish_report(publish_path) + archive_payload = gate.build_release_final_publish_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert archive_payload["release_final_publish_archive_status"] == "contract_failed" + assert archive_payload["release_final_publish_archive_decision"] == "abort_archive" + assert archive_payload["release_final_publish_archive_exit_code"] == 1 + assert "final_verdict_publish_decision_mismatch" in archive_payload["structural_issues"] + assert ( + "final_verdict_publish_requires_manual_action_mismatch" + in archive_payload["structural_issues"] + ) + assert "final_verdict_publish_channel_mismatch" in archive_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_publish_archive_gate_module() + publish_payload = _sample_release_final_verdict_publish_payload() + publish_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_payload["release_final_outcome_exit_code"] = 1 + publish_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + publish_payload["release_final_terminal_publish_exit_code"] = 1 + publish_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_payload["release_final_handoff_exit_code"] = 1 + publish_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_payload["release_final_closure_exit_code"] = 1 + publish_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_payload["release_final_closure_publish_exit_code"] = 1 + publish_payload["release_final_archive_status"] = "archived_with_follow_up" + publish_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + publish_payload["release_final_archive_exit_code"] = 1 + publish_payload["release_final_verdict_status"] = "released_with_follow_up" + publish_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + publish_payload["release_final_verdict_exit_code"] = 1 + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["final_verdict_publish_should_notify"] = True + publish_payload["final_verdict_publish_requires_manual_action"] = False + publish_payload["final_verdict_publish_channel"] = "follow_up" + publish_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + publish_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + publish_payload["release_final_verdict_publish_exit_code"] = 1 + publish_payload["reason_codes"] = ["release_final_verdict_publish_with_follow_up"] + + publish_path = tmp_path / "release_final_verdict_publish_output.json" + publish_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_release_final_verdict_publish_report(publish_path) + archive_payload = gate.build_release_final_publish_archive_payload( + publish_report, + source_path=publish_path.resolve(), + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_publish_archive.md" + ), + ) + + assert outputs["workflow_release_final_publish_archive_status"] == "archived_with_follow_up" + assert ( + outputs["workflow_release_final_publish_archive_decision"] + == "archive_release_shipped_with_follow_up" + ) + assert outputs["workflow_release_final_publish_archive_exit_code"] == "1" + assert outputs["workflow_release_final_publish_archive_should_archive"] == "true" + assert outputs["workflow_release_final_publish_archive_requires_manual_action"] == "false" + assert outputs["workflow_release_final_publish_archive_channel"] == "follow_up" + assert outputs["workflow_release_final_publish_archive_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_release_final_publish_archive_run_id"] == "1234567890" + assert outputs["workflow_release_final_publish_archive_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_publish_archive_report_json"].endswith( + "ci_workflow_release_final_publish_archive.json" + ) + assert outputs["workflow_release_final_publish_archive_report_markdown"].endswith( + "ci_workflow_release_final_publish_archive.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py new file mode 100644 index 0000000..ae106aa --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_terminal_publish_gate_runtime.py @@ -0,0 +1,238 @@ +"""Contract tests for P2-49 Linux CI workflow release final terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_terminal_publish_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_final_terminal_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_outcome_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "final_should_ship_release": True, + "final_follow_up_open": False, + "final_should_page_owner": False, + "final_outcome_target": "release", + "release_final_outcome_summary": ( + "release_delivery_final_verdict_status=completed " + "release_follow_up_final_verdict_status=completed " + "release_final_outcome_status=released " + "release_final_outcome_decision=ship_and_close" + ), + "reason_codes": ["release_final_outcome_released"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"source": "delivery", "path": "/tmp/ci_workflow_release_delivery_final_verdict.json", "exists": True}, + {"source": "follow_up", "path": "/tmp/ci_workflow_release_follow_up_final_verdict.json", "exists": True}, + ], + } + + +def test_build_release_final_terminal_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_release_final_terminal_publish_gate_module() + outcome_payload = _sample_release_final_outcome_payload() + + outcome_path = tmp_path / "ci_workflow_release_final_outcome.json" + outcome_path.write_text(json.dumps(outcome_payload, indent=2), encoding="utf-8") + + outcome_report = gate.load_release_final_outcome_report(outcome_path) + final_payload = gate.build_release_final_terminal_publish_payload( + outcome_report, + source_path=outcome_path.resolve(), + ) + + assert final_payload["release_final_terminal_publish_status"] == "published" + assert final_payload["release_final_terminal_publish_decision"] == "announce_release_closure" + assert final_payload["release_final_terminal_publish_exit_code"] == 0 + assert final_payload["final_terminal_should_notify"] is True + assert final_payload["final_terminal_requires_manual_action"] is False + assert final_payload["final_terminal_channel"] == "release" + assert "release_final_terminal_published" in final_payload["reason_codes"] + + +def test_build_release_final_terminal_publish_payload_published_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_terminal_publish_gate_module() + outcome_payload = _sample_release_final_outcome_payload() + outcome_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + outcome_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + outcome_payload["release_follow_up_final_verdict_exit_code"] = 1 + outcome_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + outcome_payload["release_final_outcome_status"] = "released_with_follow_up" + outcome_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + outcome_payload["release_final_outcome_exit_code"] = 1 + outcome_payload["final_should_ship_release"] = True + outcome_payload["final_follow_up_open"] = True + outcome_payload["final_should_page_owner"] = False + outcome_payload["final_outcome_target"] = "follow_up" + outcome_payload["reason_codes"] = ["release_final_outcome_released_with_follow_up"] + + outcome_path = tmp_path / "release_final_outcome_with_follow_up.json" + outcome_path.write_text(json.dumps(outcome_payload, indent=2), encoding="utf-8") + + outcome_report = gate.load_release_final_outcome_report(outcome_path) + final_payload = gate.build_release_final_terminal_publish_payload( + outcome_report, + source_path=outcome_path.resolve(), + ) + + assert final_payload["release_final_terminal_publish_status"] == "published_with_follow_up" + assert ( + final_payload["release_final_terminal_publish_decision"] + == "announce_release_with_follow_up" + ) + assert final_payload["release_final_terminal_publish_exit_code"] == 1 + assert final_payload["final_terminal_should_notify"] is True + assert final_payload["final_terminal_requires_manual_action"] is False + assert final_payload["final_terminal_channel"] == "follow_up" + assert "release_final_terminal_published_with_follow_up" in final_payload["reason_codes"] + + +def test_build_release_final_terminal_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_terminal_publish_gate_module() + outcome_payload = _sample_release_final_outcome_payload() + outcome_payload["release_delivery_final_verdict_status"] = "blocked" + outcome_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + outcome_payload["release_delivery_final_verdict_exit_code"] = 1 + outcome_payload["release_follow_up_final_verdict_status"] = "blocked" + outcome_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + outcome_payload["release_follow_up_final_verdict_exit_code"] = 1 + outcome_payload["release_final_outcome_status"] = "blocked" + outcome_payload["release_final_outcome_decision"] = "escalate_blocker" + outcome_payload["release_final_outcome_exit_code"] = 1 + outcome_payload["final_should_ship_release"] = False + outcome_payload["final_follow_up_open"] = True + outcome_payload["final_should_page_owner"] = True + outcome_payload["final_outcome_target"] = "blocker" + outcome_payload["reason_codes"] = ["release_final_outcome_blocked"] + + outcome_path = tmp_path / "release_final_outcome_blocked.json" + outcome_path.write_text(json.dumps(outcome_payload, indent=2), encoding="utf-8") + + outcome_report = gate.load_release_final_outcome_report(outcome_path) + final_payload = gate.build_release_final_terminal_publish_payload( + outcome_report, + source_path=outcome_path.resolve(), + ) + + assert final_payload["release_final_terminal_publish_status"] == "blocked" + assert final_payload["release_final_terminal_publish_decision"] == "announce_blocker" + assert final_payload["release_final_terminal_publish_exit_code"] == 1 + assert final_payload["final_terminal_should_notify"] is True + assert final_payload["final_terminal_requires_manual_action"] is True + assert final_payload["final_terminal_channel"] == "blocker" + assert "release_final_terminal_blocked" in final_payload["reason_codes"] + + +def test_build_release_final_terminal_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_release_final_terminal_publish_gate_module() + outcome_payload = _sample_release_final_outcome_payload() + outcome_payload["release_final_outcome_status"] = "released" + outcome_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + outcome_payload["final_follow_up_open"] = True + outcome_payload["final_outcome_target"] = "follow_up" + + outcome_path = tmp_path / "release_final_outcome_mismatch.json" + outcome_path.write_text(json.dumps(outcome_payload, indent=2), encoding="utf-8") + + outcome_report = gate.load_release_final_outcome_report(outcome_path) + final_payload = gate.build_release_final_terminal_publish_payload( + outcome_report, + source_path=outcome_path.resolve(), + ) + + assert final_payload["release_final_terminal_publish_status"] == "contract_failed" + assert final_payload["release_final_terminal_publish_decision"] == "abort_publish" + assert final_payload["release_final_terminal_publish_exit_code"] == 1 + assert "final_outcome_decision_mismatch" in final_payload["structural_issues"] + assert "final_follow_up_open_mismatch" in final_payload["structural_issues"] + assert "final_outcome_target_mismatch" in final_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_terminal_publish_gate_module() + outcome_payload = _sample_release_final_outcome_payload() + outcome_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + outcome_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + outcome_payload["release_follow_up_final_verdict_exit_code"] = 1 + outcome_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + outcome_payload["release_final_outcome_status"] = "released_with_follow_up" + outcome_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + outcome_payload["release_final_outcome_exit_code"] = 1 + outcome_payload["final_should_ship_release"] = True + outcome_payload["final_follow_up_open"] = True + outcome_payload["final_should_page_owner"] = False + outcome_payload["final_outcome_target"] = "follow_up" + outcome_payload["reason_codes"] = ["release_final_outcome_released_with_follow_up"] + + outcome_path = tmp_path / "release_final_outcome_output.json" + outcome_path.write_text(json.dumps(outcome_payload, indent=2), encoding="utf-8") + + outcome_report = gate.load_release_final_outcome_report(outcome_path) + final_payload = gate.build_release_final_terminal_publish_payload( + outcome_report, + source_path=outcome_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_terminal_publish.md" + ), + ) + + assert outputs["workflow_release_final_terminal_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_release_final_terminal_publish_decision"] + == "announce_release_with_follow_up" + ) + assert outputs["workflow_release_final_terminal_publish_exit_code"] == "1" + assert outputs["workflow_release_final_terminal_should_notify"] == "true" + assert outputs["workflow_release_final_terminal_requires_manual_action"] == "false" + assert outputs["workflow_release_final_terminal_channel"] == "follow_up" + assert outputs["workflow_release_final_terminal_outcome_status"] == "released_with_follow_up" + assert outputs["workflow_release_final_terminal_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_terminal_run_id"] == "1234567890" + assert outputs["workflow_release_final_terminal_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_terminal_report_json"].endswith( + "ci_workflow_release_final_terminal_publish.json" + ) + assert outputs["workflow_release_final_terminal_report_markdown"].endswith( + "ci_workflow_release_final_terminal_publish.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py new file mode 100644 index 0000000..1a6bb57 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_verdict_gate_runtime.py @@ -0,0 +1,301 @@ +"""Contract tests for P2-54 Linux CI workflow release final verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_verdict_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_final_verdict_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_archive_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_closure_publish_report": "/tmp/ci_workflow_release_final_closure_publish.json", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_final_archive_status": "archived", + "release_final_archive_decision": "archive_release_closed", + "release_final_archive_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_archive_should_publish": True, + "final_archive_requires_manual_action": False, + "final_archive_channel": "release", + "release_final_archive_summary": ( + "release_final_closure_publish_status=published " + "release_final_archive_status=archived " + "release_final_archive_decision=archive_release_closed" + ), + "reason_codes": ["release_final_archive_archived"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_closure_publish", + "path": "/tmp/ci_workflow_release_final_closure_publish.json", + "exists": True, + }, + { + "source": "final_closure", + "path": "/tmp/ci_workflow_release_final_closure.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_verdict_payload_released(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_gate_module() + archive_payload = _sample_release_final_archive_payload() + + archive_path = tmp_path / "ci_workflow_release_final_archive.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_release_final_archive_report(archive_path) + verdict_payload = gate.build_release_final_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert verdict_payload["release_final_verdict_status"] == "released" + assert verdict_payload["release_final_verdict_decision"] == "ship_release" + assert verdict_payload["release_final_verdict_exit_code"] == 0 + assert verdict_payload["final_verdict_should_ship"] is True + assert verdict_payload["final_verdict_requires_manual_action"] is False + assert verdict_payload["final_verdict_channel"] == "release" + assert "release_final_verdict_released" in verdict_payload["reason_codes"] + + +def test_build_release_final_verdict_payload_released_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_gate_module() + archive_payload = _sample_release_final_archive_payload() + archive_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + archive_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + archive_payload["release_final_outcome_status"] = "released_with_follow_up" + archive_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + archive_payload["release_final_outcome_exit_code"] = 1 + archive_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + archive_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + archive_payload["release_final_terminal_publish_exit_code"] = 1 + archive_payload["release_final_handoff_status"] = "completed_with_follow_up" + archive_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + archive_payload["release_final_handoff_exit_code"] = 1 + archive_payload["release_final_closure_status"] = "closed_with_follow_up" + archive_payload["release_final_closure_decision"] = "close_with_follow_up" + archive_payload["release_final_closure_exit_code"] = 1 + archive_payload["release_final_closure_publish_status"] = "published_with_follow_up" + archive_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + archive_payload["release_final_closure_publish_exit_code"] = 1 + archive_payload["release_final_archive_status"] = "archived_with_follow_up" + archive_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + archive_payload["release_final_archive_exit_code"] = 1 + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["final_archive_should_publish"] = True + archive_payload["final_archive_requires_manual_action"] = False + archive_payload["final_archive_channel"] = "follow_up" + archive_payload["reason_codes"] = ["release_final_archive_archived_with_follow_up"] + + archive_path = tmp_path / "release_final_archive_with_follow_up.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_release_final_archive_report(archive_path) + verdict_payload = gate.build_release_final_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert verdict_payload["release_final_verdict_status"] == "released_with_follow_up" + assert verdict_payload["release_final_verdict_decision"] == "ship_release_with_follow_up" + assert verdict_payload["release_final_verdict_exit_code"] == 1 + assert verdict_payload["final_verdict_should_ship"] is True + assert verdict_payload["final_verdict_requires_manual_action"] is False + assert verdict_payload["final_verdict_channel"] == "follow_up" + assert "release_final_verdict_released_with_follow_up" in verdict_payload["reason_codes"] + + +def test_build_release_final_verdict_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_gate_module() + archive_payload = _sample_release_final_archive_payload() + archive_payload["release_delivery_final_verdict_status"] = "blocked" + archive_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + archive_payload["release_delivery_final_verdict_exit_code"] = 1 + archive_payload["release_follow_up_final_verdict_status"] = "blocked" + archive_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + archive_payload["release_final_outcome_status"] = "blocked" + archive_payload["release_final_outcome_decision"] = "escalate_blocker" + archive_payload["release_final_outcome_exit_code"] = 1 + archive_payload["release_final_terminal_publish_status"] = "blocked" + archive_payload["release_final_terminal_publish_decision"] = "announce_blocker" + archive_payload["release_final_terminal_publish_exit_code"] = 1 + archive_payload["release_final_handoff_status"] = "blocked" + archive_payload["release_final_handoff_decision"] = "handoff_blocker" + archive_payload["release_final_handoff_exit_code"] = 1 + archive_payload["release_final_closure_status"] = "blocked" + archive_payload["release_final_closure_decision"] = "close_blocker" + archive_payload["release_final_closure_exit_code"] = 1 + archive_payload["release_final_closure_publish_status"] = "blocked" + archive_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + archive_payload["release_final_closure_publish_exit_code"] = 1 + archive_payload["release_final_archive_status"] = "blocked" + archive_payload["release_final_archive_decision"] = "archive_release_blocker" + archive_payload["release_final_archive_exit_code"] = 1 + archive_payload["final_archive_should_publish"] = False + archive_payload["final_archive_requires_manual_action"] = True + archive_payload["final_archive_channel"] = "blocker" + archive_payload["reason_codes"] = ["release_final_archive_blocked"] + + archive_path = tmp_path / "release_final_archive_blocked.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_release_final_archive_report(archive_path) + verdict_payload = gate.build_release_final_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert verdict_payload["release_final_verdict_status"] == "blocked" + assert verdict_payload["release_final_verdict_decision"] == "escalate_release_blocker" + assert verdict_payload["release_final_verdict_exit_code"] == 1 + assert verdict_payload["final_verdict_should_ship"] is False + assert verdict_payload["final_verdict_requires_manual_action"] is True + assert verdict_payload["final_verdict_channel"] == "blocker" + assert "release_final_verdict_blocked" in verdict_payload["reason_codes"] + + +def test_build_release_final_verdict_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_gate_module() + archive_payload = _sample_release_final_archive_payload() + archive_payload["release_final_archive_status"] = "archived" + archive_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + archive_payload["final_archive_requires_manual_action"] = True + archive_payload["final_archive_channel"] = "follow_up" + + archive_path = tmp_path / "release_final_archive_mismatch.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_release_final_archive_report(archive_path) + verdict_payload = gate.build_release_final_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert verdict_payload["release_final_verdict_status"] == "contract_failed" + assert verdict_payload["release_final_verdict_decision"] == "abort_verdict" + assert verdict_payload["release_final_verdict_exit_code"] == 1 + assert "final_archive_decision_mismatch" in verdict_payload["structural_issues"] + assert ( + "final_archive_requires_manual_action_mismatch" + in verdict_payload["structural_issues"] + ) + assert "final_archive_channel_mismatch" in verdict_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_gate_module() + archive_payload = _sample_release_final_archive_payload() + archive_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + archive_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + archive_payload["release_final_outcome_status"] = "released_with_follow_up" + archive_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + archive_payload["release_final_outcome_exit_code"] = 1 + archive_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + archive_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + archive_payload["release_final_terminal_publish_exit_code"] = 1 + archive_payload["release_final_handoff_status"] = "completed_with_follow_up" + archive_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + archive_payload["release_final_handoff_exit_code"] = 1 + archive_payload["release_final_closure_status"] = "closed_with_follow_up" + archive_payload["release_final_closure_decision"] = "close_with_follow_up" + archive_payload["release_final_closure_exit_code"] = 1 + archive_payload["release_final_closure_publish_status"] = "published_with_follow_up" + archive_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + archive_payload["release_final_closure_publish_exit_code"] = 1 + archive_payload["release_final_archive_status"] = "archived_with_follow_up" + archive_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + archive_payload["release_final_archive_exit_code"] = 1 + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["final_archive_should_publish"] = True + archive_payload["final_archive_requires_manual_action"] = False + archive_payload["final_archive_channel"] = "follow_up" + archive_payload["reason_codes"] = ["release_final_archive_archived_with_follow_up"] + + archive_path = tmp_path / "release_final_archive_output.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_release_final_archive_report(archive_path) + verdict_payload = gate.build_release_final_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + outputs = gate.build_github_output_values( + verdict_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict.md"), + ) + + assert outputs["workflow_release_final_verdict_status"] == "released_with_follow_up" + assert ( + outputs["workflow_release_final_verdict_decision"] + == "ship_release_with_follow_up" + ) + assert outputs["workflow_release_final_verdict_exit_code"] == "1" + assert outputs["workflow_release_final_verdict_should_ship"] == "true" + assert outputs["workflow_release_final_verdict_requires_manual_action"] == "false" + assert outputs["workflow_release_final_verdict_channel"] == "follow_up" + assert outputs["workflow_release_final_verdict_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_final_verdict_run_id"] == "1234567890" + assert outputs["workflow_release_final_verdict_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_verdict_report_json"].endswith( + "ci_workflow_release_final_verdict.json" + ) + assert outputs["workflow_release_final_verdict_report_markdown"].endswith( + "ci_workflow_release_final_verdict.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py new file mode 100644 index 0000000..cdef3af --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_final_verdict_publish_gate_runtime.py @@ -0,0 +1,325 @@ +"""Contract tests for P2-55 Linux CI workflow release final verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_final_verdict_publish_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_final_verdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_final_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_archive_report": "/tmp/ci_workflow_release_final_archive.json", + "source_release_final_closure_publish_report": "/tmp/ci_workflow_release_final_closure_publish.json", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_final_archive_status": "archived", + "release_final_archive_decision": "archive_release_closed", + "release_final_archive_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_verdict_should_ship": True, + "final_verdict_requires_manual_action": False, + "final_verdict_channel": "release", + "release_final_verdict_status": "released", + "release_final_verdict_decision": "ship_release", + "release_final_verdict_exit_code": 0, + "release_final_verdict_summary": ( + "release_final_archive_status=archived " + "release_final_verdict_status=released " + "release_final_verdict_decision=ship_release" + ), + "reason_codes": ["release_final_verdict_released"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_archive", + "path": "/tmp/ci_workflow_release_final_archive.json", + "exists": True, + }, + { + "source": "final_closure_publish", + "path": "/tmp/ci_workflow_release_final_closure_publish.json", + "exists": True, + }, + ], + } + + +def test_build_release_final_verdict_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_publish_gate_module() + verdict_payload = _sample_release_final_verdict_payload() + + verdict_path = tmp_path / "ci_workflow_release_final_verdict.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_release_final_verdict_report(verdict_path) + publish_payload = gate.build_release_final_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["release_final_verdict_publish_status"] == "published" + assert publish_payload["release_final_verdict_publish_decision"] == "announce_release_shipped" + assert publish_payload["release_final_verdict_publish_exit_code"] == 0 + assert publish_payload["final_verdict_publish_should_notify"] is True + assert publish_payload["final_verdict_publish_requires_manual_action"] is False + assert publish_payload["final_verdict_publish_channel"] == "release" + assert "release_final_verdict_publish_published" in publish_payload["reason_codes"] + + +def test_build_release_final_verdict_publish_payload_published_with_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_publish_gate_module() + verdict_payload = _sample_release_final_verdict_payload() + verdict_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + verdict_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + verdict_payload["release_follow_up_final_verdict_exit_code"] = 1 + verdict_payload["release_final_outcome_status"] = "released_with_follow_up" + verdict_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + verdict_payload["release_final_outcome_exit_code"] = 1 + verdict_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + verdict_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + verdict_payload["release_final_terminal_publish_exit_code"] = 1 + verdict_payload["release_final_handoff_status"] = "completed_with_follow_up" + verdict_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + verdict_payload["release_final_handoff_exit_code"] = 1 + verdict_payload["release_final_closure_status"] = "closed_with_follow_up" + verdict_payload["release_final_closure_decision"] = "close_with_follow_up" + verdict_payload["release_final_closure_exit_code"] = 1 + verdict_payload["release_final_closure_publish_status"] = "published_with_follow_up" + verdict_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + verdict_payload["release_final_closure_publish_exit_code"] = 1 + verdict_payload["release_final_archive_status"] = "archived_with_follow_up" + verdict_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + verdict_payload["release_final_archive_exit_code"] = 1 + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["final_verdict_should_ship"] = True + verdict_payload["final_verdict_requires_manual_action"] = False + verdict_payload["final_verdict_channel"] = "follow_up" + verdict_payload["release_final_verdict_status"] = "released_with_follow_up" + verdict_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + verdict_payload["release_final_verdict_exit_code"] = 1 + verdict_payload["reason_codes"] = ["release_final_verdict_released_with_follow_up"] + + verdict_path = tmp_path / "release_final_verdict_with_follow_up.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_release_final_verdict_report(verdict_path) + publish_payload = gate.build_release_final_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["release_final_verdict_publish_status"] == "published_with_follow_up" + assert ( + publish_payload["release_final_verdict_publish_decision"] + == "announce_release_shipped_with_follow_up" + ) + assert publish_payload["release_final_verdict_publish_exit_code"] == 1 + assert publish_payload["final_verdict_publish_should_notify"] is True + assert publish_payload["final_verdict_publish_requires_manual_action"] is False + assert publish_payload["final_verdict_publish_channel"] == "follow_up" + assert "release_final_verdict_publish_with_follow_up" in publish_payload["reason_codes"] + + +def test_build_release_final_verdict_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_publish_gate_module() + verdict_payload = _sample_release_final_verdict_payload() + verdict_payload["release_delivery_final_verdict_status"] = "blocked" + verdict_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + verdict_payload["release_delivery_final_verdict_exit_code"] = 1 + verdict_payload["release_follow_up_final_verdict_status"] = "blocked" + verdict_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + verdict_payload["release_follow_up_final_verdict_exit_code"] = 1 + verdict_payload["release_final_outcome_status"] = "blocked" + verdict_payload["release_final_outcome_decision"] = "escalate_blocker" + verdict_payload["release_final_outcome_exit_code"] = 1 + verdict_payload["release_final_terminal_publish_status"] = "blocked" + verdict_payload["release_final_terminal_publish_decision"] = "announce_blocker" + verdict_payload["release_final_terminal_publish_exit_code"] = 1 + verdict_payload["release_final_handoff_status"] = "blocked" + verdict_payload["release_final_handoff_decision"] = "handoff_blocker" + verdict_payload["release_final_handoff_exit_code"] = 1 + verdict_payload["release_final_closure_status"] = "blocked" + verdict_payload["release_final_closure_decision"] = "close_blocker" + verdict_payload["release_final_closure_exit_code"] = 1 + verdict_payload["release_final_closure_publish_status"] = "blocked" + verdict_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + verdict_payload["release_final_closure_publish_exit_code"] = 1 + verdict_payload["release_final_archive_status"] = "blocked" + verdict_payload["release_final_archive_decision"] = "archive_release_blocker" + verdict_payload["release_final_archive_exit_code"] = 1 + verdict_payload["final_verdict_should_ship"] = False + verdict_payload["final_verdict_requires_manual_action"] = True + verdict_payload["final_verdict_channel"] = "blocker" + verdict_payload["release_final_verdict_status"] = "blocked" + verdict_payload["release_final_verdict_decision"] = "escalate_release_blocker" + verdict_payload["release_final_verdict_exit_code"] = 1 + verdict_payload["reason_codes"] = ["release_final_verdict_blocked"] + + verdict_path = tmp_path / "release_final_verdict_blocked.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_release_final_verdict_report(verdict_path) + publish_payload = gate.build_release_final_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["release_final_verdict_publish_status"] == "blocked" + assert publish_payload["release_final_verdict_publish_decision"] == "announce_release_blocker" + assert publish_payload["release_final_verdict_publish_exit_code"] == 1 + assert publish_payload["final_verdict_publish_should_notify"] is True + assert publish_payload["final_verdict_publish_requires_manual_action"] is True + assert publish_payload["final_verdict_publish_channel"] == "blocker" + assert "release_final_verdict_publish_blocked" in publish_payload["reason_codes"] + + +def test_build_release_final_verdict_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_release_final_verdict_publish_gate_module() + verdict_payload = _sample_release_final_verdict_payload() + verdict_payload["release_final_verdict_status"] = "released" + verdict_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + verdict_payload["final_verdict_requires_manual_action"] = True + verdict_payload["final_verdict_channel"] = "follow_up" + + verdict_path = tmp_path / "release_final_verdict_mismatch.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_release_final_verdict_report(verdict_path) + publish_payload = gate.build_release_final_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["release_final_verdict_publish_status"] == "contract_failed" + assert publish_payload["release_final_verdict_publish_decision"] == "abort_publish" + assert publish_payload["release_final_verdict_publish_exit_code"] == 1 + assert "final_verdict_decision_mismatch" in publish_payload["structural_issues"] + assert "final_verdict_requires_manual_action_mismatch" in publish_payload["structural_issues"] + assert "final_verdict_channel_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_final_verdict_publish_gate_module() + verdict_payload = _sample_release_final_verdict_payload() + verdict_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + verdict_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + verdict_payload["release_follow_up_final_verdict_exit_code"] = 1 + verdict_payload["release_final_outcome_status"] = "released_with_follow_up" + verdict_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + verdict_payload["release_final_outcome_exit_code"] = 1 + verdict_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + verdict_payload["release_final_terminal_publish_decision"] = "announce_release_with_follow_up" + verdict_payload["release_final_terminal_publish_exit_code"] = 1 + verdict_payload["release_final_handoff_status"] = "completed_with_follow_up" + verdict_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + verdict_payload["release_final_handoff_exit_code"] = 1 + verdict_payload["release_final_closure_status"] = "closed_with_follow_up" + verdict_payload["release_final_closure_decision"] = "close_with_follow_up" + verdict_payload["release_final_closure_exit_code"] = 1 + verdict_payload["release_final_closure_publish_status"] = "published_with_follow_up" + verdict_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + verdict_payload["release_final_closure_publish_exit_code"] = 1 + verdict_payload["release_final_archive_status"] = "archived_with_follow_up" + verdict_payload["release_final_archive_decision"] = "archive_release_closed_with_follow_up" + verdict_payload["release_final_archive_exit_code"] = 1 + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["final_verdict_should_ship"] = True + verdict_payload["final_verdict_requires_manual_action"] = False + verdict_payload["final_verdict_channel"] = "follow_up" + verdict_payload["release_final_verdict_status"] = "released_with_follow_up" + verdict_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + verdict_payload["release_final_verdict_exit_code"] = 1 + verdict_payload["reason_codes"] = ["release_final_verdict_released_with_follow_up"] + + verdict_path = tmp_path / "release_final_verdict_output.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_release_final_verdict_report(verdict_path) + publish_payload = gate.build_release_final_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_final_verdict_publish.md" + ), + ) + + assert outputs["workflow_release_final_verdict_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_release_final_verdict_publish_decision"] + == "announce_release_shipped_with_follow_up" + ) + assert outputs["workflow_release_final_verdict_publish_exit_code"] == "1" + assert outputs["workflow_release_final_verdict_publish_should_notify"] == "true" + assert outputs["workflow_release_final_verdict_publish_requires_manual_action"] == "false" + assert outputs["workflow_release_final_verdict_publish_channel"] == "follow_up" + assert outputs["workflow_release_final_verdict_publish_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_release_final_verdict_publish_run_id"] == "1234567890" + assert outputs["workflow_release_final_verdict_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_release_final_verdict_publish_report_json"].endswith( + "ci_workflow_release_final_verdict_publish.json" + ) + assert outputs["workflow_release_final_verdict_publish_report_markdown"].endswith( + "ci_workflow_release_final_verdict_publish.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py new file mode 100644 index 0000000..1fa68ba --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_finalization_gate_runtime.py @@ -0,0 +1,164 @@ +"""Contract tests for P2-35 Linux CI workflow release finalization gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_finalization_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_finalization_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_finalization_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_completion_report": "/tmp/ci_workflow_release_completion.json", + "source_release_trace_report": "/tmp/ci_workflow_release_trace.json", + "source_release_trigger_report": "/tmp/ci_workflow_release_trigger.json", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "release_trigger_status": "triggered", + "release_tracking_status": "release_run_completed_success", + "release_completion_status": "release_run_completed_success", + "release_completion_exit_code": 0, + "release_publish_status": "passed", + "release_publish_exit_code": 0, + "should_finalize_release": True, + "release_terminal_publish_summary": ( + "release_completion_status=release_run_completed_success " + "release_publish_status=passed should_finalize_release=True" + ), + "allow_in_progress": False, + "should_poll_release_run": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "reason_codes": ["release_completed_success"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_finalization_payload_marks_finalize(tmp_path: Path): + gate = _load_ci_workflow_release_finalization_gate_module() + payload = _sample_release_terminal_publish_payload() + report_path = tmp_path / "ci_workflow_release_terminal_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_publish_report(report_path) + finalization_payload = gate.build_release_finalization_payload( + report, + source_path=report_path.resolve(), + hold_policy="pass", + ) + + assert finalization_payload["release_finalization_status"] == "finalized" + assert finalization_payload["release_finalization_decision"] == "finalize" + assert finalization_payload["should_finalize_release"] is True + assert finalization_payload["release_finalization_exit_code"] == 0 + assert finalization_payload["reason_codes"] == ["release_finalized"] + + +def test_build_release_finalization_payload_hold_fail_policy(tmp_path: Path): + gate = _load_ci_workflow_release_finalization_gate_module() + payload = _sample_release_terminal_publish_payload() + payload["release_tracking_status"] = "release_run_in_progress" + payload["release_completion_status"] = "release_run_await_timeout" + payload["release_publish_status"] = "in_progress" + payload["release_publish_exit_code"] = 0 + payload["should_finalize_release"] = False + payload["reason_codes"] = ["release_run_await_timeout"] + + report_path = tmp_path / "ci_workflow_release_terminal_publish_in_progress.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_publish_report(report_path) + finalization_payload = gate.build_release_finalization_payload( + report, + source_path=report_path.resolve(), + hold_policy="fail", + ) + + assert finalization_payload["release_finalization_status"] == "awaiting_release" + assert finalization_payload["release_finalization_decision"] == "hold" + assert finalization_payload["should_finalize_release"] is False + assert finalization_payload["release_finalization_exit_code"] == 1 + assert "on_hold_policy_fail" in finalization_payload["reason_codes"] + + +def test_build_release_finalization_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_finalization_gate_module() + payload = _sample_release_terminal_publish_payload() + payload["release_publish_status"] = "passed" + payload["release_publish_exit_code"] = 1 + payload["should_finalize_release"] = True + + report_path = tmp_path / "ci_workflow_release_terminal_publish_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_publish_report(report_path) + finalization_payload = gate.build_release_finalization_payload( + report, + source_path=report_path.resolve(), + hold_policy="pass", + ) + + assert finalization_payload["release_finalization_status"] == "contract_failed" + assert finalization_payload["release_finalization_decision"] == "abort" + assert finalization_payload["release_finalization_exit_code"] == 1 + assert "release_publish_exit_code_mismatch_passed" in finalization_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_finalization_gate_module() + payload = _sample_release_terminal_publish_payload() + payload["release_publish_status"] = "failed" + payload["release_publish_exit_code"] = 1 + payload["should_finalize_release"] = False + payload["reason_codes"] = ["release_run_completed_with_failure"] + + report_path = tmp_path / "ci_workflow_release_terminal_publish_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_terminal_publish_report(report_path) + finalization_payload = gate.build_release_finalization_payload( + report, + source_path=report_path.resolve(), + hold_policy="pass", + ) + outputs = gate.build_github_output_values( + finalization_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_finalization.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_finalization.md" + ), + ) + + assert outputs["workflow_release_finalization_status"] == "failed" + assert outputs["workflow_release_finalization_decision"] == "abort" + assert outputs["workflow_release_finalization_should_finalize_release"] == "false" + assert outputs["workflow_release_finalization_exit_code"] == "1" + assert outputs["workflow_release_finalization_publish_status"] == "failed" + assert outputs["workflow_release_finalization_run_id"] == "2233445566" + assert outputs["workflow_release_finalization_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_finalization_report_json"].endswith( + "ci_workflow_release_finalization.json" + ) + assert outputs["workflow_release_finalization_report_markdown"].endswith( + "ci_workflow_release_finalization.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py new file mode 100644 index 0000000..5023499 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_follow_up_closure_gate_runtime.py @@ -0,0 +1,422 @@ +"""Contract tests for P2-45 Linux CI workflow release follow-up closure gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_follow_up_closure_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_follow_up_closure_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_follow_up_closure_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_follow_up_dispatch_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_delivery_final_verdict_report": ( + "/tmp/ci_workflow_release_delivery_final_verdict.json" + ), + "source_release_delivery_terminal_publish_report": ( + "/tmp/ci_workflow_release_delivery_terminal_publish.json" + ), + "source_release_delivery_report": "/tmp/ci_workflow_release_delivery.json", + "source_release_terminal_verdict_report": "/tmp/ci_workflow_release_terminal_verdict.json", + "source_release_incident_report": "/tmp/ci_workflow_release_incident.json", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_publish_archive": True, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_url": "", + "release_terminal_verdict_status": "released", + "release_terminal_verdict_decision": "ship", + "release_terminal_verdict_exit_code": 0, + "terminal_should_ship_release": True, + "terminal_requires_follow_up": False, + "terminal_incident_linked": False, + "release_delivery_status": "shipped", + "release_delivery_decision": "deliver", + "release_delivery_exit_code": 0, + "delivery_should_ship_release": True, + "delivery_requires_human_action": False, + "delivery_should_announce_blocker": False, + "release_delivery_terminal_publish_status": "published", + "release_delivery_terminal_publish_decision": "announce_release", + "release_delivery_terminal_publish_exit_code": 0, + "terminal_publish_should_notify": True, + "terminal_publish_should_create_follow_up": False, + "terminal_publish_channel": "release", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "final_should_close_release": True, + "final_should_open_follow_up": False, + "final_should_page_owner": False, + "final_announcement_target": "release", + "release_follow_up_dispatch_status": "closed", + "release_follow_up_dispatch_decision": "no_action", + "release_follow_up_dispatch_exit_code": 0, + "follow_up_required": False, + "escalation_required": False, + "dispatch_target": "none", + "release_run_id": 5566778899, + "release_run_url": "https://github.com/acme/demo/actions/runs/5566778899", + "reason_codes": ["release_follow_up_dispatch_closed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_follow_up_dispatch.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_delivery_final_verdict.json", "exists": True}, + ], + } + + +def test_build_release_follow_up_closure_payload_closed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_closure_gate_module() + payload = _sample_release_follow_up_dispatch_payload() + report_path = tmp_path / "ci_workflow_release_follow_up_dispatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_dispatch_report(report_path) + closure_payload = gate.build_release_follow_up_closure_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + follow_up_repo="", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + follow_up_command_parts=[], + dry_run=True, + command_result=None, + ) + + assert closure_payload["release_follow_up_closure_status"] == "closed" + assert closure_payload["release_follow_up_closure_decision"] == "no_action" + assert closure_payload["release_follow_up_closure_exit_code"] == 0 + assert closure_payload["should_queue_follow_up"] is False + assert closure_payload["follow_up_queue_attempted"] is False + assert closure_payload["follow_up_task_queued"] is False + assert closure_payload["escalation_task_queued"] is False + assert closure_payload["reason_codes"] == ["release_follow_up_closed_no_action"] + + +def test_build_release_follow_up_closure_payload_queued_dry_run(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_closure_gate_module() + payload = _sample_release_follow_up_dispatch_payload() + payload["release_archive_status"] = "pending" + payload["release_archive_decision"] = "hold" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "awaiting_archive" + payload["release_verdict_decision"] = "hold" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["release_terminal_verdict_status"] = "awaiting_archive" + payload["release_terminal_verdict_decision"] = "hold" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["release_delivery_status"] = "pending_follow_up" + payload["release_delivery_decision"] = "hold" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "pending_follow_up" + payload["release_delivery_terminal_publish_decision"] = "announce_hold" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "hold" + payload["release_delivery_final_verdict_status"] = "requires_follow_up" + payload["release_delivery_final_verdict_decision"] = "open_follow_up" + payload["release_delivery_final_verdict_exit_code"] = 1 + payload["final_should_close_release"] = False + payload["final_should_open_follow_up"] = True + payload["final_should_page_owner"] = False + payload["final_announcement_target"] = "hold" + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "follow_up_queue" + payload["reason_codes"] = ["release_follow_up_dispatch_required"] + report_path = tmp_path / "ci_workflow_release_follow_up_dispatch_follow_up.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_dispatch_report(report_path) + command_parts = gate.build_follow_up_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + follow_up_command="", + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + ) + closure_payload = gate.build_release_follow_up_closure_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + follow_up_command_parts=command_parts, + dry_run=True, + command_result=None, + ) + + assert closure_payload["release_follow_up_closure_status"] == "queued_dry_run" + assert closure_payload["release_follow_up_closure_decision"] == "queue_follow_up" + assert closure_payload["release_follow_up_closure_exit_code"] == 0 + assert closure_payload["should_queue_follow_up"] is True + assert closure_payload["follow_up_queue_attempted"] is False + assert closure_payload["follow_up_task_queued"] is False + assert closure_payload["escalation_task_queued"] is False + assert closure_payload["follow_up_command_parts"][:3] == ["gh", "issue", "create"] + + +def test_build_release_follow_up_closure_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_closure_gate_module() + payload = _sample_release_follow_up_dispatch_payload() + payload["release_follow_up_dispatch_status"] = "closed" + payload["release_follow_up_dispatch_decision"] = "no_action" + payload["release_follow_up_dispatch_exit_code"] = 0 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "none" + report_path = tmp_path / "ci_workflow_release_follow_up_dispatch_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_dispatch_report(report_path) + closure_payload = gate.build_release_follow_up_closure_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + follow_up_repo="", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + follow_up_command_parts=[], + dry_run=True, + command_result=None, + ) + + assert closure_payload["release_follow_up_closure_status"] == "contract_failed" + assert closure_payload["release_follow_up_closure_decision"] == "abort_queue" + assert closure_payload["release_follow_up_closure_exit_code"] == 1 + assert "follow_up_required_mismatch_closed" in closure_payload["structural_issues"] + + +def test_build_release_follow_up_closure_payload_marks_queue_failed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_closure_gate_module() + payload = _sample_release_follow_up_dispatch_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/101" + payload["release_terminal_verdict_status"] = "blocked_incident_dispatched" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["release_delivery_status"] = "blocked_incident" + payload["release_delivery_decision"] = "escalate" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "blocked" + payload["release_delivery_terminal_publish_decision"] = "announce_blocker" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "blocker" + payload["release_delivery_final_verdict_status"] = "blocked" + payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + payload["release_delivery_final_verdict_exit_code"] = 1 + payload["final_should_close_release"] = False + payload["final_should_open_follow_up"] = True + payload["final_should_page_owner"] = True + payload["final_announcement_target"] = "blocker" + payload["release_follow_up_dispatch_status"] = "escalated" + payload["release_follow_up_dispatch_decision"] = "dispatch_escalation" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = True + payload["dispatch_target"] = "incident_commander" + payload["reason_codes"] = ["release_follow_up_dispatch_escalated"] + report_path = tmp_path / "ci_workflow_release_follow_up_dispatch_escalated.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_dispatch_report(report_path) + command_parts = gate.build_follow_up_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + follow_up_command="", + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + ) + closure_payload = gate.build_release_follow_up_closure_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + follow_up_command_parts=command_parts, + dry_run=False, + command_result={ + "attempted": True, + "returncode": 1, + "stdout_tail": "", + "stderr_tail": "follow-up create failed", + "error_type": "", + "error_message": "", + }, + ) + + assert closure_payload["release_follow_up_closure_status"] == "queue_failed" + assert closure_payload["release_follow_up_closure_decision"] == "abort_queue" + assert closure_payload["release_follow_up_closure_exit_code"] == 1 + assert closure_payload["should_queue_follow_up"] is True + assert closure_payload["follow_up_queue_attempted"] is True + assert "release_follow_up_command_failed" in closure_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_closure_gate_module() + payload = _sample_release_follow_up_dispatch_payload() + payload["release_archive_status"] = "pending" + payload["release_archive_decision"] = "hold" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "awaiting_archive" + payload["release_verdict_decision"] = "hold" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["release_terminal_verdict_status"] = "awaiting_archive" + payload["release_terminal_verdict_decision"] = "hold" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["release_delivery_status"] = "pending_follow_up" + payload["release_delivery_decision"] = "hold" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "pending_follow_up" + payload["release_delivery_terminal_publish_decision"] = "announce_hold" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "hold" + payload["release_delivery_final_verdict_status"] = "requires_follow_up" + payload["release_delivery_final_verdict_decision"] = "open_follow_up" + payload["release_delivery_final_verdict_exit_code"] = 1 + payload["final_should_close_release"] = False + payload["final_should_open_follow_up"] = True + payload["final_should_page_owner"] = False + payload["final_announcement_target"] = "hold" + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "follow_up_queue" + payload["reason_codes"] = ["release_follow_up_dispatch_required"] + report_path = tmp_path / "ci_workflow_release_follow_up_dispatch_follow_up.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_dispatch_report(report_path) + command_parts = gate.build_follow_up_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + follow_up_command="", + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + ) + closure_payload = gate.build_release_follow_up_closure_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + follow_up_repo="acme/demo", + follow_up_label="release-follow-up", + follow_up_title_prefix="[release-follow-up]", + follow_up_command_parts=command_parts, + dry_run=False, + command_result={ + "attempted": True, + "returncode": 0, + "stdout_tail": "created https://github.com/acme/demo/issues/77", + "stderr_tail": "", + "error_type": "", + "error_message": "", + }, + ) + outputs = gate.build_github_output_values( + closure_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_closure.md" + ), + ) + + assert outputs["workflow_release_follow_up_closure_status"] == "queued" + assert outputs["workflow_release_follow_up_closure_decision"] == "queue_follow_up" + assert outputs["workflow_release_follow_up_closure_exit_code"] == "0" + assert outputs["workflow_release_follow_up_closure_should_queue"] == "true" + assert outputs["workflow_release_follow_up_closure_attempted"] == "true" + assert outputs["workflow_release_follow_up_closure_follow_up_queued"] == "true" + assert outputs["workflow_release_follow_up_closure_escalation_queued"] == "false" + assert outputs["workflow_release_follow_up_closure_target"] == "follow_up_queue" + assert outputs["workflow_release_follow_up_closure_queue_url"].endswith("/issues/77") + assert outputs["workflow_release_follow_up_closure_dispatch_status"] == "follow_up_required" + assert outputs["workflow_release_follow_up_closure_run_id"] == "5566778899" + assert outputs["workflow_release_follow_up_closure_run_url"].endswith( + "/actions/runs/5566778899" + ) + assert outputs["workflow_release_follow_up_closure_report_json"].endswith( + "ci_workflow_release_follow_up_closure.json" + ) + assert outputs["workflow_release_follow_up_closure_report_markdown"].endswith( + "ci_workflow_release_follow_up_closure.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py new file mode 100644 index 0000000..e96ce26 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_follow_up_dispatch_gate_runtime.py @@ -0,0 +1,289 @@ +"""Contract tests for P2-44 Linux CI workflow release follow-up dispatch gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_follow_up_dispatch_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_follow_up_dispatch_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_follow_up_dispatch_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_delivery_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_delivery_terminal_publish_report": ( + "/tmp/ci_workflow_release_delivery_terminal_publish.json" + ), + "source_release_delivery_report": "/tmp/ci_workflow_release_delivery.json", + "source_release_terminal_verdict_report": "/tmp/ci_workflow_release_terminal_verdict.json", + "source_release_incident_report": "/tmp/ci_workflow_release_incident.json", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_publish_archive": True, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_url": "", + "release_terminal_verdict_status": "released", + "release_terminal_verdict_decision": "ship", + "release_terminal_verdict_exit_code": 0, + "terminal_should_ship_release": True, + "terminal_requires_follow_up": False, + "terminal_incident_linked": False, + "release_delivery_status": "shipped", + "release_delivery_decision": "deliver", + "release_delivery_exit_code": 0, + "delivery_should_ship_release": True, + "delivery_requires_human_action": False, + "delivery_should_announce_blocker": False, + "release_delivery_terminal_publish_status": "published", + "release_delivery_terminal_publish_decision": "announce_release", + "release_delivery_terminal_publish_exit_code": 0, + "terminal_publish_should_notify": True, + "terminal_publish_should_create_follow_up": False, + "terminal_publish_channel": "release", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "final_should_close_release": True, + "final_should_open_follow_up": False, + "final_should_page_owner": False, + "final_announcement_target": "release", + "release_run_id": 4455667788, + "release_run_url": "https://github.com/acme/demo/actions/runs/4455667788", + "reason_codes": ["release_delivery_final_completed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_delivery_final_verdict.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_delivery_terminal_publish.json", "exists": True}, + ], + } + + +def test_build_release_follow_up_dispatch_payload_closed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_dispatch_gate_module() + payload = _sample_release_delivery_final_verdict_payload() + report_path = tmp_path / "ci_workflow_release_delivery_final_verdict.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_final_verdict_report(report_path) + dispatch_payload = gate.build_release_follow_up_dispatch_payload( + report, + source_path=report_path.resolve(), + ) + + assert dispatch_payload["release_follow_up_dispatch_status"] == "closed" + assert dispatch_payload["release_follow_up_dispatch_decision"] == "no_action" + assert dispatch_payload["release_follow_up_dispatch_exit_code"] == 0 + assert dispatch_payload["follow_up_required"] is False + assert dispatch_payload["escalation_required"] is False + assert dispatch_payload["dispatch_target"] == "none" + assert dispatch_payload["reason_codes"] == ["release_follow_up_dispatch_closed"] + + +def test_build_release_follow_up_dispatch_payload_follow_up_required(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_dispatch_gate_module() + payload = _sample_release_delivery_final_verdict_payload() + payload["release_archive_status"] = "pending" + payload["release_archive_decision"] = "hold" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "awaiting_archive" + payload["release_verdict_decision"] = "hold" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["release_terminal_verdict_status"] = "awaiting_archive" + payload["release_terminal_verdict_decision"] = "hold" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["release_delivery_status"] = "pending_follow_up" + payload["release_delivery_decision"] = "hold" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "pending_follow_up" + payload["release_delivery_terminal_publish_decision"] = "announce_hold" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "hold" + payload["release_delivery_final_verdict_status"] = "requires_follow_up" + payload["release_delivery_final_verdict_decision"] = "open_follow_up" + payload["release_delivery_final_verdict_exit_code"] = 1 + payload["final_should_close_release"] = False + payload["final_should_open_follow_up"] = True + payload["final_should_page_owner"] = False + payload["final_announcement_target"] = "hold" + payload["reason_codes"] = ["release_delivery_final_follow_up_required"] + report_path = tmp_path / "ci_workflow_release_delivery_final_verdict_hold.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_final_verdict_report(report_path) + dispatch_payload = gate.build_release_follow_up_dispatch_payload( + report, + source_path=report_path.resolve(), + ) + + assert dispatch_payload["release_follow_up_dispatch_status"] == "follow_up_required" + assert dispatch_payload["release_follow_up_dispatch_decision"] == "dispatch_follow_up" + assert dispatch_payload["release_follow_up_dispatch_exit_code"] == 1 + assert dispatch_payload["follow_up_required"] is True + assert dispatch_payload["escalation_required"] is False + assert dispatch_payload["dispatch_target"] == "follow_up_queue" + assert "release_follow_up_dispatch_required" in dispatch_payload["reason_codes"] + + +def test_build_release_follow_up_dispatch_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_dispatch_gate_module() + payload = _sample_release_delivery_final_verdict_payload() + payload["release_delivery_final_verdict_status"] = "completed" + payload["release_delivery_final_verdict_decision"] = "close_release" + payload["release_delivery_final_verdict_exit_code"] = 0 + payload["final_should_close_release"] = True + payload["final_should_open_follow_up"] = False + payload["final_should_page_owner"] = True + payload["final_announcement_target"] = "release" + report_path = tmp_path / "ci_workflow_release_delivery_final_verdict_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_final_verdict_report(report_path) + dispatch_payload = gate.build_release_follow_up_dispatch_payload( + report, + source_path=report_path.resolve(), + ) + + assert dispatch_payload["release_follow_up_dispatch_status"] == "contract_failed" + assert dispatch_payload["release_follow_up_dispatch_decision"] == "abort_dispatch" + assert dispatch_payload["release_follow_up_dispatch_exit_code"] == 1 + assert "final_should_page_owner_mismatch_completed" in dispatch_payload["structural_issues"] + + +def test_build_release_follow_up_dispatch_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_dispatch_gate_module() + payload = _sample_release_delivery_final_verdict_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_delivery_final_verdict.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_delivery_terminal_publish.json", "exists": False}, + ] + report_path = tmp_path / "ci_workflow_release_delivery_final_verdict_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_final_verdict_report(report_path) + dispatch_payload = gate.build_release_follow_up_dispatch_payload( + report, + source_path=report_path.resolve(), + ) + + assert dispatch_payload["release_follow_up_dispatch_status"] == "contract_failed" + assert dispatch_payload["release_follow_up_dispatch_decision"] == "abort_dispatch" + assert dispatch_payload["release_follow_up_dispatch_exit_code"] == 1 + assert "/tmp/ci_workflow_release_delivery_terminal_publish.json" in dispatch_payload[ + "missing_artifacts" + ] + assert "release_follow_up_dispatch_evidence_missing" in dispatch_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_dispatch_gate_module() + payload = _sample_release_delivery_final_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/99" + payload["release_terminal_verdict_status"] = "blocked_incident_dispatched" + payload["release_terminal_verdict_decision"] = "escalate" + payload["release_terminal_verdict_exit_code"] = 1 + payload["terminal_should_ship_release"] = False + payload["terminal_requires_follow_up"] = True + payload["terminal_incident_linked"] = True + payload["release_delivery_status"] = "blocked_incident" + payload["release_delivery_decision"] = "escalate" + payload["release_delivery_exit_code"] = 1 + payload["delivery_should_ship_release"] = False + payload["delivery_requires_human_action"] = True + payload["delivery_should_announce_blocker"] = True + payload["release_delivery_terminal_publish_status"] = "blocked" + payload["release_delivery_terminal_publish_decision"] = "announce_blocker" + payload["release_delivery_terminal_publish_exit_code"] = 1 + payload["terminal_publish_should_notify"] = True + payload["terminal_publish_should_create_follow_up"] = True + payload["terminal_publish_channel"] = "blocker" + payload["release_delivery_final_verdict_status"] = "blocked" + payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + payload["release_delivery_final_verdict_exit_code"] = 1 + payload["final_should_close_release"] = False + payload["final_should_open_follow_up"] = True + payload["final_should_page_owner"] = True + payload["final_announcement_target"] = "blocker" + payload["reason_codes"] = ["release_delivery_final_blocked"] + report_path = tmp_path / "ci_workflow_release_delivery_final_verdict_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_delivery_final_verdict_report(report_path) + dispatch_payload = gate.build_release_follow_up_dispatch_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + dispatch_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_dispatch.md" + ), + ) + + assert outputs["workflow_release_follow_up_dispatch_status"] == "escalated" + assert outputs["workflow_release_follow_up_dispatch_decision"] == "dispatch_escalation" + assert outputs["workflow_release_follow_up_dispatch_exit_code"] == "1" + assert outputs["workflow_release_follow_up_dispatch_follow_up_required"] == "true" + assert outputs["workflow_release_follow_up_dispatch_escalation_required"] == "true" + assert outputs["workflow_release_follow_up_dispatch_target"] == "incident_commander" + assert outputs["workflow_release_follow_up_dispatch_final_status"] == "blocked" + assert outputs["workflow_release_follow_up_dispatch_incident_status"] == "dispatched" + assert outputs["workflow_release_follow_up_dispatch_incident_url"].endswith("/issues/99") + assert outputs["workflow_release_follow_up_dispatch_run_id"] == "4455667788" + assert outputs["workflow_release_follow_up_dispatch_run_url"].endswith( + "/actions/runs/4455667788" + ) + assert outputs["workflow_release_follow_up_dispatch_report_json"].endswith( + "ci_workflow_release_follow_up_dispatch.json" + ) + assert outputs["workflow_release_follow_up_dispatch_report_markdown"].endswith( + "ci_workflow_release_follow_up_dispatch.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py new file mode 100644 index 0000000..01052f7 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_follow_up_final_verdict_gate_runtime.py @@ -0,0 +1,281 @@ +"""Contract tests for P2-47 Linux CI workflow release follow-up final verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_follow_up_final_verdict_gate_module(): + script_path = ( + Path("scripts") / "run_p2_linux_ci_workflow_release_follow_up_final_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_follow_up_final_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_follow_up_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_follow_up_closure_report": "/tmp/ci_workflow_release_follow_up_closure.json", + "source_release_follow_up_dispatch_report": "/tmp/ci_workflow_release_follow_up_dispatch.json", + "project_root": "/tmp/project", + "release_follow_up_dispatch_status": "closed", + "release_follow_up_dispatch_decision": "no_action", + "release_follow_up_dispatch_exit_code": 0, + "follow_up_required": False, + "escalation_required": False, + "dispatch_target": "none", + "should_queue_follow_up": False, + "release_follow_up_closure_status": "closed", + "release_follow_up_closure_decision": "no_action", + "release_follow_up_closure_exit_code": 0, + "follow_up_queue_attempted": False, + "follow_up_task_queued": False, + "escalation_task_queued": False, + "follow_up_command": "", + "follow_up_command_parts": [], + "follow_up_command_returncode": None, + "follow_up_command_stdout_tail": "", + "follow_up_command_stderr_tail": "", + "follow_up_error_type": "", + "follow_up_error_message": "", + "follow_up_repo": "", + "follow_up_label": "release-follow-up", + "follow_up_title_prefix": "[release-follow-up]", + "follow_up_queue_url": "", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "release_follow_up_terminal_publish_status": "published", + "release_follow_up_terminal_publish_decision": "announce_closed", + "release_follow_up_terminal_publish_exit_code": 0, + "follow_up_terminal_publish_should_notify": True, + "follow_up_terminal_requires_manual_action": False, + "follow_up_terminal_publish_channel": "release", + "release_follow_up_terminal_publish_summary": ( + "release_follow_up_closure_status=closed " + "release_follow_up_terminal_publish_status=published " + "release_follow_up_terminal_publish_decision=announce_closed" + ), + "reason_codes": ["release_follow_up_terminal_closed"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_follow_up_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_follow_up_dispatch.json", "exists": True}, + ], + } + + +def test_build_release_follow_up_final_verdict_payload_completed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_final_verdict_gate_module() + payload = _sample_release_follow_up_terminal_publish_payload() + report_path = tmp_path / "ci_workflow_release_follow_up_terminal_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_terminal_publish_report(report_path) + final_payload = gate.build_release_follow_up_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_follow_up_final_verdict_status"] == "completed" + assert final_payload["release_follow_up_final_verdict_decision"] == "close_follow_up" + assert final_payload["release_follow_up_final_verdict_exit_code"] == 0 + assert final_payload["final_should_close_follow_up"] is True + assert final_payload["final_should_open_follow_up"] is False + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_announcement_target"] == "release" + assert final_payload["reason_codes"] == ["release_follow_up_final_completed_closed"] + + +def test_build_release_follow_up_final_verdict_payload_requires_follow_up(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_final_verdict_gate_module() + payload = _sample_release_follow_up_terminal_publish_payload() + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "follow_up_queue" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queued_dry_run" + payload["release_follow_up_closure_decision"] = "queue_follow_up" + payload["release_follow_up_closure_exit_code"] = 0 + payload["follow_up_queue_attempted"] = False + payload["follow_up_task_queued"] = False + payload["escalation_task_queued"] = False + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_repo"] = "acme/demo" + payload["release_follow_up_terminal_publish_status"] = "pending_queue" + payload["release_follow_up_terminal_publish_decision"] = "announce_pending_queue" + payload["release_follow_up_terminal_publish_exit_code"] = 1 + payload["follow_up_terminal_publish_should_notify"] = True + payload["follow_up_terminal_requires_manual_action"] = True + payload["follow_up_terminal_publish_channel"] = "follow_up" + payload["reason_codes"] = ["release_follow_up_terminal_pending_queue"] + report_path = tmp_path / "ci_workflow_release_follow_up_terminal_publish_pending.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_terminal_publish_report(report_path) + final_payload = gate.build_release_follow_up_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_follow_up_final_verdict_status"] == "requires_follow_up" + assert final_payload["release_follow_up_final_verdict_decision"] == "keep_follow_up_open" + assert final_payload["release_follow_up_final_verdict_exit_code"] == 1 + assert final_payload["final_should_close_follow_up"] is False + assert final_payload["final_should_open_follow_up"] is True + assert final_payload["final_should_page_owner"] is False + assert final_payload["final_announcement_target"] == "follow_up" + assert "release_follow_up_final_pending_queue" in final_payload["reason_codes"] + + +def test_build_release_follow_up_final_verdict_payload_queue_failed_blocked(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_final_verdict_gate_module() + payload = _sample_release_follow_up_terminal_publish_payload() + payload["release_follow_up_dispatch_status"] = "escalated" + payload["release_follow_up_dispatch_decision"] = "dispatch_escalation" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = True + payload["dispatch_target"] = "incident_commander" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queue_failed" + payload["release_follow_up_closure_decision"] = "abort_queue" + payload["release_follow_up_closure_exit_code"] = 1 + payload["follow_up_queue_attempted"] = True + payload["follow_up_task_queued"] = False + payload["escalation_task_queued"] = False + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_command_returncode"] = 1 + payload["follow_up_command_stderr_tail"] = "create failed" + payload["follow_up_repo"] = "acme/demo" + payload["release_follow_up_terminal_publish_status"] = "queue_failed" + payload["release_follow_up_terminal_publish_decision"] = "announce_queue_failure" + payload["release_follow_up_terminal_publish_exit_code"] = 1 + payload["follow_up_terminal_publish_should_notify"] = True + payload["follow_up_terminal_requires_manual_action"] = True + payload["follow_up_terminal_publish_channel"] = "blocker" + payload["reason_codes"] = ["release_follow_up_terminal_queue_failed"] + report_path = tmp_path / "ci_workflow_release_follow_up_terminal_publish_queue_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_terminal_publish_report(report_path) + final_payload = gate.build_release_follow_up_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_follow_up_final_verdict_status"] == "blocked" + assert final_payload["release_follow_up_final_verdict_decision"] == "escalate_queue_failure" + assert final_payload["release_follow_up_final_verdict_exit_code"] == 1 + assert final_payload["final_should_close_follow_up"] is False + assert final_payload["final_should_open_follow_up"] is True + assert final_payload["final_should_page_owner"] is True + assert final_payload["final_announcement_target"] == "blocker" + assert "release_follow_up_final_queue_failed" in final_payload["reason_codes"] + + +def test_build_release_follow_up_final_verdict_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_release_follow_up_final_verdict_gate_module() + payload = _sample_release_follow_up_terminal_publish_payload() + payload["release_follow_up_terminal_publish_status"] = "published" + payload["release_follow_up_terminal_publish_decision"] = "announce_closed" + payload["release_follow_up_terminal_publish_exit_code"] = 0 + payload["follow_up_terminal_publish_should_notify"] = True + payload["follow_up_terminal_requires_manual_action"] = False + payload["follow_up_terminal_publish_channel"] = "release" + payload["release_follow_up_closure_status"] = "closed" + payload["release_follow_up_closure_decision"] = "no_action" + payload["dispatch_target"] = "follow_up_queue" + report_path = tmp_path / "ci_workflow_release_follow_up_terminal_publish_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_terminal_publish_report(report_path) + final_payload = gate.build_release_follow_up_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["release_follow_up_final_verdict_status"] == "contract_failed" + assert final_payload["release_follow_up_final_verdict_decision"] == "abort_close" + assert final_payload["release_follow_up_final_verdict_exit_code"] == 1 + assert "dispatch_target_mismatch_closed" in final_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_final_verdict_gate_module() + payload = _sample_release_follow_up_terminal_publish_payload() + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["dispatch_target"] = "follow_up_queue" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queued" + payload["release_follow_up_closure_decision"] = "queue_follow_up" + payload["release_follow_up_closure_exit_code"] = 0 + payload["follow_up_queue_attempted"] = True + payload["follow_up_task_queued"] = True + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_command_returncode"] = 0 + payload["follow_up_command_stdout_tail"] = "created https://github.com/acme/demo/issues/123" + payload["follow_up_repo"] = "acme/demo" + payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/123" + payload["release_follow_up_terminal_publish_status"] = "published" + payload["release_follow_up_terminal_publish_decision"] = "announce_queued" + payload["release_follow_up_terminal_publish_exit_code"] = 0 + payload["follow_up_terminal_publish_channel"] = "follow_up" + payload["reason_codes"] = ["release_follow_up_terminal_queued"] + report_path = tmp_path / "ci_workflow_release_follow_up_terminal_publish_queued.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_terminal_publish_report(report_path) + final_payload = gate.build_release_follow_up_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_final_verdict.md" + ), + ) + + assert outputs["workflow_release_follow_up_final_verdict_status"] == "completed" + assert outputs["workflow_release_follow_up_final_verdict_decision"] == "close_follow_up" + assert outputs["workflow_release_follow_up_final_verdict_exit_code"] == "0" + assert outputs["workflow_release_follow_up_final_should_close_follow_up"] == "true" + assert outputs["workflow_release_follow_up_final_should_open_follow_up"] == "false" + assert outputs["workflow_release_follow_up_final_should_page_owner"] == "false" + assert outputs["workflow_release_follow_up_final_announcement_target"] == "follow_up" + assert outputs["workflow_release_follow_up_final_terminal_publish_status"] == "published" + assert outputs["workflow_release_follow_up_final_terminal_publish_channel"] == "follow_up" + assert outputs["workflow_release_follow_up_final_queue_url"].endswith("/issues/123") + assert outputs["workflow_release_follow_up_final_run_id"] == "9988776655" + assert outputs["workflow_release_follow_up_final_run_url"].endswith( + "/actions/runs/9988776655" + ) + assert outputs["workflow_release_follow_up_final_report_json"].endswith( + "ci_workflow_release_follow_up_final_verdict.json" + ) + assert outputs["workflow_release_follow_up_final_report_markdown"].endswith( + "ci_workflow_release_follow_up_final_verdict.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py new file mode 100644 index 0000000..8beb726 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate_runtime.py @@ -0,0 +1,263 @@ +"""Contract tests for P2-46 Linux CI workflow release follow-up terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_follow_up_terminal_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_linux_ci_workflow_release_follow_up_terminal_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_follow_up_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_follow_up_closure_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_follow_up_dispatch_report": ( + "/tmp/ci_workflow_release_follow_up_dispatch.json" + ), + "project_root": "/tmp/project", + "release_follow_up_dispatch_status": "closed", + "release_follow_up_dispatch_decision": "no_action", + "release_follow_up_dispatch_exit_code": 0, + "follow_up_required": False, + "escalation_required": False, + "dispatch_target": "none", + "should_queue_follow_up": False, + "release_follow_up_closure_status": "closed", + "release_follow_up_closure_decision": "no_action", + "release_follow_up_closure_exit_code": 0, + "follow_up_queue_attempted": False, + "follow_up_task_queued": False, + "escalation_task_queued": False, + "follow_up_command": "", + "follow_up_command_parts": [], + "follow_up_command_returncode": None, + "follow_up_command_stdout_tail": "", + "follow_up_command_stderr_tail": "", + "follow_up_error_type": "", + "follow_up_error_message": "", + "follow_up_repo": "", + "follow_up_label": "release-follow-up", + "follow_up_title_prefix": "[release-follow-up]", + "follow_up_queue_url": "", + "release_run_id": 7788990011, + "release_run_url": "https://github.com/acme/demo/actions/runs/7788990011", + "reason_codes": ["release_follow_up_closed_no_action"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_follow_up_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_follow_up_dispatch.json", "exists": True}, + ], + } + + +def test_build_release_follow_up_terminal_publish_payload_closed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_terminal_publish_gate_module() + payload = _sample_release_follow_up_closure_payload() + report_path = tmp_path / "ci_workflow_release_follow_up_closure.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_closure_report(report_path) + terminal_payload = gate.build_release_follow_up_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_follow_up_terminal_publish_status"] == "published" + assert terminal_payload["release_follow_up_terminal_publish_decision"] == "announce_closed" + assert terminal_payload["release_follow_up_terminal_publish_exit_code"] == 0 + assert terminal_payload["follow_up_terminal_publish_should_notify"] is True + assert terminal_payload["follow_up_terminal_requires_manual_action"] is False + assert terminal_payload["follow_up_terminal_publish_channel"] == "release" + assert terminal_payload["reason_codes"] == ["release_follow_up_terminal_closed"] + + +def test_build_release_follow_up_terminal_publish_payload_pending_queue(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_terminal_publish_gate_module() + payload = _sample_release_follow_up_closure_payload() + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "follow_up_queue" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queued_dry_run" + payload["release_follow_up_closure_decision"] = "queue_follow_up" + payload["release_follow_up_closure_exit_code"] = 0 + payload["follow_up_queue_attempted"] = False + payload["follow_up_task_queued"] = False + payload["escalation_task_queued"] = False + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_repo"] = "acme/demo" + payload["follow_up_queue_url"] = "" + payload["reason_codes"] = ["release_follow_up_dispatch_required"] + report_path = tmp_path / "ci_workflow_release_follow_up_closure_pending.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_closure_report(report_path) + terminal_payload = gate.build_release_follow_up_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_follow_up_terminal_publish_status"] == "pending_queue" + assert ( + terminal_payload["release_follow_up_terminal_publish_decision"] + == "announce_pending_queue" + ) + assert terminal_payload["release_follow_up_terminal_publish_exit_code"] == 1 + assert terminal_payload["follow_up_terminal_publish_should_notify"] is True + assert terminal_payload["follow_up_terminal_requires_manual_action"] is True + assert terminal_payload["follow_up_terminal_publish_channel"] == "follow_up" + assert "release_follow_up_terminal_pending_queue" in terminal_payload["reason_codes"] + + +def test_build_release_follow_up_terminal_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_release_follow_up_terminal_publish_gate_module() + payload = _sample_release_follow_up_closure_payload() + payload["release_follow_up_closure_status"] = "closed" + payload["release_follow_up_closure_decision"] = "no_action" + payload["release_follow_up_closure_exit_code"] = 0 + payload["dispatch_target"] = "follow_up_queue" + payload["follow_up_required"] = False + report_path = tmp_path / "ci_workflow_release_follow_up_closure_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_closure_report(report_path) + terminal_payload = gate.build_release_follow_up_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_follow_up_terminal_publish_status"] == "contract_failed" + assert terminal_payload["release_follow_up_terminal_publish_decision"] == "abort_publish" + assert terminal_payload["release_follow_up_terminal_publish_exit_code"] == 1 + assert "dispatch_target_mismatch_closed" in terminal_payload["structural_issues"] + + +def test_build_release_follow_up_terminal_publish_payload_queue_failed(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_terminal_publish_gate_module() + payload = _sample_release_follow_up_closure_payload() + payload["release_follow_up_dispatch_status"] = "escalated" + payload["release_follow_up_dispatch_decision"] = "dispatch_escalation" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = True + payload["dispatch_target"] = "incident_commander" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queue_failed" + payload["release_follow_up_closure_decision"] = "abort_queue" + payload["release_follow_up_closure_exit_code"] = 1 + payload["follow_up_queue_attempted"] = True + payload["follow_up_task_queued"] = False + payload["escalation_task_queued"] = False + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_command_returncode"] = 1 + payload["follow_up_command_stderr_tail"] = "create failed" + payload["follow_up_error_type"] = "" + payload["follow_up_error_message"] = "" + payload["follow_up_repo"] = "acme/demo" + payload["follow_up_queue_url"] = "" + payload["reason_codes"] = ["release_follow_up_command_failed"] + report_path = tmp_path / "ci_workflow_release_follow_up_closure_queue_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_closure_report(report_path) + terminal_payload = gate.build_release_follow_up_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_follow_up_terminal_publish_status"] == "queue_failed" + assert ( + terminal_payload["release_follow_up_terminal_publish_decision"] + == "announce_queue_failure" + ) + assert terminal_payload["release_follow_up_terminal_publish_exit_code"] == 1 + assert terminal_payload["follow_up_terminal_publish_should_notify"] is True + assert terminal_payload["follow_up_terminal_requires_manual_action"] is True + assert terminal_payload["follow_up_terminal_publish_channel"] == "blocker" + assert "release_follow_up_terminal_queue_failed" in terminal_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_follow_up_terminal_publish_gate_module() + payload = _sample_release_follow_up_closure_payload() + payload["release_follow_up_dispatch_status"] = "follow_up_required" + payload["release_follow_up_dispatch_decision"] = "dispatch_follow_up" + payload["release_follow_up_dispatch_exit_code"] = 1 + payload["follow_up_required"] = True + payload["escalation_required"] = False + payload["dispatch_target"] = "follow_up_queue" + payload["should_queue_follow_up"] = True + payload["release_follow_up_closure_status"] = "queued" + payload["release_follow_up_closure_decision"] = "queue_follow_up" + payload["release_follow_up_closure_exit_code"] = 0 + payload["follow_up_queue_attempted"] = True + payload["follow_up_task_queued"] = True + payload["escalation_task_queued"] = False + payload["follow_up_command"] = "gh issue create --repo acme/demo" + payload["follow_up_command_parts"] = ["gh", "issue", "create", "--repo", "acme/demo"] + payload["follow_up_command_returncode"] = 0 + payload["follow_up_command_stdout_tail"] = "created https://github.com/acme/demo/issues/123" + payload["follow_up_repo"] = "acme/demo" + payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/123" + payload["reason_codes"] = ["release_follow_up_queued"] + report_path = tmp_path / "ci_workflow_release_follow_up_closure_queued.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_follow_up_closure_report(report_path) + terminal_payload = gate.build_release_follow_up_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + terminal_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_follow_up_terminal_publish.md" + ), + ) + + assert outputs["workflow_release_follow_up_terminal_publish_status"] == "published" + assert outputs["workflow_release_follow_up_terminal_publish_decision"] == "announce_queued" + assert outputs["workflow_release_follow_up_terminal_publish_exit_code"] == "0" + assert outputs["workflow_release_follow_up_terminal_publish_should_notify"] == "true" + assert outputs["workflow_release_follow_up_terminal_publish_manual_action"] == "false" + assert outputs["workflow_release_follow_up_terminal_publish_channel"] == "follow_up" + assert outputs["workflow_release_follow_up_terminal_publish_closure_status"] == "queued" + assert outputs["workflow_release_follow_up_terminal_publish_queue_url"].endswith( + "/issues/123" + ) + assert outputs["workflow_release_follow_up_terminal_publish_run_id"] == "7788990011" + assert outputs["workflow_release_follow_up_terminal_publish_run_url"].endswith( + "/actions/runs/7788990011" + ) + assert outputs["workflow_release_follow_up_terminal_publish_report_json"].endswith( + "ci_workflow_release_follow_up_terminal_publish.json" + ) + assert outputs["workflow_release_follow_up_terminal_publish_report_markdown"].endswith( + "ci_workflow_release_follow_up_terminal_publish.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py new file mode 100644 index 0000000..e3fc64c --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_handoff_gate_runtime.py @@ -0,0 +1,165 @@ +"""Contract tests for P2-30 Linux CI workflow release handoff gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_handoff_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_handoff_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_handoff_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "completion_status": "run_completed_success", + "completion_exit_code": 0, + "publish_status": "passed", + "publish_exit_code": 0, + "should_promote": True, + "publish_summary": ( + "completion_status=run_completed_success publish_status=passed " + "should_promote=True" + ), + "execution_status": "dispatched", + "tracking_status": "run_completed_success", + "allow_in_progress": False, + "should_poll_workflow_run": True, + "run_id": 1234567890, + "run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "reason_codes": ["workflow_completed_success"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_handoff_payload_ready_for_release(tmp_path: Path): + gate = _load_ci_workflow_release_handoff_gate_module() + payload = _sample_terminal_publish_payload() + report_path = tmp_path / "ci_workflow_terminal_publish.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_terminal_publish_report(report_path) + handoff_payload = gate.build_release_handoff_payload( + report, + source_path=report_path.resolve(), + target_environment="production", + release_channel="stable", + ) + + assert handoff_payload["handoff_status"] == "ready_for_release" + assert handoff_payload["handoff_action"] == "promote" + assert handoff_payload["should_trigger_release"] is True + assert handoff_payload["release_exit_code"] == 0 + assert handoff_payload["reason_codes"] == ["release_ready"] + + +def test_build_release_handoff_payload_awaiting_completion(tmp_path: Path): + gate = _load_ci_workflow_release_handoff_gate_module() + payload = _sample_terminal_publish_payload() + payload["publish_status"] = "in_progress" + payload["publish_exit_code"] = 0 + payload["should_promote"] = False + payload["completion_status"] = "run_await_timeout" + payload["reason_codes"] = ["run_await_timeout"] + + report_path = tmp_path / "ci_workflow_terminal_publish_in_progress.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_terminal_publish_report(report_path) + handoff_payload = gate.build_release_handoff_payload( + report, + source_path=report_path.resolve(), + target_environment="production", + release_channel="stable", + ) + + assert handoff_payload["handoff_status"] == "awaiting_completion" + assert handoff_payload["handoff_action"] == "hold" + assert handoff_payload["should_trigger_release"] is False + assert handoff_payload["release_exit_code"] == 0 + assert "run_await_timeout" in handoff_payload["reason_codes"] + + +def test_build_release_handoff_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_handoff_gate_module() + payload = _sample_terminal_publish_payload() + payload["publish_status"] = "passed" + payload["publish_exit_code"] = 0 + payload["should_promote"] = False + + report_path = tmp_path / "ci_workflow_terminal_publish_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_terminal_publish_report(report_path) + handoff_payload = gate.build_release_handoff_payload( + report, + source_path=report_path.resolve(), + target_environment="production", + release_channel="stable", + ) + + assert handoff_payload["handoff_status"] == "contract_failed" + assert handoff_payload["handoff_action"] == "hold" + assert handoff_payload["should_trigger_release"] is False + assert handoff_payload["release_exit_code"] == 1 + assert "should_promote_mismatch_passed" in handoff_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_handoff_gate_module() + payload = _sample_terminal_publish_payload() + payload["publish_status"] = "failed" + payload["publish_exit_code"] = 1 + payload["should_promote"] = False + payload["completion_status"] = "run_completed_failure" + payload["completion_exit_code"] = 1 + payload["reason_codes"] = ["run_completed_with_failure"] + + report_path = tmp_path / "ci_workflow_terminal_publish_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_terminal_publish_report(report_path) + handoff_payload = gate.build_release_handoff_payload( + report, + source_path=report_path.resolve(), + target_environment="staging", + release_channel="canary", + ) + outputs = gate.build_github_output_values( + handoff_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_handoff.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_handoff.md"), + ) + + assert outputs["workflow_release_handoff_status"] == "failed" + assert outputs["workflow_release_handoff_action"] == "reject" + assert outputs["workflow_release_handoff_should_trigger_release"] == "false" + assert outputs["workflow_release_handoff_release_exit_code"] == "1" + assert outputs["workflow_release_handoff_target_environment"] == "staging" + assert outputs["workflow_release_handoff_release_channel"] == "canary" + assert outputs["workflow_release_handoff_publish_status"] == "failed" + assert outputs["workflow_release_handoff_completion_status"] == "run_completed_failure" + assert outputs["workflow_release_handoff_run_id"] == "1234567890" + assert outputs["workflow_release_handoff_report_json"].endswith( + "ci_workflow_release_handoff.json" + ) + assert outputs["workflow_release_handoff_report_markdown"].endswith( + "ci_workflow_release_handoff.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py new file mode 100644 index 0000000..4f772a2 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_incident_gate_runtime.py @@ -0,0 +1,257 @@ +"""Contract tests for P2-39 Linux CI workflow release incident gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_incident_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_incident_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_incident_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_verdict_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_publish_archive": True, + "should_ship_release": True, + "should_open_incident": False, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "release_verdict_summary": ( + "release_archive_status=ready release_verdict_status=published " + "release_verdict_decision=ship" + ), + "reason_codes": ["release_verdict_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": True}, + ], + } + + +def test_build_release_incident_payload_not_required(tmp_path: Path): + gate = _load_ci_workflow_release_incident_gate_module() + payload = _sample_release_verdict_payload() + report_path = tmp_path / "ci_workflow_release_verdict.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_verdict_report(report_path) + incident_payload = gate.build_release_incident_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + incident_repo="", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + incident_command_parts=[], + dry_run=True, + command_result=None, + ) + + assert incident_payload["incident_dispatch_status"] == "not_required" + assert incident_payload["should_dispatch_incident"] is False + assert incident_payload["incident_dispatch_exit_code"] == 0 + assert incident_payload["should_open_incident"] is False + + +def test_build_release_incident_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_release_incident_gate_module() + payload = _sample_release_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["reason_codes"] = ["release_archive_failed"] + report_path = tmp_path / "ci_workflow_release_verdict_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_verdict_report(report_path) + command_parts = gate.build_incident_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + incident_command="", + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + ) + incident_payload = gate.build_release_incident_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + incident_command_parts=command_parts, + dry_run=True, + command_result=None, + ) + + assert incident_payload["incident_dispatch_status"] == "ready_dry_run" + assert incident_payload["should_dispatch_incident"] is True + assert incident_payload["incident_dispatch_exit_code"] == 0 + assert incident_payload["incident_dispatch_attempted"] is False + assert incident_payload["incident_command_parts"][:3] == ["gh", "issue", "create"] + + +def test_build_release_incident_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_incident_gate_module() + payload = _sample_release_verdict_payload() + payload["release_verdict_status"] = "published" + payload["release_verdict_decision"] = "ship" + payload["release_verdict_exit_code"] = 0 + payload["should_ship_release"] = True + payload["should_open_incident"] = True + report_path = tmp_path / "ci_workflow_release_verdict_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_verdict_report(report_path) + incident_payload = gate.build_release_incident_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + incident_repo="", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + incident_command_parts=[], + dry_run=True, + command_result=None, + ) + + assert incident_payload["incident_dispatch_status"] == "contract_failed" + assert incident_payload["incident_dispatch_exit_code"] == 1 + assert "should_open_incident_mismatch_published" in incident_payload["structural_issues"] + + +def test_build_release_incident_payload_marks_dispatch_failed(tmp_path: Path): + gate = _load_ci_workflow_release_incident_gate_module() + payload = _sample_release_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["reason_codes"] = ["release_archive_failed"] + report_path = tmp_path / "ci_workflow_release_verdict_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_verdict_report(report_path) + command_parts = gate.build_incident_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + incident_command="", + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + ) + incident_payload = gate.build_release_incident_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + incident_command_parts=command_parts, + dry_run=False, + command_result={ + "attempted": True, + "returncode": 1, + "stdout_tail": "", + "stderr_tail": "issue create failed", + "error_type": "", + "error_message": "", + }, + ) + + assert incident_payload["incident_dispatch_status"] == "dispatch_failed" + assert incident_payload["incident_dispatch_exit_code"] == 1 + assert incident_payload["incident_dispatch_attempted"] is True + assert "release_incident_command_failed" in incident_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_incident_gate_module() + payload = _sample_release_verdict_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["reason_codes"] = ["release_archive_failed"] + report_path = tmp_path / "ci_workflow_release_verdict_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_verdict_report(report_path) + command_parts = gate.build_incident_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + incident_command="", + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + ) + incident_payload = gate.build_release_incident_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + incident_repo="acme/demo", + incident_label="release-incident", + incident_title_prefix="[release-incident]", + incident_command_parts=command_parts, + dry_run=True, + command_result=None, + ) + outputs = gate.build_github_output_values( + incident_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_incident.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_incident.md"), + ) + + assert outputs["workflow_release_incident_status"] == "ready_dry_run" + assert outputs["workflow_release_incident_should_dispatch"] == "true" + assert outputs["workflow_release_incident_attempted"] == "false" + assert outputs["workflow_release_incident_exit_code"] == "0" + assert outputs["workflow_release_incident_repo"] == "acme/demo" + assert outputs["workflow_release_incident_label"] == "release-incident" + assert outputs["workflow_release_incident_verdict_status"] == "blocked" + assert outputs["workflow_release_incident_run_id"] == "2233445566" + assert outputs["workflow_release_incident_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_incident_report_json"].endswith( + "ci_workflow_release_incident.json" + ) + assert outputs["workflow_release_incident_report_markdown"].endswith( + "ci_workflow_release_incident.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py new file mode 100644 index 0000000..68bc4ba --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_terminal_publish_gate_runtime.py @@ -0,0 +1,185 @@ +"""Contract tests for P2-34 Linux CI workflow release terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_terminal_publish_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_terminal_publish_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_completion_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_trace_report": "/tmp/ci_workflow_release_trace.json", + "project_root": "/tmp", + "source_release_trigger_report": "/tmp/ci_workflow_release_trigger.json", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "release_trigger_status": "triggered", + "release_tracking_status": "release_run_tracking_ready", + "release_completion_status": "release_run_completed_success", + "release_completion_exit_code": 0, + "dry_run": False, + "allow_in_progress": False, + "should_poll_release_run": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "repo_owner": "acme", + "repo_name": "demo", + "release_workflow_path": ".github/workflows/release.yml", + "release_workflow_ref": "main", + "release_poll_command": ( + "gh run view 2233445566 --json " + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt --repo acme/demo" + ), + "release_poll_command_parts": [ + "gh", + "run", + "view", + "2233445566", + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + "--repo", + "acme/demo", + ], + "poll_interval_seconds": 20, + "max_polls": 10, + "poll_timeout_seconds": 300, + "poll_iterations": 1, + "poll_attempted": True, + "poll_returncode": 0, + "poll_status": "completed", + "poll_conclusion": "success", + "poll_url": "https://github.com/acme/demo/actions/runs/2233445566", + "poll_error_type": "", + "poll_error_message": "", + "poll_stdout_tail": "", + "poll_stderr_tail": "", + "release_trace_reason_codes": ["release_trigger_command_succeeded"], + "reason_codes": ["release_run_completed_success"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_terminal_publish_payload_marks_passed_and_finalize(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_publish_gate_module() + payload = _sample_release_completion_payload() + report_path = tmp_path / "ci_workflow_release_completion.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_completion_report(report_path) + publish_payload = gate.build_release_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["release_publish_status"] == "passed" + assert publish_payload["release_publish_exit_code"] == 0 + assert publish_payload["should_finalize_release"] is True + assert publish_payload["reason_codes"] == ["release_completed_success"] + + +def test_build_release_terminal_publish_payload_timeout_with_allow_in_progress(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_publish_gate_module() + payload = _sample_release_completion_payload() + payload["release_tracking_status"] = "release_run_in_progress" + payload["release_completion_status"] = "release_run_await_timeout" + payload["release_completion_exit_code"] = 0 + payload["allow_in_progress"] = True + payload["reason_codes"] = ["release_run_await_timeout"] + + report_path = tmp_path / "ci_workflow_release_completion_timeout.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_completion_report(report_path) + publish_payload = gate.build_release_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["release_publish_status"] == "in_progress" + assert publish_payload["release_publish_exit_code"] == 0 + assert publish_payload["should_finalize_release"] is False + assert "release_run_await_timeout" in publish_payload["reason_codes"] + + +def test_build_release_terminal_publish_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_publish_gate_module() + payload = _sample_release_completion_payload() + payload["release_completion_status"] = "release_run_completed_success" + payload["release_completion_exit_code"] = 1 + + report_path = tmp_path / "ci_workflow_release_completion_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_completion_report(report_path) + publish_payload = gate.build_release_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["release_publish_status"] == "contract_failed" + assert publish_payload["release_publish_exit_code"] == 1 + assert publish_payload["should_finalize_release"] is False + assert "release_completion_exit_code_mismatch_success" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_publish_gate_module() + payload = _sample_release_completion_payload() + payload["release_completion_status"] = "release_run_completed_failure" + payload["release_completion_exit_code"] = 1 + payload["reason_codes"] = ["release_run_completed_with_failure"] + + report_path = tmp_path / "ci_workflow_release_completion_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_completion_report(report_path) + publish_payload = gate.build_release_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_publish.md" + ), + ) + + assert outputs["workflow_release_terminal_publish_status"] == "failed" + assert outputs["workflow_release_terminal_publish_exit_code"] == "1" + assert outputs["workflow_release_terminal_publish_should_finalize_release"] == "false" + assert outputs["workflow_release_terminal_publish_completion_status"] == ( + "release_run_completed_failure" + ) + assert outputs["workflow_release_terminal_publish_run_id"] == "2233445566" + assert outputs["workflow_release_terminal_publish_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_terminal_publish_report_json"].endswith( + "ci_workflow_release_terminal_publish.json" + ) + assert outputs["workflow_release_terminal_publish_report_markdown"].endswith( + "ci_workflow_release_terminal_publish.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py new file mode 100644 index 0000000..d5a0a0d --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_terminal_verdict_gate_runtime.py @@ -0,0 +1,221 @@ +"""Contract tests for P2-40 Linux CI workflow release terminal verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_terminal_verdict_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_terminal_verdict_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_terminal_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_incident_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_verdict_report": "/tmp/ci_workflow_release_verdict.json", + "source_release_archive_report": "/tmp/ci_workflow_release_archive.json", + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "release_verdict_status": "published", + "release_verdict_decision": "ship", + "release_verdict_exit_code": 0, + "should_publish_archive": True, + "should_ship_release": True, + "should_open_incident": False, + "should_dispatch_incident": False, + "incident_dispatch_status": "not_required", + "incident_dispatch_exit_code": 0, + "incident_dispatch_attempted": False, + "incident_command": "", + "incident_command_parts": [], + "incident_command_returncode": None, + "incident_command_stdout_tail": "", + "incident_command_stderr_tail": "", + "incident_error_type": "", + "incident_error_message": "", + "incident_repo": "", + "incident_label": "release-incident", + "incident_title_prefix": "[release-incident]", + "incident_url": "", + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "incident_dispatch_summary": ( + "release_verdict_status=published incident_dispatch_status=not_required " + "should_dispatch_incident=False" + ), + "reason_codes": ["incident_not_required_release_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": True}, + ], + } + + +def test_build_release_terminal_verdict_payload_released(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_verdict_gate_module() + payload = _sample_release_incident_payload() + report_path = tmp_path / "ci_workflow_release_incident.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_incident_report(report_path) + terminal_payload = gate.build_release_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_terminal_verdict_status"] == "released" + assert terminal_payload["release_terminal_verdict_decision"] == "ship" + assert terminal_payload["release_terminal_verdict_exit_code"] == 0 + assert terminal_payload["terminal_should_ship_release"] is True + assert terminal_payload["terminal_requires_follow_up"] is False + assert terminal_payload["reason_codes"] == ["release_terminal_released"] + + +def test_build_release_terminal_verdict_payload_blocked_incident_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_verdict_gate_module() + payload = _sample_release_incident_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "ready_dry_run" + payload["incident_dispatch_exit_code"] = 0 + payload["reason_codes"] = ["release_archive_failed"] + report_path = tmp_path / "ci_workflow_release_incident_blocked.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_incident_report(report_path) + terminal_payload = gate.build_release_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_terminal_verdict_status"] == "blocked_incident_ready_dry_run" + assert terminal_payload["release_terminal_verdict_decision"] == "escalate" + assert terminal_payload["release_terminal_verdict_exit_code"] == 1 + assert terminal_payload["terminal_should_ship_release"] is False + assert terminal_payload["terminal_requires_follow_up"] is True + assert "release_terminal_blocked_incident_ready_dry_run" in terminal_payload["reason_codes"] + + +def test_build_release_terminal_verdict_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_verdict_gate_module() + payload = _sample_release_incident_payload() + payload["release_verdict_status"] = "published" + payload["release_verdict_decision"] = "ship" + payload["release_verdict_exit_code"] = 0 + payload["should_ship_release"] = True + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "ready_dry_run" + report_path = tmp_path / "ci_workflow_release_incident_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_incident_report(report_path) + terminal_payload = gate.build_release_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_terminal_verdict_status"] == "contract_failed" + assert terminal_payload["release_terminal_verdict_decision"] == "block" + assert terminal_payload["release_terminal_verdict_exit_code"] == 1 + assert "should_open_incident_mismatch_published" in terminal_payload["structural_issues"] + + +def test_build_release_terminal_verdict_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_verdict_gate_module() + payload = _sample_release_incident_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": False}, + ] + report_path = tmp_path / "ci_workflow_release_incident_missing_evidence.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_incident_report(report_path) + terminal_payload = gate.build_release_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert terminal_payload["release_terminal_verdict_status"] == "contract_failed" + assert terminal_payload["release_terminal_verdict_decision"] == "block" + assert terminal_payload["release_terminal_verdict_exit_code"] == 1 + assert "/tmp/ci_workflow_release_finalization.json" in terminal_payload["missing_artifacts"] + assert "release_terminal_evidence_missing" in terminal_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_terminal_verdict_gate_module() + payload = _sample_release_incident_payload() + payload["release_archive_status"] = "failed" + payload["release_archive_decision"] = "block" + payload["release_archive_exit_code"] = 1 + payload["release_verdict_status"] = "blocked" + payload["release_verdict_decision"] = "block" + payload["release_verdict_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["should_ship_release"] = False + payload["should_open_incident"] = True + payload["should_dispatch_incident"] = True + payload["incident_dispatch_status"] = "dispatched" + payload["incident_dispatch_exit_code"] = 0 + payload["incident_dispatch_attempted"] = True + payload["incident_url"] = "https://github.com/acme/demo/issues/77" + payload["reason_codes"] = ["release_incident_dispatched"] + report_path = tmp_path / "ci_workflow_release_incident_dispatched.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_incident_report(report_path) + terminal_payload = gate.build_release_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + terminal_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_release_terminal_verdict.md" + ), + ) + + assert outputs["workflow_release_terminal_verdict_status"] == "blocked_incident_dispatched" + assert outputs["workflow_release_terminal_verdict_decision"] == "escalate" + assert outputs["workflow_release_terminal_verdict_exit_code"] == "1" + assert outputs["workflow_release_terminal_verdict_should_ship_release"] == "false" + assert outputs["workflow_release_terminal_verdict_requires_follow_up"] == "true" + assert outputs["workflow_release_terminal_verdict_incident_status"] == "dispatched" + assert outputs["workflow_release_terminal_verdict_incident_url"].endswith("/issues/77") + assert outputs["workflow_release_terminal_verdict_run_id"] == "2233445566" + assert outputs["workflow_release_terminal_verdict_run_url"].endswith( + "/actions/runs/2233445566" + ) + assert outputs["workflow_release_terminal_verdict_report_json"].endswith( + "ci_workflow_release_terminal_verdict.json" + ) + assert outputs["workflow_release_terminal_verdict_report_markdown"].endswith( + "ci_workflow_release_terminal_verdict.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py new file mode 100644 index 0000000..414f377 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_trace_gate_runtime.py @@ -0,0 +1,191 @@ +"""Contract tests for P2-32 Linux CI workflow release trace gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_trace_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_trace_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_trace_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_trigger_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_handoff_report": "/tmp/ci_workflow_release_handoff.json", + "project_root": "/tmp", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "handoff_status": "ready_for_release", + "handoff_action": "promote", + "should_trigger_release": True, + "release_exit_code": 0, + "release_trigger_status": "triggered", + "release_trigger_exit_code": 0, + "release_trigger_attempted": True, + "release_command": ( + "gh workflow run .github/workflows/release.yml --ref main " + "--raw-field source_release_handoff_report=/tmp/ci_workflow_release_handoff.json" + ), + "release_command_parts": [ + "gh", + "workflow", + "run", + ".github/workflows/release.yml", + "--ref", + "main", + "--raw-field", + "source_release_handoff_report=/tmp/ci_workflow_release_handoff.json", + ], + "release_workflow_path": ".github/workflows/release.yml", + "release_workflow_ref": "main", + "target_environment": "production", + "release_channel": "stable", + "publish_status": "passed", + "completion_status": "run_completed_success", + "run_id": 2233445566, + "run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "dry_run": False, + "command_returncode": 0, + "command_stdout_tail": ( + "created workflow run: " + "https://github.com/acme/demo/actions/runs/2233445566" + ), + "command_stderr_tail": "", + "trigger_error_type": "", + "trigger_error_message": "", + "reason_codes": ["release_trigger_command_succeeded"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_trace_payload_extracts_run_reference(tmp_path: Path): + gate = _load_ci_workflow_release_trace_gate_module() + payload = _sample_release_trigger_payload() + report_path = tmp_path / "ci_workflow_release_trigger.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + release_trigger_report = gate.load_release_trigger_report(report_path) + trace_payload = gate.build_release_trace_payload( + release_trigger_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["release_tracking_status"] == "release_run_tracking_ready" + assert trace_payload["should_poll_release_run"] is True + assert trace_payload["release_run_id"] == 2233445566 + assert trace_payload["repo_owner"] == "acme" + assert trace_payload["repo_name"] == "demo" + assert trace_payload["release_trace_exit_code"] == 0 + assert trace_payload["release_poll_attempted"] is False + assert trace_payload["release_poll_command_parts"][:3] == ["gh", "run", "view"] + + +def test_build_release_trace_payload_handles_missing_run_reference(tmp_path: Path): + gate = _load_ci_workflow_release_trace_gate_module() + payload = _sample_release_trigger_payload() + payload["command_stdout_tail"] = "release dispatch succeeded without run URL" + report_path = tmp_path / "ci_workflow_release_trigger_missing_run.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + release_trigger_report = gate.load_release_trigger_report(report_path) + trace_payload = gate.build_release_trace_payload( + release_trigger_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["release_tracking_status"] == "release_run_tracking_missing" + assert trace_payload["should_poll_release_run"] is False + assert trace_payload["release_trace_exit_code"] == 1 + assert "release_run_reference_missing" in trace_payload["reason_codes"] + + +def test_build_release_trace_payload_trigger_failed_passthrough(tmp_path: Path): + gate = _load_ci_workflow_release_trace_gate_module() + payload = _sample_release_trigger_payload() + payload["release_trigger_status"] = "trigger_failed" + payload["release_trigger_exit_code"] = 1 + payload["command_returncode"] = 1 + payload["command_stdout_tail"] = "" + payload["command_stderr_tail"] = "release dispatch failed" + payload["reason_codes"] = ["release_trigger_command_failed"] + report_path = tmp_path / "ci_workflow_release_trigger_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + release_trigger_report = gate.load_release_trigger_report(report_path) + trace_payload = gate.build_release_trace_payload( + release_trigger_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=True, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["release_tracking_status"] == "release_trigger_failed" + assert trace_payload["should_poll_release_run"] is False + assert trace_payload["release_run_id"] is None + assert trace_payload["release_trace_exit_code"] == 1 + assert trace_payload["release_poll_attempted"] is False + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_trace_gate_module() + payload = _sample_release_trigger_payload() + report_path = tmp_path / "ci_workflow_release_trigger.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + release_trigger_report = gate.load_release_trigger_report(report_path) + trace_payload = gate.build_release_trace_payload( + release_trigger_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + outputs = gate.build_github_output_values( + trace_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_trace.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_trace.md"), + ) + + assert outputs["workflow_release_trace_status"] == "release_run_tracking_ready" + assert outputs["workflow_release_trace_should_poll"] == "true" + assert outputs["workflow_release_trace_run_id"] == "2233445566" + assert outputs["workflow_release_trace_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_trace_exit_code"] == "0" + assert outputs["workflow_release_trace_report_json"].endswith( + "ci_workflow_release_trace.json" + ) + assert outputs["workflow_release_trace_report_markdown"].endswith( + "ci_workflow_release_trace.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py new file mode 100644 index 0000000..8c1e990 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_trigger_gate_runtime.py @@ -0,0 +1,228 @@ +"""Contract tests for P2-31 Linux CI workflow release trigger gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_trigger_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_trigger_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_trigger_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_handoff_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_terminal_publish_report": "/tmp/ci_workflow_terminal_publish.json", + "source_dispatch_completion_report": "/tmp/ci_workflow_dispatch_completion.json", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "target_environment": "production", + "release_channel": "stable", + "publish_status": "passed", + "publish_exit_code": 0, + "should_promote": True, + "completion_status": "run_completed_success", + "completion_exit_code": 0, + "handoff_status": "ready_for_release", + "handoff_action": "promote", + "should_trigger_release": True, + "release_exit_code": 0, + "handoff_summary": ( + "publish_status=passed handoff_status=ready_for_release " + "should_trigger_release=True target_environment=production " + "release_channel=stable" + ), + "publish_summary": ( + "completion_status=run_completed_success publish_status=passed " + "should_promote=True" + ), + "run_id": 1234567890, + "run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "reason_codes": ["release_ready"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_release_trigger_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_release_trigger_gate_module() + payload = _sample_release_handoff_payload() + report_path = tmp_path / "ci_workflow_release_handoff.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_handoff_report(report_path) + command_parts = gate.build_release_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + release_command="", + ) + trigger_payload = gate.build_release_trigger_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + release_command_parts=command_parts, + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + dry_run=True, + command_result=None, + ) + + assert trigger_payload["release_trigger_status"] == "ready_dry_run" + assert trigger_payload["release_trigger_exit_code"] == 0 + assert trigger_payload["release_trigger_attempted"] is False + assert trigger_payload["should_trigger_release"] is True + assert trigger_payload["release_command_parts"][:3] == ["gh", "workflow", "run"] + + +def test_build_release_trigger_payload_awaiting_completion_passthrough(tmp_path: Path): + gate = _load_ci_workflow_release_trigger_gate_module() + payload = _sample_release_handoff_payload() + payload["handoff_status"] = "awaiting_completion" + payload["handoff_action"] = "hold" + payload["should_trigger_release"] = False + payload["release_exit_code"] = 0 + payload["publish_status"] = "in_progress" + payload["completion_status"] = "run_await_timeout" + payload["reason_codes"] = ["run_await_timeout"] + + report_path = tmp_path / "ci_workflow_release_handoff_waiting.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_handoff_report(report_path) + trigger_payload = gate.build_release_trigger_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + release_command_parts=[], + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + dry_run=False, + command_result=None, + ) + + assert trigger_payload["release_trigger_status"] == "awaiting_completion" + assert trigger_payload["release_trigger_exit_code"] == 0 + assert trigger_payload["release_trigger_attempted"] is False + assert trigger_payload["should_trigger_release"] is False + + +def test_load_release_handoff_report_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_trigger_gate_module() + payload = _sample_release_handoff_payload() + payload["handoff_status"] = "ready_for_release" + payload["handoff_action"] = "promote" + payload["should_trigger_release"] = False + payload["release_exit_code"] = 0 + report_path = tmp_path / "ci_workflow_release_handoff_bad_contract.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_release_handoff_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_release_trigger_payload_marks_failed_command(tmp_path: Path): + gate = _load_ci_workflow_release_trigger_gate_module() + payload = _sample_release_handoff_payload() + report_path = tmp_path / "ci_workflow_release_handoff.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_handoff_report(report_path) + command_parts = gate.build_release_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + release_command="", + ) + trigger_payload = gate.build_release_trigger_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + release_command_parts=command_parts, + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + dry_run=False, + command_result={ + "attempted": True, + "returncode": 1, + "stdout_tail": "", + "stderr_tail": "release dispatch failed", + "error_type": "", + "error_message": "", + }, + ) + + assert trigger_payload["release_trigger_status"] == "trigger_failed" + assert trigger_payload["release_trigger_exit_code"] == 1 + assert trigger_payload["release_trigger_attempted"] is True + assert "release_trigger_command_failed" in trigger_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_trigger_gate_module() + payload = _sample_release_handoff_payload() + report_path = tmp_path / "ci_workflow_release_handoff.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_handoff_report(report_path) + command_parts = gate.build_release_command_parts( + report, + source_report_path=report_path.resolve(), + gh_executable="gh", + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + release_command="", + ) + trigger_payload = gate.build_release_trigger_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path.resolve(), + release_command_parts=command_parts, + release_workflow_path=".github/workflows/release.yml", + release_workflow_ref="main", + dry_run=True, + command_result=None, + ) + outputs = gate.build_github_output_values( + trigger_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_trigger.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_trigger.md"), + ) + + assert outputs["workflow_release_trigger_status"] == "ready_dry_run" + assert outputs["workflow_release_trigger_should_trigger_release"] == "true" + assert outputs["workflow_release_trigger_attempted"] == "false" + assert outputs["workflow_release_trigger_exit_code"] == "0" + assert outputs["workflow_release_trigger_target_environment"] == "production" + assert outputs["workflow_release_trigger_release_channel"] == "stable" + assert outputs["workflow_release_trigger_handoff_status"] == "ready_for_release" + assert outputs["workflow_release_trigger_run_id"] == "1234567890" + assert outputs["workflow_release_trigger_run_url"].endswith("/1234567890") + assert outputs["workflow_release_trigger_report_json"].endswith( + "ci_workflow_release_trigger.json" + ) + assert outputs["workflow_release_trigger_report_markdown"].endswith( + "ci_workflow_release_trigger.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py new file mode 100644 index 0000000..7fb62ec --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_release_verdict_gate_runtime.py @@ -0,0 +1,169 @@ +"""Contract tests for P2-38 Linux CI workflow release verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_release_verdict_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_release_verdict_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_release_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_archive_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_release_closure_report": "/tmp/ci_workflow_release_closure.json", + "release_closure_status": "closed", + "release_closure_decision": "ship", + "release_closure_exit_code": 0, + "release_archive_status": "ready", + "release_archive_decision": "publish", + "release_archive_exit_code": 0, + "should_close_release": True, + "should_notify": True, + "should_publish_archive": True, + "release_run_id": 2233445566, + "release_run_url": "https://github.com/acme/demo/actions/runs/2233445566", + "release_archive_summary": "release_closure_status=closed release_archive_status=ready", + "reason_codes": ["release_archive_ready"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": True}, + ], + } + + +def test_build_release_verdict_payload_marks_published(tmp_path: Path): + gate = _load_ci_workflow_release_verdict_gate_module() + payload = _sample_release_archive_payload() + report_path = tmp_path / "ci_workflow_release_archive.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_archive_report(report_path) + verdict_payload = gate.build_release_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert verdict_payload["release_verdict_status"] == "published" + assert verdict_payload["release_verdict_decision"] == "ship" + assert verdict_payload["should_ship_release"] is True + assert verdict_payload["should_open_incident"] is False + assert verdict_payload["release_verdict_exit_code"] == 0 + assert verdict_payload["reason_codes"] == ["release_verdict_published"] + + +def test_build_release_verdict_payload_pending_hold_path(tmp_path: Path): + gate = _load_ci_workflow_release_verdict_gate_module() + payload = _sample_release_archive_payload() + payload["release_archive_status"] = "pending" + payload["release_archive_decision"] = "hold" + payload["release_archive_exit_code"] = 1 + payload["should_publish_archive"] = False + payload["reason_codes"] = ["release_run_await_timeout"] + + report_path = tmp_path / "ci_workflow_release_archive_pending.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_archive_report(report_path) + verdict_payload = gate.build_release_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert verdict_payload["release_verdict_status"] == "awaiting_archive" + assert verdict_payload["release_verdict_decision"] == "hold" + assert verdict_payload["should_ship_release"] is False + assert verdict_payload["should_open_incident"] is False + assert verdict_payload["release_verdict_exit_code"] == 1 + assert "release_run_await_timeout" in verdict_payload["reason_codes"] + + +def test_build_release_verdict_payload_rejects_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_release_verdict_gate_module() + payload = _sample_release_archive_payload() + payload["release_archive_status"] = "ready" + payload["release_archive_decision"] = "hold" + + report_path = tmp_path / "ci_workflow_release_archive_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_archive_report(report_path) + verdict_payload = gate.build_release_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert verdict_payload["release_verdict_status"] == "contract_failed" + assert verdict_payload["release_verdict_decision"] == "block" + assert verdict_payload["release_verdict_exit_code"] == 1 + assert "release_archive_decision_mismatch_ready" in verdict_payload["structural_issues"] + + +def test_build_release_verdict_payload_tracks_missing_evidence(tmp_path: Path): + gate = _load_ci_workflow_release_verdict_gate_module() + payload = _sample_release_archive_payload() + payload["evidence_manifest"] = [ + {"path": "/tmp/ci_workflow_release_closure.json", "exists": True}, + {"path": "/tmp/ci_workflow_release_finalization.json", "exists": False}, + ] + + report_path = tmp_path / "ci_workflow_release_archive_missing.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_archive_report(report_path) + verdict_payload = gate.build_release_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert verdict_payload["release_verdict_status"] == "contract_failed" + assert verdict_payload["release_verdict_decision"] == "block" + assert verdict_payload["release_verdict_exit_code"] == 1 + assert "/tmp/ci_workflow_release_finalization.json" in verdict_payload["missing_artifacts"] + assert "release_verdict_evidence_missing" in verdict_payload["reason_codes"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_release_verdict_gate_module() + payload = _sample_release_archive_payload() + report_path = tmp_path / "ci_workflow_release_archive.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_release_archive_report(report_path) + verdict_payload = gate.build_release_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + verdict_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_release_verdict.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_release_verdict.md"), + ) + + assert outputs["workflow_release_verdict_status"] == "published" + assert outputs["workflow_release_verdict_decision"] == "ship" + assert outputs["workflow_release_verdict_should_ship_release"] == "true" + assert outputs["workflow_release_verdict_should_open_incident"] == "false" + assert outputs["workflow_release_verdict_exit_code"] == "0" + assert outputs["workflow_release_verdict_run_id"] == "2233445566" + assert outputs["workflow_release_verdict_run_url"].endswith("/actions/runs/2233445566") + assert outputs["workflow_release_verdict_report_json"].endswith( + "ci_workflow_release_verdict.json" + ) + assert outputs["workflow_release_verdict_report_markdown"].endswith( + "ci_workflow_release_verdict.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_sync_gate_runtime.py b/tests/test_p2_linux_ci_workflow_sync_gate_runtime.py new file mode 100644 index 0000000..6f7ac87 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_sync_gate_runtime.py @@ -0,0 +1,167 @@ +"""Contract tests for P2-19 Linux CI workflow sync gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_sync_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_sync_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_ci_workflow_sync_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_ci_workflow_plan() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_ci_matrix": "/tmp/ci_matrix.json", + "shard_total": 2, + "manifest_total_tests": 4, + "selected_shards": 2, + "skipped_empty_shards": False, + "fan_out_matrix": { + "include": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 2 --shard-index 1", + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 2, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 2 --shard-index 2", + }, + ] + }, + "fan_in": { + "summary_paths": [ + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ], + "merged_summary_path": ".claude/reports/linux_unified_gate/merged_summary.json", + "aggregation_command": "python scripts/run_p2_linux_shard_aggregation_gate.py --output .claude/reports/linux_unified_gate/merged_summary.json --summary .claude/reports/linux_unified_gate/shards/shard-1/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "publish_command": "python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md", + "final_report_json": ".claude/reports/linux_unified_gate/final_report.json", + "final_report_markdown": ".claude/reports/linux_unified_gate/final_report.md", + }, + } + + +def _write_plan(path: Path) -> None: + path.write_text(json.dumps(_sample_ci_workflow_plan(), indent=2), encoding="utf-8") + + +def test_check_render_drift_detects_missing_artifacts(tmp_path: Path): + gate = _load_ci_workflow_sync_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + _write_plan(plan_path) + + expected_yaml, expected_metadata = gate.build_expected_artifacts( + plan_path=plan_path, + workflow_path=tmp_path / ".github" / "workflows" / "linux_unified_gate.yml", + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + ) + drift = gate.check_render_drift( + expected_workflow_yaml=expected_yaml, + expected_metadata=expected_metadata, + workflow_path=tmp_path / ".github" / "workflows" / "linux_unified_gate.yml", + metadata_path=tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json", + strict_generated_at=False, + ) + + assert drift["workflow_missing"] is True + assert drift["metadata_missing"] is True + assert drift["workflow_drift"] is True + assert drift["metadata_drift"] is True + + +def test_check_render_drift_detects_workflow_content_drift(tmp_path: Path): + gate = _load_ci_workflow_sync_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + _write_plan(plan_path) + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + expected_yaml, expected_metadata = gate.build_expected_artifacts( + plan_path=plan_path, + workflow_path=workflow_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + ) + + workflow_path.parent.mkdir(parents=True, exist_ok=True) + workflow_path.write_text(expected_yaml.replace("workflow_dispatch", "workflow_call"), encoding="utf-8") + metadata_path.parent.mkdir(parents=True, exist_ok=True) + metadata_path.write_text(json.dumps(expected_metadata, indent=2), encoding="utf-8") + + drift = gate.check_render_drift( + expected_workflow_yaml=expected_yaml, + expected_metadata=expected_metadata, + workflow_path=workflow_path, + metadata_path=metadata_path, + strict_generated_at=False, + ) + + assert drift["workflow_drift"] is True + assert "workflow_call" in drift["workflow_diff"] + assert drift["metadata_drift"] is False + + +def test_check_render_drift_ignores_generated_at_by_default(tmp_path: Path): + gate = _load_ci_workflow_sync_gate_module() + plan_path = tmp_path / "ci_workflow_plan.json" + _write_plan(plan_path) + workflow_path = tmp_path / ".github" / "workflows" / "linux_unified_gate.yml" + metadata_path = tmp_path / ".claude" / "reports" / "linux_unified_gate" / "ci_workflow_render.json" + + expected_yaml, expected_metadata = gate.build_expected_artifacts( + plan_path=plan_path, + workflow_path=workflow_path, + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + ) + + workflow_path.parent.mkdir(parents=True, exist_ok=True) + workflow_path.write_text(expected_yaml, encoding="utf-8") + metadata_path.parent.mkdir(parents=True, exist_ok=True) + actual_metadata = dict(expected_metadata) + actual_metadata["generated_at"] = "2099-01-01T00:00:00+00:00" + metadata_path.write_text(json.dumps(actual_metadata, indent=2), encoding="utf-8") + + drift = gate.check_render_drift( + expected_workflow_yaml=expected_yaml, + expected_metadata=expected_metadata, + workflow_path=workflow_path, + metadata_path=metadata_path, + strict_generated_at=False, + ) + assert drift["workflow_drift"] is False + assert drift["metadata_drift"] is False + + strict_drift = gate.check_render_drift( + expected_workflow_yaml=expected_yaml, + expected_metadata=expected_metadata, + workflow_path=workflow_path, + metadata_path=metadata_path, + strict_generated_at=True, + ) + assert strict_drift["metadata_drift"] is True + assert "generated_at" in strict_drift["metadata_diff"] diff --git a/tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py b/tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py new file mode 100644 index 0000000..41e57fa --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_terminal_publish_gate_runtime.py @@ -0,0 +1,143 @@ +"""Contract tests for P2-29 Linux CI workflow terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_terminal_publish_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_terminal_publish_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_dispatch_completion_payload() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_dispatch_trace_report": "/tmp/ci_workflow_dispatch_trace.json", + "source_dispatch_execution_report": "/tmp/ci_workflow_dispatch_execution.json", + "source_dispatch_report": "/tmp/ci_workflow_dispatch.json", + "project_root": "/tmp", + "execution_status": "dispatched", + "tracking_status": "run_completed_success", + "completion_status": "run_completed_success", + "completion_exit_code": 0, + "dry_run": False, + "allow_in_progress": False, + "should_poll_workflow_run": True, + "run_id": 1234567890, + "run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "reason_codes": ["run_completed_success"], + "failed_checks": [], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_terminal_publish_payload_marks_passed_and_promotable(tmp_path: Path): + gate = _load_ci_workflow_terminal_publish_gate_module() + payload = _sample_dispatch_completion_payload() + report_path = tmp_path / "ci_workflow_dispatch_completion.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_completion_report(report_path) + publish_payload = gate.build_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["publish_status"] == "passed" + assert publish_payload["publish_exit_code"] == 0 + assert publish_payload["should_promote"] is True + assert publish_payload["reason_codes"] == ["workflow_completed_success"] + + +def test_build_terminal_publish_payload_timeout_with_allow_in_progress_becomes_in_progress( + tmp_path: Path, +): + gate = _load_ci_workflow_terminal_publish_gate_module() + payload = _sample_dispatch_completion_payload() + payload["tracking_status"] = "run_in_progress" + payload["completion_status"] = "run_await_timeout" + payload["completion_exit_code"] = 0 + payload["allow_in_progress"] = True + payload["reason_codes"] = ["run_await_timeout"] + + report_path = tmp_path / "ci_workflow_dispatch_completion_timeout.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_completion_report(report_path) + publish_payload = gate.build_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["publish_status"] == "in_progress" + assert publish_payload["publish_exit_code"] == 0 + assert publish_payload["should_promote"] is False + assert "run_await_timeout" in publish_payload["reason_codes"] + + +def test_build_terminal_publish_payload_rejects_completion_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_terminal_publish_gate_module() + payload = _sample_dispatch_completion_payload() + payload["completion_status"] = "run_completed_success" + payload["completion_exit_code"] = 1 + + report_path = tmp_path / "ci_workflow_dispatch_completion_mismatch.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_completion_report(report_path) + publish_payload = gate.build_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert publish_payload["publish_status"] == "contract_failed" + assert publish_payload["publish_exit_code"] == 1 + assert publish_payload["should_promote"] is False + assert "completion_exit_code_mismatch_success" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_terminal_publish_gate_module() + payload = _sample_dispatch_completion_payload() + payload["completion_status"] = "run_completed_failure" + payload["completion_exit_code"] = 1 + payload["reason_codes"] = ["run_completed_with_failure"] + + report_path = tmp_path / "ci_workflow_dispatch_completion_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_dispatch_completion_report(report_path) + publish_payload = gate.build_terminal_publish_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.json"), + output_markdown=Path(".claude/reports/linux_unified_gate/ci_workflow_terminal_publish.md"), + ) + + assert outputs["workflow_terminal_publish_status"] == "failed" + assert outputs["workflow_terminal_publish_exit_code"] == "1" + assert outputs["workflow_terminal_publish_should_promote"] == "false" + assert outputs["workflow_terminal_publish_completion_status"] == "run_completed_failure" + assert outputs["workflow_terminal_publish_run_id"] == "1234567890" + assert outputs["workflow_terminal_publish_report_json"].endswith( + "ci_workflow_terminal_publish.json" + ) + assert outputs["workflow_terminal_publish_report_markdown"].endswith( + "ci_workflow_terminal_publish.md" + ) + diff --git a/tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py b/tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py new file mode 100644 index 0000000..101ce37 --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_terminal_verdict_gate_runtime.py @@ -0,0 +1,444 @@ +"""Contract tests for P2-59 Linux CI workflow terminal verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_terminal_verdict_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_terminal_verdict_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_terminal_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_release_final_publish_archive_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_verdict_publish_report": "/tmp/ci_workflow_release_final_verdict_publish.json", + "source_release_final_verdict_report": "/tmp/ci_workflow_release_final_verdict.json", + "source_release_final_archive_report": "/tmp/ci_workflow_release_final_archive.json", + "source_release_final_closure_publish_report": "/tmp/ci_workflow_release_final_closure_publish.json", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_final_archive_status": "archived", + "release_final_archive_decision": "archive_release_closed", + "release_final_archive_exit_code": 0, + "release_final_verdict_status": "released", + "release_final_verdict_decision": "ship_release", + "release_final_verdict_exit_code": 0, + "release_final_verdict_publish_status": "published", + "release_final_verdict_publish_decision": "announce_release_shipped", + "release_final_verdict_publish_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "final_publish_archive_should_archive": True, + "final_publish_archive_requires_manual_action": False, + "final_publish_archive_channel": "release", + "release_final_publish_archive_status": "archived", + "release_final_publish_archive_decision": "archive_release_shipped", + "release_final_publish_archive_exit_code": 0, + "release_final_publish_archive_summary": ( + "release_final_verdict_publish_status=published " + "release_final_publish_archive_status=archived " + "release_final_publish_archive_decision=archive_release_shipped" + ), + "reason_codes": ["release_final_publish_archive_archived"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_verdict_publish", + "path": "/tmp/ci_workflow_release_final_verdict_publish.json", + "exists": True, + }, + { + "source": "final_verdict", + "path": "/tmp/ci_workflow_release_final_verdict.json", + "exists": True, + }, + ], + } + + +def _sample_gate_manifest_drift_payload() -> dict: + return { + "status": "passed", + "missing_runtime_tests": [], + "missing_manifest_entries": [], + "orphan_manifest_entries": [], + "structural_issues": [], + } + + +def test_build_terminal_verdict_payload_ready_for_linux_validation(tmp_path: Path): + gate = _load_ci_workflow_terminal_verdict_gate_module() + publish_archive_payload = _sample_release_final_publish_archive_payload() + drift_payload = _sample_gate_manifest_drift_payload() + + publish_archive_path = tmp_path / "release_final_publish_archive.json" + drift_path = tmp_path / "linux_gate_manifest_drift.json" + publish_archive_path.write_text( + json.dumps(publish_archive_payload, indent=2), + encoding="utf-8", + ) + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + publish_archive_report = gate.load_release_final_publish_archive_report(publish_archive_path) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + payload = gate.build_terminal_verdict_payload( + publish_archive_report, + drift_report, + release_source_path=publish_archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + + assert payload["terminal_verdict_status"] == "ready_for_linux_validation" + assert payload["terminal_verdict_decision"] == "proceed_linux_validation" + assert payload["terminal_verdict_exit_code"] == 0 + assert payload["terminal_verdict_should_proceed"] is True + assert payload["terminal_verdict_requires_manual_action"] is False + assert payload["terminal_verdict_channel"] == "release" + assert payload["gate_manifest_drift_status"] == "passed" + assert payload["gate_manifest_drift_missing_runtime_tests"] == [] + assert payload["gate_manifest_drift_missing_manifest_entries"] == [] + assert payload["gate_manifest_drift_orphan_manifest_entries"] == [] + + +def test_build_terminal_verdict_payload_ready_with_follow_up_for_linux_validation(tmp_path: Path): + gate = _load_ci_workflow_terminal_verdict_gate_module() + publish_archive_payload = _sample_release_final_publish_archive_payload() + publish_archive_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_archive_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_archive_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_archive_payload["release_final_outcome_exit_code"] = 1 + publish_archive_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_terminal_publish_decision"] = ( + "announce_release_with_follow_up" + ) + publish_archive_payload["release_final_terminal_publish_exit_code"] = 1 + publish_archive_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_archive_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_archive_payload["release_final_handoff_exit_code"] = 1 + publish_archive_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_archive_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_archive_payload["release_final_closure_exit_code"] = 1 + publish_archive_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_archive_payload["release_final_closure_publish_exit_code"] = 1 + publish_archive_payload["release_final_archive_status"] = "archived_with_follow_up" + publish_archive_payload["release_final_archive_decision"] = ( + "archive_release_closed_with_follow_up" + ) + publish_archive_payload["release_final_archive_exit_code"] = 1 + publish_archive_payload["release_final_verdict_status"] = "released_with_follow_up" + publish_archive_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + publish_archive_payload["release_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + publish_archive_payload["release_final_verdict_publish_exit_code"] = 1 + publish_archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_archive_payload["final_publish_archive_should_archive"] = True + publish_archive_payload["final_publish_archive_requires_manual_action"] = False + publish_archive_payload["final_publish_archive_channel"] = "follow_up" + publish_archive_payload["release_final_publish_archive_status"] = "archived_with_follow_up" + publish_archive_payload["release_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + publish_archive_payload["release_final_publish_archive_exit_code"] = 1 + publish_archive_payload["reason_codes"] = [ + "release_final_publish_archive_archived_with_follow_up" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + publish_archive_path = tmp_path / "release_final_publish_archive_with_follow_up.json" + drift_path = tmp_path / "linux_gate_manifest_drift.json" + publish_archive_path.write_text( + json.dumps(publish_archive_payload, indent=2), + encoding="utf-8", + ) + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + publish_archive_report = gate.load_release_final_publish_archive_report(publish_archive_path) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + payload = gate.build_terminal_verdict_payload( + publish_archive_report, + drift_report, + release_source_path=publish_archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + + assert payload["terminal_verdict_status"] == "ready_with_follow_up_for_linux_validation" + assert payload["terminal_verdict_decision"] == "proceed_linux_validation_with_follow_up" + assert payload["terminal_verdict_exit_code"] == 1 + assert payload["terminal_verdict_should_proceed"] is True + assert payload["terminal_verdict_requires_manual_action"] is False + assert payload["terminal_verdict_channel"] == "follow_up" + + +def test_build_terminal_verdict_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_terminal_verdict_gate_module() + publish_archive_payload = _sample_release_final_publish_archive_payload() + publish_archive_payload["release_delivery_final_verdict_status"] = "blocked" + publish_archive_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + publish_archive_payload["release_delivery_final_verdict_exit_code"] = 1 + publish_archive_payload["release_follow_up_final_verdict_status"] = "blocked" + publish_archive_payload["release_follow_up_final_verdict_decision"] = ( + "escalate_queue_failure" + ) + publish_archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_outcome_status"] = "blocked" + publish_archive_payload["release_final_outcome_decision"] = "escalate_blocker" + publish_archive_payload["release_final_outcome_exit_code"] = 1 + publish_archive_payload["release_final_terminal_publish_status"] = "blocked" + publish_archive_payload["release_final_terminal_publish_decision"] = "announce_blocker" + publish_archive_payload["release_final_terminal_publish_exit_code"] = 1 + publish_archive_payload["release_final_handoff_status"] = "blocked" + publish_archive_payload["release_final_handoff_decision"] = "handoff_blocker" + publish_archive_payload["release_final_handoff_exit_code"] = 1 + publish_archive_payload["release_final_closure_status"] = "blocked" + publish_archive_payload["release_final_closure_decision"] = "close_blocker" + publish_archive_payload["release_final_closure_exit_code"] = 1 + publish_archive_payload["release_final_closure_publish_status"] = "blocked" + publish_archive_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + publish_archive_payload["release_final_closure_publish_exit_code"] = 1 + publish_archive_payload["release_final_archive_status"] = "blocked" + publish_archive_payload["release_final_archive_decision"] = "archive_release_blocker" + publish_archive_payload["release_final_archive_exit_code"] = 1 + publish_archive_payload["release_final_verdict_status"] = "blocked" + publish_archive_payload["release_final_verdict_decision"] = "escalate_release_blocker" + publish_archive_payload["release_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_verdict_publish_status"] = "blocked" + publish_archive_payload["release_final_verdict_publish_decision"] = "announce_release_blocker" + publish_archive_payload["release_final_verdict_publish_exit_code"] = 1 + publish_archive_payload["final_publish_archive_should_archive"] = False + publish_archive_payload["final_publish_archive_requires_manual_action"] = True + publish_archive_payload["final_publish_archive_channel"] = "blocker" + publish_archive_payload["release_final_publish_archive_status"] = "blocked" + publish_archive_payload["release_final_publish_archive_decision"] = "archive_release_blocker" + publish_archive_payload["release_final_publish_archive_exit_code"] = 1 + publish_archive_payload["reason_codes"] = ["release_final_publish_archive_blocked"] + + drift_payload = _sample_gate_manifest_drift_payload() + + publish_archive_path = tmp_path / "release_final_publish_archive_blocked.json" + drift_path = tmp_path / "linux_gate_manifest_drift.json" + publish_archive_path.write_text( + json.dumps(publish_archive_payload, indent=2), + encoding="utf-8", + ) + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + publish_archive_report = gate.load_release_final_publish_archive_report(publish_archive_path) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + payload = gate.build_terminal_verdict_payload( + publish_archive_report, + drift_report, + release_source_path=publish_archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + + assert payload["terminal_verdict_status"] == "blocked" + assert payload["terminal_verdict_decision"] == "halt_linux_validation_blocker" + assert payload["terminal_verdict_exit_code"] == 1 + assert payload["terminal_verdict_should_proceed"] is False + assert payload["terminal_verdict_requires_manual_action"] is True + assert payload["terminal_verdict_channel"] == "blocker" + + +def test_build_terminal_verdict_payload_rejects_mismatch_and_drift_failure(tmp_path: Path): + gate = _load_ci_workflow_terminal_verdict_gate_module() + publish_archive_payload = _sample_release_final_publish_archive_payload() + publish_archive_payload["release_final_publish_archive_status"] = "archived" + publish_archive_payload["release_final_publish_archive_decision"] = "archive_release_blocker" + publish_archive_payload["final_publish_archive_should_archive"] = False + publish_archive_payload["final_publish_archive_requires_manual_action"] = True + publish_archive_payload["final_publish_archive_channel"] = "blocker" + + drift_payload = _sample_gate_manifest_drift_payload() + drift_payload["status"] = "failed" + drift_payload["missing_runtime_tests"] = ["test_missing_gate_runtime.py"] + drift_payload["missing_manifest_entries"] = ["tests/test_missing_gate_runtime.py"] + drift_payload["orphan_manifest_entries"] = ["tests/test_orphan_gate_runtime.py"] + drift_payload["structural_issues"] = ["missing_gate_runtime_tests"] + + publish_archive_path = tmp_path / "release_final_publish_archive_mismatch.json" + drift_path = tmp_path / "linux_gate_manifest_drift_failed.json" + publish_archive_path.write_text( + json.dumps(publish_archive_payload, indent=2), + encoding="utf-8", + ) + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + publish_archive_report = gate.load_release_final_publish_archive_report(publish_archive_path) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + payload = gate.build_terminal_verdict_payload( + publish_archive_report, + drift_report, + release_source_path=publish_archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + + assert payload["terminal_verdict_status"] == "contract_failed" + assert payload["terminal_verdict_decision"] == "abort_linux_validation" + assert payload["terminal_verdict_exit_code"] == 1 + assert payload["terminal_verdict_should_proceed"] is False + assert payload["terminal_verdict_requires_manual_action"] is True + assert payload["terminal_verdict_channel"] == "blocker" + assert "final_publish_archive_decision_mismatch" in payload["structural_issues"] + assert "gate_manifest_drift_failed" in payload["structural_issues"] + assert "gate_manifest_drift_missing_runtime_tests" in payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_terminal_verdict_gate_module() + publish_archive_payload = _sample_release_final_publish_archive_payload() + publish_archive_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + publish_archive_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + publish_archive_payload["release_follow_up_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_outcome_status"] = "released_with_follow_up" + publish_archive_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + publish_archive_payload["release_final_outcome_exit_code"] = 1 + publish_archive_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_terminal_publish_decision"] = ( + "announce_release_with_follow_up" + ) + publish_archive_payload["release_final_terminal_publish_exit_code"] = 1 + publish_archive_payload["release_final_handoff_status"] = "completed_with_follow_up" + publish_archive_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + publish_archive_payload["release_final_handoff_exit_code"] = 1 + publish_archive_payload["release_final_closure_status"] = "closed_with_follow_up" + publish_archive_payload["release_final_closure_decision"] = "close_with_follow_up" + publish_archive_payload["release_final_closure_exit_code"] = 1 + publish_archive_payload["release_final_closure_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + publish_archive_payload["release_final_closure_publish_exit_code"] = 1 + publish_archive_payload["release_final_archive_status"] = "archived_with_follow_up" + publish_archive_payload["release_final_archive_decision"] = ( + "archive_release_closed_with_follow_up" + ) + publish_archive_payload["release_final_archive_exit_code"] = 1 + publish_archive_payload["release_final_verdict_status"] = "released_with_follow_up" + publish_archive_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + publish_archive_payload["release_final_verdict_exit_code"] = 1 + publish_archive_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + publish_archive_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + publish_archive_payload["release_final_verdict_publish_exit_code"] = 1 + publish_archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_archive_payload["final_publish_archive_should_archive"] = True + publish_archive_payload["final_publish_archive_requires_manual_action"] = False + publish_archive_payload["final_publish_archive_channel"] = "follow_up" + publish_archive_payload["release_final_publish_archive_status"] = "archived_with_follow_up" + publish_archive_payload["release_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + publish_archive_payload["release_final_publish_archive_exit_code"] = 1 + publish_archive_payload["reason_codes"] = [ + "release_final_publish_archive_archived_with_follow_up" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + publish_archive_path = tmp_path / "release_final_publish_archive_with_follow_up.json" + drift_path = tmp_path / "linux_gate_manifest_drift.json" + publish_archive_path.write_text( + json.dumps(publish_archive_payload, indent=2), + encoding="utf-8", + ) + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + publish_archive_report = gate.load_release_final_publish_archive_report(publish_archive_path) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + payload = gate.build_terminal_verdict_payload( + publish_archive_report, + drift_report, + release_source_path=publish_archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + outputs = gate.build_github_output_values( + payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_terminal_verdict.md" + ), + ) + + assert outputs["workflow_terminal_verdict_status"] == "ready_with_follow_up_for_linux_validation" + assert ( + outputs["workflow_terminal_verdict_decision"] + == "proceed_linux_validation_with_follow_up" + ) + assert outputs["workflow_terminal_verdict_exit_code"] == "1" + assert outputs["workflow_terminal_verdict_should_proceed"] == "true" + assert outputs["workflow_terminal_verdict_requires_manual_action"] == "false" + assert outputs["workflow_terminal_verdict_channel"] == "follow_up" + assert outputs["workflow_terminal_verdict_gate_manifest_drift_status"] == "passed" + assert ( + outputs["workflow_terminal_verdict_gate_manifest_drift_missing_runtime_tests"] + == "0" + ) + assert ( + outputs["workflow_terminal_verdict_gate_manifest_drift_missing_manifest_entries"] + == "0" + ) + assert ( + outputs["workflow_terminal_verdict_gate_manifest_drift_orphan_manifest_entries"] + == "0" + ) + assert outputs["workflow_terminal_verdict_release_final_publish_archive_status"] == ( + "archived_with_follow_up" + ) + assert outputs["workflow_terminal_verdict_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_terminal_verdict_run_id"] == "1234567890" + assert outputs["workflow_terminal_verdict_run_url"].endswith("/actions/runs/1234567890") + assert outputs["workflow_terminal_verdict_report_json"].endswith( + "ci_workflow_terminal_verdict.json" + ) + assert outputs["workflow_terminal_verdict_report_markdown"].endswith( + "ci_workflow_terminal_verdict.md" + ) diff --git a/tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py b/tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py new file mode 100644 index 0000000..1d3c56a --- /dev/null +++ b/tests/test_p2_linux_ci_workflow_yaml_gate_runtime.py @@ -0,0 +1,140 @@ +"""Contract tests for P2-18 Linux CI workflow YAML render gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_yaml_gate_module(): + script_path = Path("scripts") / "run_p2_linux_ci_workflow_yaml_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_ci_workflow_yaml_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_ci_workflow_plan() -> dict: + return { + "generated_at": "2026-04-30T00:00:00+00:00", + "source_ci_matrix": "/tmp/ci_matrix.json", + "shard_total": 3, + "manifest_total_tests": 5, + "selected_shards": 3, + "skipped_empty_shards": False, + "fan_out_matrix": { + "include": [ + { + "job_id": "linux-shard-1", + "shard_index": 1, + "shard_total": 3, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-1", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 1", + }, + { + "job_id": "linux-shard-2", + "shard_index": 2, + "shard_total": 3, + "total_tests": 2, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-2", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 2", + }, + { + "job_id": "linux-shard-3", + "shard_index": 3, + "shard_total": 3, + "total_tests": 1, + "report_dir": ".claude/reports/linux_unified_gate/shards/shard-3", + "summary_path": ".claude/reports/linux_unified_gate/shards/shard-3/summary.json", + "command": "python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 3", + }, + ] + }, + "fan_in": { + "summary_paths": [ + ".claude/reports/linux_unified_gate/shards/shard-1/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-2/summary.json", + ".claude/reports/linux_unified_gate/shards/shard-3/summary.json", + ], + "merged_summary_path": ".claude/reports/linux_unified_gate/merged_summary.json", + "aggregation_command": "python scripts/run_p2_linux_shard_aggregation_gate.py --output .claude/reports/linux_unified_gate/merged_summary.json --summary .claude/reports/linux_unified_gate/shards/shard-1/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-2/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-3/summary.json", + "publish_command": "python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md", + "final_report_json": ".claude/reports/linux_unified_gate/final_report.json", + "final_report_markdown": ".claude/reports/linux_unified_gate/final_report.md", + }, + } + + +def test_build_ci_workflow_yaml_contains_fan_out_and_fan_in_contract(): + gate = _load_ci_workflow_yaml_gate_module() + workflow_yaml = gate.build_ci_workflow_yaml( + _sample_ci_workflow_plan(), + workflow_name="Linux Unified Gate", + python_version="3.11", + artifact_prefix="linux-unified-summary", + ) + + assert "name: \"Linux Unified Gate\"" in workflow_yaml + assert "linux_fan_out:" in workflow_yaml + assert "linux_fan_in:" in workflow_yaml + assert "shard_index: 1" in workflow_yaml + assert "command: \"python scripts/run_p2_linux_unified_execution_gate.py --shard-total 3 --shard-index 1\"" in workflow_yaml + assert "run: ${{ matrix.command }}" in workflow_yaml + assert "run: \"python scripts/run_p2_linux_shard_aggregation_gate.py --output .claude/reports/linux_unified_gate/merged_summary.json --summary .claude/reports/linux_unified_gate/shards/shard-1/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-2/summary.json --summary .claude/reports/linux_unified_gate/shards/shard-3/summary.json\"" in workflow_yaml + assert "run: \"python scripts/run_p2_linux_report_publish_gate.py --merged-summary .claude/reports/linux_unified_gate/merged_summary.json --output-json .claude/reports/linux_unified_gate/final_report.json --output-markdown .claude/reports/linux_unified_gate/final_report.md\"" in workflow_yaml + assert "pattern: \"linux-unified-summary-*\"" in workflow_yaml + assert "merge-multiple: true" in workflow_yaml + + +def test_build_render_metadata_contract(): + gate = _load_ci_workflow_yaml_gate_module() + metadata = gate.build_render_metadata( + _sample_ci_workflow_plan(), + source_plan_path=Path("reports/ci_workflow_plan.json"), + workflow_output_path=Path(".github/workflows/linux_unified_gate.yml"), + artifact_prefix="linux-unified-summary", + ) + + assert metadata["source_ci_workflow_plan"].endswith("reports/ci_workflow_plan.json") + assert metadata["workflow_output_path"].endswith(".github/workflows/linux_unified_gate.yml") + assert metadata["selected_shards"] == 3 + assert metadata["manifest_total_tests"] == 5 + assert metadata["fan_in"]["merged_summary_path"].endswith("merged_summary.json") + + +def test_load_ci_workflow_plan_rejects_summary_order_mismatch(tmp_path: Path): + gate = _load_ci_workflow_yaml_gate_module() + payload = _sample_ci_workflow_plan() + payload["fan_in"]["summary_paths"] = list(reversed(payload["fan_in"]["summary_paths"])) + path = tmp_path / "ci_workflow_plan_bad_summary_order.json" + path.write_text(json.dumps(payload), encoding="utf-8") + + try: + gate.load_ci_workflow_plan(path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_load_ci_workflow_plan_rejects_selected_shards_mismatch(tmp_path: Path): + gate = _load_ci_workflow_yaml_gate_module() + payload = _sample_ci_workflow_plan() + payload["selected_shards"] = 2 + path = tmp_path / "ci_workflow_plan_bad_selected.json" + path.write_text(json.dumps(payload), encoding="utf-8") + + try: + gate.load_ci_workflow_plan(path) + raised = False + except ValueError: + raised = True + + assert raised diff --git a/tests/test_p2_linux_gate_manifest_drift_gate_runtime.py b/tests/test_p2_linux_gate_manifest_drift_gate_runtime.py new file mode 100644 index 0000000..ea2678c --- /dev/null +++ b/tests/test_p2_linux_gate_manifest_drift_gate_runtime.py @@ -0,0 +1,77 @@ +"""Contract tests for P2-57 Linux gate manifest drift gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_linux_gate_manifest_drift_gate_module(): + script_path = Path("scripts") / "run_p2_linux_gate_manifest_drift_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_gate_manifest_drift_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_collect_gate_script_basenames_includes_new_gate(): + gate = _load_p2_linux_gate_manifest_drift_gate_module() + scripts = gate.collect_gate_script_basenames(scripts_dir=Path("scripts")) + + assert "run_p2_linux_gate_manifest_drift_gate.py" in scripts + assert "run_linux_unified_gate.py" not in scripts + assert all(name.startswith("run_") and name.endswith("_gate.py") for name in scripts) + + +def test_expected_runtime_test_for_gate_contract(): + gate = _load_p2_linux_gate_manifest_drift_gate_module() + runtime_test = gate.expected_runtime_test_for_gate("run_p2_linux_ci_matrix_gate.py") + assert runtime_test == "test_p2_linux_ci_matrix_gate_runtime.py" + + +def test_build_manifest_drift_report_passes_in_repo_contract(): + gate = _load_p2_linux_gate_manifest_drift_gate_module() + report = gate.build_manifest_drift_report(project_root=Path(".")) + + assert report["status"] == "passed" + assert report["missing_runtime_tests"] == [] + assert report["missing_manifest_entries"] == [] + assert report["orphan_manifest_entries"] == [] + assert "run_p2_linux_gate_manifest_drift_gate.py" in report["gate_scripts_scanned"] + assert ( + "test_p2_linux_gate_manifest_drift_gate_runtime.py" + in report["gate_runtime_tests_expected"] + ) + + +def test_build_github_output_values_contract(): + gate = _load_p2_linux_gate_manifest_drift_gate_module() + payload = { + "status": "failed", + "missing_runtime_tests": ["test_a.py"], + "missing_manifest_entries": ["tests/test_a.py"], + "orphan_manifest_entries": ["tests/test_orphan.py"], + } + outputs = gate.build_github_output_values( + payload=payload, + output_json=Path(".claude/reports/linux_unified_gate/linux_gate_manifest_drift.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/linux_gate_manifest_drift.md" + ), + ) + + assert outputs["linux_gate_manifest_drift_status"] == "failed" + assert outputs["linux_gate_manifest_drift_missing_runtime_tests"] == "1" + assert outputs["linux_gate_manifest_drift_missing_manifest_entries"] == "1" + assert outputs["linux_gate_manifest_drift_orphan_manifest_entries"] == "1" + assert outputs["linux_gate_manifest_drift_report_json"].endswith( + "linux_gate_manifest_drift.json" + ) + assert outputs["linux_gate_manifest_drift_report_markdown"].endswith( + "linux_gate_manifest_drift.md" + ) diff --git a/tests/test_p2_linux_report_publish_gate_runtime.py b/tests/test_p2_linux_report_publish_gate_runtime.py new file mode 100644 index 0000000..81365c5 --- /dev/null +++ b/tests/test_p2_linux_report_publish_gate_runtime.py @@ -0,0 +1,97 @@ +"""Contract tests for P2-13 Linux report publish gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_publish_gate_module(): + script_path = Path("scripts") / "run_p2_linux_report_publish_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_report_publish_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _valid_merged_summary() -> dict: + return { + "manifest_total_tests": 6, + "shard_total": 2, + "shards_present": [1, 2], + "shards_missing": [], + "totals": { + "total_tests": 6, + "passed": 6, + "failed": 0, + }, + "structural_issues": [], + "overall_status": "passed", + } + + +def test_load_merged_summary_validates_required_contract(tmp_path: Path): + gate = _load_publish_gate_module() + summary_path = tmp_path / "merged_summary.json" + summary_path.write_text( + ( + "{" + '"manifest_total_tests": 6,' + '"shard_total": 2,' + '"shards_present": [1, 2],' + '"shards_missing": [],' + '"totals": {"total_tests": 6, "passed": 6, "failed": 0},' + '"structural_issues": [],' + '"overall_status": "passed"' + "}" + ), + encoding="utf-8", + ) + + payload = gate.load_merged_summary(summary_path) + assert payload["manifest_total_tests"] == 6 + assert payload["totals"]["passed"] == 6 + assert payload["overall_status"] == "passed" + + +def test_build_publish_payload_happy_path_status_passed(): + gate = _load_publish_gate_module() + payload = gate.build_publish_payload( + _valid_merged_summary(), + source_path=Path("merged_summary.json"), + ) + + assert payload["overall_status"] == "passed" + assert payload["totals"]["total_tests"] == 6 + assert payload["notes"] == [] + + +def test_build_publish_payload_marks_failed_when_status_mismatch(): + gate = _load_publish_gate_module() + merged = _valid_merged_summary() + merged["overall_status"] = "failed" + payload = gate.build_publish_payload( + merged, + source_path=Path("merged_summary.json"), + ) + + assert payload["overall_status"] == "failed" + assert payload["notes"] + assert "overall_status mismatch" in payload["notes"][0] + + +def test_render_markdown_report_contains_required_sections(): + gate = _load_publish_gate_module() + payload = gate.build_publish_payload( + _valid_merged_summary(), + source_path=Path("merged_summary.json"), + ) + report = gate.render_markdown_report(payload) + + assert "Linux Unified Gate Final Report" in report + assert "## Totals" in report + assert "## Shards" in report + assert "## Structural Issues" in report + assert "## Notes" in report diff --git a/tests/test_p2_linux_shard_aggregation_gate_runtime.py b/tests/test_p2_linux_shard_aggregation_gate_runtime.py new file mode 100644 index 0000000..9fea288 --- /dev/null +++ b/tests/test_p2_linux_shard_aggregation_gate_runtime.py @@ -0,0 +1,134 @@ +"""Contract tests for P2-12 Linux shard aggregation gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_aggregation_gate_module(): + script_path = Path("scripts") / "run_p2_linux_shard_aggregation_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_shard_aggregation_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_discover_summary_files_deduplicates_across_inputs(tmp_path: Path): + gate = _load_aggregation_gate_module() + summary = tmp_path / "summary.json" + summary.write_text("{}", encoding="utf-8") + + nested = tmp_path / "nested" + nested.mkdir(parents=True, exist_ok=True) + nested_summary = nested / "summary.json" + nested_summary.write_text("{}", encoding="utf-8") + + files = gate.discover_summary_files( + summary_paths=[str(summary)], + summary_globs=[str(tmp_path / "**" / "summary.json")], + artifacts_dir=str(tmp_path), + ) + + normalized = {path.resolve() for path in files} + assert summary.resolve() in normalized + assert nested_summary.resolve() in normalized + + +def test_load_shard_summary_validates_required_contract(tmp_path: Path): + gate = _load_aggregation_gate_module() + summary = tmp_path / "summary.json" + summary.write_text( + json.dumps( + { + "manifest_total_tests": 6, + "total_tests": 3, + "passed": 3, + "failed": 0, + "shard_total": 2, + "shard_index": 1, + "report_dir": "reports/shard-1", + } + ), + encoding="utf-8", + ) + + loaded = gate.load_shard_summary(summary) + assert loaded["manifest_total_tests"] == 6 + assert loaded["total_tests"] == 3 + assert loaded["passed"] == 3 + assert loaded["failed"] == 0 + assert loaded["shard_total"] == 2 + assert loaded["shard_index"] == 1 + + +def test_aggregate_shard_summaries_happy_path(): + gate = _load_aggregation_gate_module() + merged = gate.aggregate_shard_summaries( + [ + { + "path": "summary-1.json", + "manifest_total_tests": 6, + "total_tests": 3, + "passed": 3, + "failed": 0, + "shard_total": 2, + "shard_index": 1, + "report_dir": "report-1", + }, + { + "path": "summary-2.json", + "manifest_total_tests": 6, + "total_tests": 3, + "passed": 3, + "failed": 0, + "shard_total": 2, + "shard_index": 2, + "report_dir": "report-2", + }, + ] + ) + + assert merged["overall_status"] == "passed" + assert merged["totals"]["total_tests"] == 6 + assert merged["totals"]["passed"] == 6 + assert merged["totals"]["failed"] == 0 + assert merged["shards_missing"] == [] + assert merged["structural_issues"] == [] + + +def test_aggregate_shard_summaries_detects_missing_shard_and_total_drift(): + gate = _load_aggregation_gate_module() + merged = gate.aggregate_shard_summaries( + [ + { + "path": "summary-1.json", + "manifest_total_tests": 6, + "total_tests": 2, + "passed": 2, + "failed": 0, + "shard_total": 3, + "shard_index": 1, + "report_dir": "report-1", + }, + { + "path": "summary-3.json", + "manifest_total_tests": 6, + "total_tests": 2, + "passed": 2, + "failed": 0, + "shard_total": 3, + "shard_index": 3, + "report_dir": "report-3", + }, + ] + ) + + assert merged["overall_status"] == "failed" + assert merged["shards_missing"] == [2] + issues = " | ".join(merged["structural_issues"]) + assert "missing shard indexes" in issues + assert "aggregated total_tests" in issues diff --git a/tests/test_p2_linux_shard_plan_gate_runtime.py b/tests/test_p2_linux_shard_plan_gate_runtime.py new file mode 100644 index 0000000..3e89002 --- /dev/null +++ b/tests/test_p2_linux_shard_plan_gate_runtime.py @@ -0,0 +1,73 @@ +"""Contract tests for P2-15 Linux shard plan gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_shard_plan_gate_module(): + script_path = Path("scripts") / "run_p2_linux_shard_plan_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_shard_plan_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_shard_plan_payload_distributes_tests_deterministically(): + gate = _load_shard_plan_gate_module() + payload = gate.build_shard_plan_payload( + python_executable="python", + test_files=["tests/a.py", "tests/b.py", "tests/c.py", "tests/d.py", "tests/e.py"], + shard_total=3, + report_root=Path(".claude/reports/linux_unified_gate/shards"), + continue_on_failure=False, + pytest_args=None, + ) + + assert payload["manifest_total_tests"] == 5 + assert payload["shard_total"] == 3 + assert len(payload["shards"]) == 3 + + assert payload["shards"][0]["test_files"] == ["tests/a.py", "tests/d.py"] + assert payload["shards"][1]["test_files"] == ["tests/b.py", "tests/e.py"] + assert payload["shards"][2]["test_files"] == ["tests/c.py"] + + +def test_build_shard_plan_payload_command_contract_includes_forwarded_args(): + gate = _load_shard_plan_gate_module() + payload = gate.build_shard_plan_payload( + python_executable="python3", + test_files=["tests/a.py"], + shard_total=1, + report_root=Path("reports/shards"), + continue_on_failure=True, + pytest_args=["-k", "daemon", "--maxfail", "1"], + ) + + shard = payload["shards"][0] + assert shard["command_parts"][:2] == ["python3", "scripts/run_p2_linux_unified_execution_gate.py"] + assert "--continue-on-failure" in shard["command_parts"] + assert "--" in shard["command_parts"] + assert shard["command_parts"][-4:] == ["-k", "daemon", "--maxfail", "1"] + assert shard["summary_path"].endswith("shard-1/summary.json") + + +def test_build_shard_plan_payload_rejects_invalid_shard_total(): + gate = _load_shard_plan_gate_module() + try: + gate.build_shard_plan_payload( + python_executable="python", + test_files=["tests/a.py"], + shard_total=0, + report_root=Path("reports/shards"), + continue_on_failure=False, + pytest_args=[], + ) + raised = False + except ValueError: + raised = True + + assert raised diff --git a/tests/test_p2_linux_unified_execution_gate_runtime.py b/tests/test_p2_linux_unified_execution_gate_runtime.py new file mode 100644 index 0000000..fc8d72b --- /dev/null +++ b/tests/test_p2_linux_unified_execution_gate_runtime.py @@ -0,0 +1,140 @@ +"""Contract tests for P2-09 Linux unified execution gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_execution_gate_module(): + script_path = Path("scripts") / "run_p2_linux_unified_execution_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_unified_execution_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _load_unified_manifest_module(): + script_path = Path("scripts") / "run_linux_unified_gate.py" + spec = importlib.util.spec_from_file_location("linux_unified_gate_script", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_linux_unified_execution_gate_tracks_unified_manifest(): + gate = _load_execution_gate_module() + manifest = _load_unified_manifest_module() + + test_files = tuple(manifest.LINUX_UNIFIED_TEST_FILES) + report_dir = Path("tmp/report") + commands = gate.build_gate_commands( + python_executable="python-custom", + test_files=test_files, + report_dir=report_dir, + extra_pytest_args=None, + ) + + assert len(commands) == len(test_files) + assert commands + for idx, test_file in enumerate(test_files): + command = commands[idx] + assert command[:6] == ["python-custom", "-m", "pytest", "-q", "-c", "pytest.ini"] + assert command[6] == test_file + assert command[7:9] == ["--junitxml", str(report_dir / f"{Path(test_file).stem}.xml")] + + +def test_p2_linux_unified_execution_gate_applies_extra_pytest_args(): + gate = _load_execution_gate_module() + commands = gate.build_gate_commands( + python_executable="python", + test_files=["tests/test_main_runtime.py"], + report_dir=Path("tmp/report"), + extra_pytest_args=["-k", "daemon", "--maxfail", "1"], + ) + + assert len(commands) == 1 + assert commands[0][-4:] == ["-k", "daemon", "--maxfail", "1"] + + +def test_p2_linux_unified_execution_gate_selects_deterministic_shard_slice(): + gate = _load_execution_gate_module() + files = [ + "tests/a.py", + "tests/b.py", + "tests/c.py", + "tests/d.py", + "tests/e.py", + ] + + shard_1 = gate.select_shard_test_files(files, shard_total=3, shard_index=1) + shard_2 = gate.select_shard_test_files(files, shard_total=3, shard_index=2) + shard_3 = gate.select_shard_test_files(files, shard_total=3, shard_index=3) + + assert shard_1 == ["tests/a.py", "tests/d.py"] + assert shard_2 == ["tests/b.py", "tests/e.py"] + assert shard_3 == ["tests/c.py"] + + +def test_p2_linux_unified_execution_gate_rejects_invalid_shard_arguments(): + gate = _load_execution_gate_module() + files = ["tests/a.py"] + + try: + gate.select_shard_test_files(files, shard_total=0, shard_index=1) + raised_total = False + except ValueError: + raised_total = True + + try: + gate.select_shard_test_files(files, shard_total=2, shard_index=3) + raised_index = False + except ValueError: + raised_index = True + + assert raised_total + assert raised_index + + +def test_p2_linux_unified_execution_gate_summary_includes_shard_metadata(): + gate = _load_execution_gate_module() + payload = gate._build_result_payload( + python_executable="python", + report_dir=Path("tmp/report"), + manifest_test_files=["tests/a.py", "tests/b.py", "tests/c.py"], + selected_test_files=["tests/a.py", "tests/c.py"], + shard_total=2, + shard_index=1, + commands=[["python", "-m", "pytest", "tests/a.py"]], + per_test_status=[{"test_file": "tests/a.py", "exit_code": 0, "junit_xml": "tmp/a.xml"}], + ) + + assert payload["manifest_total_tests"] == 3 + assert payload["total_tests"] == 2 + assert payload["shard_total"] == 2 + assert payload["shard_index"] == 1 + + +def test_p2_linux_unified_execution_gate_empty_shard_summary_contract(): + gate = _load_execution_gate_module() + payload = gate._build_result_payload( + python_executable="python", + report_dir=Path("tmp/report"), + manifest_test_files=["tests/a.py", "tests/b.py"], + selected_test_files=[], + shard_total=3, + shard_index=3, + commands=[], + per_test_status=[], + ) + + assert payload["manifest_total_tests"] == 2 + assert payload["total_tests"] == 0 + assert payload["passed"] == 0 + assert payload["failed"] == 0 + assert payload["shard_total"] == 3 + assert payload["shard_index"] == 3 diff --git a/tests/test_p2_linux_unified_pipeline_gate_runtime.py b/tests/test_p2_linux_unified_pipeline_gate_runtime.py new file mode 100644 index 0000000..bf17e84 --- /dev/null +++ b/tests/test_p2_linux_unified_pipeline_gate_runtime.py @@ -0,0 +1,110 @@ +"""Contract tests for P2-14 Linux unified pipeline gate.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_pipeline_gate_module(): + script_path = Path("scripts") / "run_p2_linux_unified_pipeline_gate.py" + spec = importlib.util.spec_from_file_location("p2_linux_unified_pipeline_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_pipeline_commands_default_chain_includes_execution_summary_feed(): + gate = _load_pipeline_gate_module() + commands = gate.build_pipeline_commands( + python_executable="python", + execution_report_dir=Path(".claude/reports/linux_unified_gate"), + shard_total=3, + shard_index=2, + continue_on_failure=True, + skip_execution=False, + skip_aggregation=False, + skip_publish=False, + summary_paths=[], + summary_globs=[], + artifacts_dir=None, + merged_summary_output=Path(".claude/reports/linux_unified_gate/merged_summary.json"), + final_report_json=Path(".claude/reports/linux_unified_gate/final_report.json"), + final_report_markdown=Path(".claude/reports/linux_unified_gate/final_report.md"), + pytest_args=["-k", "daemon"], + ) + + assert [stage for stage, _ in commands] == ["execution", "aggregation", "publish"] + + execution_cmd = commands[0][1] + assert execution_cmd[:2] == ["python", "scripts/run_p2_linux_unified_execution_gate.py"] + assert "--continue-on-failure" in execution_cmd + assert "--" in execution_cmd + assert execution_cmd[-2:] == ["-k", "daemon"] + + aggregation_cmd = commands[1][1] + assert aggregation_cmd[:2] == ["python", "scripts/run_p2_linux_shard_aggregation_gate.py"] + assert "--summary" in aggregation_cmd + summary_index = aggregation_cmd.index("--summary") + 1 + assert aggregation_cmd[summary_index].endswith("/summary.json") + + publish_cmd = commands[2][1] + assert publish_cmd[:2] == ["python", "scripts/run_p2_linux_report_publish_gate.py"] + assert "--merged-summary" in publish_cmd + + +def test_build_pipeline_commands_skip_execution_uses_external_summary_inputs(): + gate = _load_pipeline_gate_module() + commands = gate.build_pipeline_commands( + python_executable="python3", + execution_report_dir=Path("ignored/report-dir"), + shard_total=1, + shard_index=1, + continue_on_failure=False, + skip_execution=True, + skip_aggregation=False, + skip_publish=True, + summary_paths=["artifacts/job1/summary.json"], + summary_globs=["artifacts/**/summary.json"], + artifacts_dir="artifacts", + merged_summary_output=Path("out/merged_summary.json"), + final_report_json=Path("out/final_report.json"), + final_report_markdown=Path("out/final_report.md"), + pytest_args=None, + ) + + assert [stage for stage, _ in commands] == ["aggregation"] + aggregation_cmd = commands[0][1] + assert aggregation_cmd[0] == "python3" + assert "--summary" in aggregation_cmd + assert "artifacts/job1/summary.json" in aggregation_cmd + assert "--summary-glob" in aggregation_cmd + assert "artifacts/**/summary.json" in aggregation_cmd + assert "--artifacts-dir" in aggregation_cmd + assert "artifacts" in aggregation_cmd + assert "ignored/report-dir/summary.json" not in aggregation_cmd + + +def test_build_pipeline_commands_skip_all_returns_empty_plan(): + gate = _load_pipeline_gate_module() + commands = gate.build_pipeline_commands( + python_executable="python", + execution_report_dir=Path("tmp"), + shard_total=1, + shard_index=1, + continue_on_failure=False, + skip_execution=True, + skip_aggregation=True, + skip_publish=True, + summary_paths=[], + summary_globs=[], + artifacts_dir=None, + merged_summary_output=Path("tmp/merged_summary.json"), + final_report_json=Path("tmp/final_report.json"), + final_report_markdown=Path("tmp/final_report.md"), + pytest_args=[], + ) + + assert commands == [] diff --git a/tests/test_p2_lv_dispatch_rt.py b/tests/test_p2_lv_dispatch_rt.py new file mode 100644 index 0000000..1635f5b --- /dev/null +++ b/tests/test_p2_lv_dispatch_rt.py @@ -0,0 +1,475 @@ +"""Contract tests for P2-60 Linux CI workflow Linux validation dispatch gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_dispatch_gate_module(): + script_path = ( + Path("scripts") / "run_p2_lv_dispatch_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_dispatch_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_terminal_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "source_release_final_verdict_publish_report": "/tmp/ci_workflow_release_final_verdict_publish.json", + "source_release_final_verdict_report": "/tmp/ci_workflow_release_final_verdict.json", + "source_release_final_archive_report": "/tmp/ci_workflow_release_final_archive.json", + "source_release_final_closure_publish_report": "/tmp/ci_workflow_release_final_closure_publish.json", + "source_release_final_closure_report": "/tmp/ci_workflow_release_final_closure.json", + "source_release_final_handoff_report": "/tmp/ci_workflow_release_final_handoff.json", + "source_release_final_terminal_publish_report": "/tmp/ci_workflow_release_final_terminal_publish.json", + "source_release_final_outcome_report": "/tmp/ci_workflow_release_final_outcome.json", + "source_release_delivery_final_verdict_report": "/tmp/ci_workflow_release_delivery_final_verdict.json", + "source_release_follow_up_final_verdict_report": "/tmp/ci_workflow_release_follow_up_final_verdict.json", + "release_delivery_final_verdict_status": "completed", + "release_delivery_final_verdict_decision": "close_release", + "release_delivery_final_verdict_exit_code": 0, + "release_follow_up_final_verdict_status": "completed", + "release_follow_up_final_verdict_decision": "close_follow_up", + "release_follow_up_final_verdict_exit_code": 0, + "release_final_outcome_status": "released", + "release_final_outcome_decision": "ship_and_close", + "release_final_outcome_exit_code": 0, + "release_final_terminal_publish_status": "published", + "release_final_terminal_publish_decision": "announce_release_closure", + "release_final_terminal_publish_exit_code": 0, + "release_final_handoff_status": "completed", + "release_final_handoff_decision": "handoff_release_closure", + "release_final_handoff_exit_code": 0, + "release_final_closure_status": "closed", + "release_final_closure_decision": "close_release", + "release_final_closure_exit_code": 0, + "release_final_closure_publish_status": "published", + "release_final_closure_publish_decision": "announce_release_closed", + "release_final_closure_publish_exit_code": 0, + "release_final_archive_status": "archived", + "release_final_archive_decision": "archive_release_closed", + "release_final_archive_exit_code": 0, + "release_final_verdict_status": "released", + "release_final_verdict_decision": "ship_release", + "release_final_verdict_exit_code": 0, + "release_final_verdict_publish_status": "published", + "release_final_verdict_publish_decision": "announce_release_shipped", + "release_final_verdict_publish_exit_code": 0, + "release_final_publish_archive_status": "archived", + "release_final_publish_archive_decision": "archive_release_shipped", + "release_final_publish_archive_exit_code": 0, + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_summary": ( + "release_final_publish_archive_status=archived " + "gate_manifest_drift_status=passed " + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation" + ), + "reason_codes": ["terminal_verdict_ready_for_linux_validation"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_publish_archive", + "path": "/tmp/ci_workflow_release_final_publish_archive.json", + "exists": True, + }, + { + "source": "manifest_drift", + "path": "/tmp/linux_gate_manifest_drift.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_dispatch_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_dispatch_gate_module() + terminal_payload = _sample_terminal_verdict_payload() + + terminal_path = tmp_path / "ci_workflow_terminal_verdict.json" + terminal_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + terminal_report = gate.load_terminal_verdict_report(terminal_path) + command_parts = gate.build_linux_validation_command_parts( + terminal_verdict_report=terminal_report, + python_executable="python", + linux_validation_command="", + ) + payload = gate.build_linux_validation_dispatch_payload( + terminal_report, + source_path=terminal_path.resolve(), + project_root=tmp_path.resolve(), + linux_validation_timeout_seconds=7200, + command_parts=command_parts, + dry_run=True, + command_result=None, + ) + + assert payload["linux_validation_dispatch_status"] == "ready_dry_run" + assert payload["linux_validation_dispatch_decision"] == "dispatch_linux_validation" + assert payload["linux_validation_dispatch_exit_code"] == 0 + assert payload["linux_validation_should_dispatch"] is True + assert payload["linux_validation_requires_manual_action"] is False + assert payload["linux_validation_channel"] == "release" + assert payload["linux_validation_dispatch_attempted"] is False + assert payload["linux_validation_command_parts"][:2] == [ + "python", + "scripts/run_p2_linux_unified_pipeline_gate.py", + ] + + +def test_build_linux_validation_dispatch_payload_ready_with_follow_up_dry_run(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_dispatch_gate_module() + terminal_payload = _sample_terminal_verdict_payload() + terminal_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + terminal_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + terminal_payload["release_final_outcome_status"] = "released_with_follow_up" + terminal_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + terminal_payload["release_final_outcome_exit_code"] = 1 + terminal_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_terminal_publish_decision"] = ( + "announce_release_with_follow_up" + ) + terminal_payload["release_final_terminal_publish_exit_code"] = 1 + terminal_payload["release_final_handoff_status"] = "completed_with_follow_up" + terminal_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + terminal_payload["release_final_handoff_exit_code"] = 1 + terminal_payload["release_final_closure_status"] = "closed_with_follow_up" + terminal_payload["release_final_closure_decision"] = "close_with_follow_up" + terminal_payload["release_final_closure_exit_code"] = 1 + terminal_payload["release_final_closure_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + terminal_payload["release_final_closure_publish_exit_code"] = 1 + terminal_payload["release_final_archive_status"] = "archived_with_follow_up" + terminal_payload["release_final_archive_decision"] = ( + "archive_release_closed_with_follow_up" + ) + terminal_payload["release_final_archive_exit_code"] = 1 + terminal_payload["release_final_verdict_status"] = "released_with_follow_up" + terminal_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + terminal_payload["release_final_verdict_exit_code"] = 1 + terminal_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + terminal_payload["release_final_verdict_publish_exit_code"] = 1 + terminal_payload["release_final_publish_archive_status"] = "archived_with_follow_up" + terminal_payload["release_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + terminal_payload["release_final_publish_archive_exit_code"] = 1 + terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_payload["terminal_verdict_channel"] = "follow_up" + terminal_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["terminal_verdict_summary"] = ( + "release_final_publish_archive_status=archived_with_follow_up " + "gate_manifest_drift_status=passed " + "terminal_verdict_status=ready_with_follow_up_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation_with_follow_up" + ) + terminal_payload["reason_codes"] = [ + "terminal_verdict_ready_with_follow_up_for_linux_validation" + ] + + terminal_path = tmp_path / "ci_workflow_terminal_verdict_with_follow_up.json" + terminal_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + terminal_report = gate.load_terminal_verdict_report(terminal_path) + command_parts = gate.build_linux_validation_command_parts( + terminal_verdict_report=terminal_report, + python_executable="python", + linux_validation_command="", + ) + payload = gate.build_linux_validation_dispatch_payload( + terminal_report, + source_path=terminal_path.resolve(), + project_root=tmp_path.resolve(), + linux_validation_timeout_seconds=7200, + command_parts=command_parts, + dry_run=True, + command_result=None, + ) + + assert payload["linux_validation_dispatch_status"] == "ready_with_follow_up_dry_run" + assert ( + payload["linux_validation_dispatch_decision"] + == "dispatch_linux_validation_with_follow_up" + ) + assert payload["linux_validation_dispatch_exit_code"] == 1 + assert payload["linux_validation_should_dispatch"] is True + assert payload["linux_validation_requires_manual_action"] is False + assert payload["linux_validation_channel"] == "follow_up" + assert payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_dispatch_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_dispatch_gate_module() + terminal_payload = _sample_terminal_verdict_payload() + terminal_payload["release_delivery_final_verdict_status"] = "blocked" + terminal_payload["release_delivery_final_verdict_decision"] = "escalate_blocker" + terminal_payload["release_delivery_final_verdict_exit_code"] = 1 + terminal_payload["release_follow_up_final_verdict_status"] = "blocked" + terminal_payload["release_follow_up_final_verdict_decision"] = "escalate_queue_failure" + terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + terminal_payload["release_final_outcome_status"] = "blocked" + terminal_payload["release_final_outcome_decision"] = "escalate_blocker" + terminal_payload["release_final_outcome_exit_code"] = 1 + terminal_payload["release_final_terminal_publish_status"] = "blocked" + terminal_payload["release_final_terminal_publish_decision"] = "announce_blocker" + terminal_payload["release_final_terminal_publish_exit_code"] = 1 + terminal_payload["release_final_handoff_status"] = "blocked" + terminal_payload["release_final_handoff_decision"] = "handoff_blocker" + terminal_payload["release_final_handoff_exit_code"] = 1 + terminal_payload["release_final_closure_status"] = "blocked" + terminal_payload["release_final_closure_decision"] = "close_blocker" + terminal_payload["release_final_closure_exit_code"] = 1 + terminal_payload["release_final_closure_publish_status"] = "blocked" + terminal_payload["release_final_closure_publish_decision"] = "announce_release_blocker" + terminal_payload["release_final_closure_publish_exit_code"] = 1 + terminal_payload["release_final_archive_status"] = "blocked" + terminal_payload["release_final_archive_decision"] = "archive_release_blocker" + terminal_payload["release_final_archive_exit_code"] = 1 + terminal_payload["release_final_verdict_status"] = "blocked" + terminal_payload["release_final_verdict_decision"] = "escalate_release_blocker" + terminal_payload["release_final_verdict_exit_code"] = 1 + terminal_payload["release_final_verdict_publish_status"] = "blocked" + terminal_payload["release_final_verdict_publish_decision"] = "announce_release_blocker" + terminal_payload["release_final_verdict_publish_exit_code"] = 1 + terminal_payload["release_final_publish_archive_status"] = "blocked" + terminal_payload["release_final_publish_archive_decision"] = "archive_release_blocker" + terminal_payload["release_final_publish_archive_exit_code"] = 1 + terminal_payload["terminal_verdict_should_proceed"] = False + terminal_payload["terminal_verdict_requires_manual_action"] = True + terminal_payload["terminal_verdict_channel"] = "blocker" + terminal_payload["terminal_verdict_status"] = "blocked" + terminal_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["terminal_verdict_summary"] = ( + "release_final_publish_archive_status=blocked " + "gate_manifest_drift_status=passed " + "terminal_verdict_status=blocked " + "terminal_verdict_decision=halt_linux_validation_blocker" + ) + terminal_payload["reason_codes"] = ["terminal_verdict_blocked"] + + terminal_path = tmp_path / "ci_workflow_terminal_verdict_blocked.json" + terminal_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + terminal_report = gate.load_terminal_verdict_report(terminal_path) + command_parts = gate.build_linux_validation_command_parts( + terminal_verdict_report=terminal_report, + python_executable="python", + linux_validation_command="", + ) + payload = gate.build_linux_validation_dispatch_payload( + terminal_report, + source_path=terminal_path.resolve(), + project_root=tmp_path.resolve(), + linux_validation_timeout_seconds=7200, + command_parts=command_parts, + dry_run=False, + command_result=None, + ) + + assert payload["linux_validation_dispatch_status"] == "blocked" + assert payload["linux_validation_dispatch_decision"] == "hold_linux_validation_blocker" + assert payload["linux_validation_dispatch_exit_code"] == 1 + assert payload["linux_validation_should_dispatch"] is False + assert payload["linux_validation_requires_manual_action"] is True + assert payload["linux_validation_channel"] == "blocker" + assert payload["linux_validation_dispatch_attempted"] is False + assert payload["linux_validation_command_parts"] == [] + + +def test_build_linux_validation_dispatch_payload_rejects_contract_mismatch_and_drift_failure( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_dispatch_gate_module() + terminal_payload = _sample_terminal_verdict_payload() + terminal_payload["terminal_verdict_status"] = "ready_for_linux_validation" + terminal_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + terminal_payload["terminal_verdict_should_proceed"] = False + terminal_payload["terminal_verdict_requires_manual_action"] = True + terminal_payload["terminal_verdict_channel"] = "blocker" + terminal_payload["gate_manifest_drift_status"] = "failed" + terminal_payload["gate_manifest_drift_missing_runtime_tests"] = [ + "test_missing_gate_runtime.py" + ] + terminal_payload["gate_manifest_drift_missing_manifest_entries"] = [ + "tests/test_missing_gate_runtime.py" + ] + terminal_payload["gate_manifest_drift_orphan_manifest_entries"] = [ + "tests/test_orphan_gate_runtime.py" + ] + terminal_payload["evidence_manifest"][1]["exists"] = False + + terminal_path = tmp_path / "ci_workflow_terminal_verdict_mismatch.json" + terminal_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + terminal_report = gate.load_terminal_verdict_report(terminal_path) + command_parts = gate.build_linux_validation_command_parts( + terminal_verdict_report=terminal_report, + python_executable="python", + linux_validation_command="", + ) + payload = gate.build_linux_validation_dispatch_payload( + terminal_report, + source_path=terminal_path.resolve(), + project_root=tmp_path.resolve(), + linux_validation_timeout_seconds=7200, + command_parts=command_parts, + dry_run=False, + command_result=None, + ) + + assert payload["linux_validation_dispatch_status"] == "contract_failed" + assert payload["linux_validation_dispatch_decision"] == "abort_linux_validation_dispatch" + assert payload["linux_validation_dispatch_exit_code"] == 1 + assert payload["linux_validation_should_dispatch"] is False + assert payload["linux_validation_requires_manual_action"] is True + assert payload["linux_validation_channel"] == "blocker" + assert "terminal_verdict_decision_mismatch" in payload["structural_issues"] + assert "gate_manifest_drift_failed" in payload["structural_issues"] + assert "terminal_verdict_evidence_missing" in payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_dispatch_gate_module() + terminal_payload = _sample_terminal_verdict_payload() + terminal_payload["release_follow_up_final_verdict_status"] = "requires_follow_up" + terminal_payload["release_follow_up_final_verdict_decision"] = "keep_follow_up_open" + terminal_payload["release_follow_up_final_verdict_exit_code"] = 1 + terminal_payload["release_final_outcome_status"] = "released_with_follow_up" + terminal_payload["release_final_outcome_decision"] = "ship_with_follow_up_open" + terminal_payload["release_final_outcome_exit_code"] = 1 + terminal_payload["release_final_terminal_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_terminal_publish_decision"] = ( + "announce_release_with_follow_up" + ) + terminal_payload["release_final_terminal_publish_exit_code"] = 1 + terminal_payload["release_final_handoff_status"] = "completed_with_follow_up" + terminal_payload["release_final_handoff_decision"] = "handoff_release_with_follow_up" + terminal_payload["release_final_handoff_exit_code"] = 1 + terminal_payload["release_final_closure_status"] = "closed_with_follow_up" + terminal_payload["release_final_closure_decision"] = "close_with_follow_up" + terminal_payload["release_final_closure_exit_code"] = 1 + terminal_payload["release_final_closure_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_closure_publish_decision"] = ( + "announce_release_closed_with_follow_up" + ) + terminal_payload["release_final_closure_publish_exit_code"] = 1 + terminal_payload["release_final_archive_status"] = "archived_with_follow_up" + terminal_payload["release_final_archive_decision"] = ( + "archive_release_closed_with_follow_up" + ) + terminal_payload["release_final_archive_exit_code"] = 1 + terminal_payload["release_final_verdict_status"] = "released_with_follow_up" + terminal_payload["release_final_verdict_decision"] = "ship_release_with_follow_up" + terminal_payload["release_final_verdict_exit_code"] = 1 + terminal_payload["release_final_verdict_publish_status"] = "published_with_follow_up" + terminal_payload["release_final_verdict_publish_decision"] = ( + "announce_release_shipped_with_follow_up" + ) + terminal_payload["release_final_verdict_publish_exit_code"] = 1 + terminal_payload["release_final_publish_archive_status"] = "archived_with_follow_up" + terminal_payload["release_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + terminal_payload["release_final_publish_archive_exit_code"] = 1 + terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_payload["terminal_verdict_channel"] = "follow_up" + terminal_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["reason_codes"] = [ + "terminal_verdict_ready_with_follow_up_for_linux_validation" + ] + + terminal_path = tmp_path / "ci_workflow_terminal_verdict_outputs.json" + terminal_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + terminal_report = gate.load_terminal_verdict_report(terminal_path) + command_parts = gate.build_linux_validation_command_parts( + terminal_verdict_report=terminal_report, + python_executable="python", + linux_validation_command="python scripts/run_p2_linux_unified_pipeline_gate.py --dry-run", + ) + payload = gate.build_linux_validation_dispatch_payload( + terminal_report, + source_path=terminal_path.resolve(), + project_root=tmp_path.resolve(), + linux_validation_timeout_seconds=7200, + command_parts=command_parts, + dry_run=True, + command_result=None, + ) + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_dispatch.md" + ), + ) + + assert outputs["workflow_linux_validation_dispatch_status"] == "ready_with_follow_up_dry_run" + assert ( + outputs["workflow_linux_validation_dispatch_decision"] + == "dispatch_linux_validation_with_follow_up" + ) + assert outputs["workflow_linux_validation_dispatch_exit_code"] == "1" + assert outputs["workflow_linux_validation_dispatch_should_dispatch"] == "true" + assert outputs["workflow_linux_validation_dispatch_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_dispatch_channel"] == "follow_up" + assert outputs["workflow_linux_validation_dispatch_attempted"] == "false" + assert outputs["workflow_linux_validation_dispatch_command_returncode"] == "" + assert ( + outputs["workflow_linux_validation_dispatch_terminal_verdict_status"] + == "ready_with_follow_up_for_linux_validation" + ) + assert outputs["workflow_linux_validation_dispatch_gate_manifest_drift_status"] == "passed" + assert outputs["workflow_linux_validation_dispatch_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_dispatch_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_dispatch_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_dispatch_report_json"].endswith( + "ci_workflow_linux_validation_dispatch.json" + ) + assert outputs["workflow_linux_validation_dispatch_report_markdown"].endswith( + "ci_workflow_linux_validation_dispatch.md" + ) + diff --git a/tests/test_p2_lv_final_publish_archive_rt.py b/tests/test_p2_lv_final_publish_archive_rt.py new file mode 100644 index 0000000..b21f8a4 --- /dev/null +++ b/tests/test_p2_lv_final_publish_archive_rt.py @@ -0,0 +1,329 @@ +"""Contract tests for P2-66 Linux CI workflow Linux validation final publish archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_final_publish_archive_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_final_publish_archive_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_final_publish_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_final_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_final_verdict_publish_status": "published", + "linux_validation_final_verdict_publish_decision": "announce_linux_validation_final_validated", + "linux_validation_final_verdict_publish_exit_code": 0, + "linux_validation_final_verdict_publish_should_notify": True, + "linux_validation_final_verdict_publish_requires_manual_action": False, + "linux_validation_final_verdict_publish_channel": "release", + "linux_validation_final_verdict_publish_attempted": False, + "linux_validation_final_verdict_publish_timeout_seconds": 900, + "linux_validation_final_verdict_publish_command": "", + "linux_validation_final_verdict_publish_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_final_verdict_publish_summary": ( + "linux_validation_terminal_publish_status=terminal_published " + "linux_validation_final_verdict_publish_status=published " + "linux_validation_final_verdict_publish_decision=announce_linux_validation_final_validated" + ), + "reason_codes": ["linux_validation_final_verdict_publish_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "final_verdict_publish", + "path": "/tmp/ci_workflow_linux_validation_final_verdict_publish.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_final_publish_archive_payload_published(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_publish_archive_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_publish_payload() + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_publish.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_publish_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_publish_archive_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_publish_archive_status"] == "archived" + assert ( + publish_payload["linux_validation_final_publish_archive_decision"] + == "archive_release_shipped" + ) + assert publish_payload["linux_validation_final_publish_archive_exit_code"] == 0 + assert publish_payload["linux_validation_final_publish_archive_should_archive"] is True + assert ( + publish_payload["linux_validation_final_publish_archive_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_final_publish_archive_channel"] == "release" + assert "linux_validation_final_publish_archive_archived" in publish_payload["reason_codes"] + + +def test_build_linux_validation_final_publish_archive_payload_published_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_publish_archive_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_publish_payload() + final_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + final_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_channel"] = "follow_up" + final_verdict_payload["linux_validation_channel"] = "follow_up" + final_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + final_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_status"] = "published_with_follow_up" + final_verdict_payload["linux_validation_final_verdict_publish_decision"] = ( + "announce_linux_validation_final_validated_with_follow_up" + ) + final_verdict_payload["linux_validation_final_verdict_publish_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_requires_manual_action"] = True + final_verdict_payload["linux_validation_final_verdict_publish_channel"] = "follow_up" + final_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_verdict_payload["reason_codes"] = [ + "linux_validation_final_verdict_publish_published_with_follow_up" + ] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_publish_follow_up.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_publish_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_publish_archive_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_final_publish_archive_status"] + == "archived_with_follow_up" + ) + assert ( + publish_payload["linux_validation_final_publish_archive_decision"] + == "archive_release_shipped_with_follow_up" + ) + assert publish_payload["linux_validation_final_publish_archive_exit_code"] == 1 + assert publish_payload["linux_validation_final_publish_archive_should_archive"] is True + assert ( + publish_payload["linux_validation_final_publish_archive_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_final_publish_archive_channel"] == "follow_up" + assert "linux_validation_final_publish_archive_archived_with_follow_up" in publish_payload["reason_codes"] + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_final_publish_archive_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_publish_archive_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_publish_payload() + final_verdict_payload["terminal_verdict_status"] = "blocked" + final_verdict_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_should_proceed"] = False + final_verdict_payload["terminal_verdict_requires_manual_action"] = True + final_verdict_payload["terminal_verdict_channel"] = "blocker" + final_verdict_payload["linux_validation_should_dispatch"] = False + final_verdict_payload["linux_validation_requires_manual_action"] = True + final_verdict_payload["linux_validation_channel"] = "blocker" + final_verdict_payload["linux_validation_dispatch_status"] = "blocked" + final_verdict_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_status"] = "blocked" + final_verdict_payload["linux_validation_final_verdict_publish_decision"] = ( + "announce_linux_validation_final_blocker" + ) + final_verdict_payload["linux_validation_final_verdict_publish_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_should_notify"] = True + final_verdict_payload["linux_validation_final_verdict_publish_requires_manual_action"] = True + final_verdict_payload["linux_validation_final_verdict_publish_channel"] = "blocker" + final_verdict_payload["reason_codes"] = ["linux_validation_final_verdict_publish_blocked"] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_publish_blocked.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_publish_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_publish_archive_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_publish_archive_status"] == "blocked" + assert ( + publish_payload["linux_validation_final_publish_archive_decision"] + == "archive_release_blocker" + ) + assert publish_payload["linux_validation_final_publish_archive_exit_code"] == 1 + assert publish_payload["linux_validation_final_publish_archive_should_archive"] is False + assert ( + publish_payload["linux_validation_final_publish_archive_requires_manual_action"] + is True + ) + assert publish_payload["linux_validation_final_publish_archive_channel"] == "blocker" + assert "linux_validation_final_publish_archive_blocked" in publish_payload["reason_codes"] + + +def test_build_linux_validation_final_publish_archive_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_publish_archive_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_publish_payload() + final_verdict_payload["linux_validation_final_verdict_publish_status"] = "published" + final_verdict_payload["linux_validation_final_verdict_publish_decision"] = ( + "announce_linux_validation_final_validated_with_follow_up" + ) + final_verdict_payload["linux_validation_final_verdict_publish_requires_manual_action"] = False + final_verdict_payload["linux_validation_final_verdict_publish_channel"] = "follow_up" + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_publish_mismatch.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_publish_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_publish_archive_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_publish_archive_status"] == "contract_failed" + assert publish_payload["linux_validation_final_publish_archive_decision"] == "abort_archive" + assert publish_payload["linux_validation_final_publish_archive_exit_code"] == 1 + assert "linux_validation_final_publish_decision_mismatch" in publish_payload["structural_issues"] + assert "linux_validation_final_publish_requires_manual_action_mismatch" in publish_payload["structural_issues"] + assert "linux_validation_final_publish_channel_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_publish_archive_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_publish_payload() + final_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + final_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_channel"] = "follow_up" + final_verdict_payload["linux_validation_channel"] = "follow_up" + final_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + final_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_status"] = "published_with_follow_up" + final_verdict_payload["linux_validation_final_verdict_publish_decision"] = ( + "announce_linux_validation_final_validated_with_follow_up" + ) + final_verdict_payload["linux_validation_final_verdict_publish_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_publish_requires_manual_action"] = False + final_verdict_payload["linux_validation_final_verdict_publish_channel"] = "follow_up" + final_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_verdict_payload["reason_codes"] = [ + "linux_validation_final_verdict_publish_published_with_follow_up" + ] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_publish_output.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_publish_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_publish_archive_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_publish_archive.md" + ), + ) + + assert outputs["workflow_linux_validation_final_publish_archive_status"] == "archived_with_follow_up" + assert ( + outputs["workflow_linux_validation_final_publish_archive_decision"] + == "archive_release_shipped_with_follow_up" + ) + assert outputs["workflow_linux_validation_final_publish_archive_exit_code"] == "1" + assert outputs["workflow_linux_validation_final_publish_archive_should_archive"] == "true" + assert outputs["workflow_linux_validation_final_publish_archive_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_final_publish_archive_channel"] == "follow_up" + assert outputs["workflow_linux_validation_final_publish_archive_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_final_publish_archive_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_final_publish_archive_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_final_publish_archive_report_json"].endswith( + "ci_workflow_linux_validation_final_publish_archive.json" + ) + assert outputs["workflow_linux_validation_final_publish_archive_report_markdown"].endswith( + "ci_workflow_linux_validation_final_publish_archive.md" + ) + + + diff --git a/tests/test_p2_lv_final_verdict_publish_rt.py b/tests/test_p2_lv_final_verdict_publish_rt.py new file mode 100644 index 0000000..7241431 --- /dev/null +++ b/tests/test_p2_lv_final_verdict_publish_rt.py @@ -0,0 +1,326 @@ +"""Contract tests for P2-65 Linux CI workflow Linux validation final verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_final_verdict_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_final_verdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_final_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_final_verdict_status": "validated", + "linux_validation_final_verdict_decision": "accept_linux_validation_terminal", + "linux_validation_final_verdict_exit_code": 0, + "linux_validation_final_should_accept": True, + "linux_validation_final_requires_follow_up": False, + "linux_validation_final_channel": "release", + "linux_validation_final_verdict_attempted": False, + "linux_validation_final_verdict_timeout_seconds": 900, + "linux_validation_final_verdict_command": "", + "linux_validation_final_verdict_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_final_verdict_summary": ( + "linux_validation_terminal_publish_status=terminal_published " + "linux_validation_final_verdict_status=validated " + "linux_validation_final_verdict_decision=accept_linux_validation_terminal" + ), + "reason_codes": ["linux_validation_final_verdict_validated"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "final_verdict", + "path": "/tmp/ci_workflow_linux_validation_final_verdict.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_final_verdict_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_publish_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_payload() + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_verdict_publish_status"] == "published" + assert ( + publish_payload["linux_validation_final_verdict_publish_decision"] + == "announce_linux_validation_final_validated" + ) + assert publish_payload["linux_validation_final_verdict_publish_exit_code"] == 0 + assert publish_payload["linux_validation_final_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_final_verdict_publish_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_final_verdict_publish_channel"] == "release" + assert "linux_validation_final_verdict_publish_published" in publish_payload["reason_codes"] + + +def test_build_linux_validation_final_verdict_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_verdict_publish_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_payload() + final_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + final_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_channel"] = "follow_up" + final_verdict_payload["linux_validation_channel"] = "follow_up" + final_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + final_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_status"] = "validated_with_follow_up" + final_verdict_payload["linux_validation_final_verdict_decision"] = ( + "accept_linux_validation_terminal_with_follow_up" + ) + final_verdict_payload["linux_validation_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_final_requires_follow_up"] = True + final_verdict_payload["linux_validation_final_channel"] = "follow_up" + final_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_verdict_payload["reason_codes"] = [ + "linux_validation_final_verdict_validated_with_follow_up" + ] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_follow_up.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_final_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + publish_payload["linux_validation_final_verdict_publish_decision"] + == "announce_linux_validation_final_validated_with_follow_up" + ) + assert publish_payload["linux_validation_final_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_final_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_final_verdict_publish_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_final_verdict_publish_channel"] == "follow_up" + assert "linux_validation_final_verdict_publish_with_follow_up" in publish_payload["reason_codes"] + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_final_verdict_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_publish_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_payload() + final_verdict_payload["terminal_verdict_status"] = "blocked" + final_verdict_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_should_proceed"] = False + final_verdict_payload["terminal_verdict_requires_manual_action"] = True + final_verdict_payload["terminal_verdict_channel"] = "blocker" + final_verdict_payload["linux_validation_should_dispatch"] = False + final_verdict_payload["linux_validation_requires_manual_action"] = True + final_verdict_payload["linux_validation_channel"] = "blocker" + final_verdict_payload["linux_validation_dispatch_status"] = "blocked" + final_verdict_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_status"] = "blocked" + final_verdict_payload["linux_validation_final_verdict_decision"] = ( + "escalate_linux_validation_terminal_blocker" + ) + final_verdict_payload["linux_validation_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_final_should_accept"] = False + final_verdict_payload["linux_validation_final_requires_follow_up"] = True + final_verdict_payload["linux_validation_final_channel"] = "blocker" + final_verdict_payload["reason_codes"] = ["linux_validation_final_verdict_blocked"] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_blocked.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_verdict_publish_status"] == "blocked" + assert ( + publish_payload["linux_validation_final_verdict_publish_decision"] + == "announce_linux_validation_final_blocker" + ) + assert publish_payload["linux_validation_final_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_final_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_final_verdict_publish_requires_manual_action"] + is True + ) + assert publish_payload["linux_validation_final_verdict_publish_channel"] == "blocker" + assert "linux_validation_final_verdict_publish_blocked" in publish_payload["reason_codes"] + + +def test_build_linux_validation_final_verdict_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_verdict_publish_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_payload() + final_verdict_payload["linux_validation_final_verdict_status"] = "validated" + final_verdict_payload["linux_validation_final_verdict_decision"] = ( + "accept_linux_validation_terminal_with_follow_up" + ) + final_verdict_payload["linux_validation_final_requires_follow_up"] = True + final_verdict_payload["linux_validation_final_channel"] = "follow_up" + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_mismatch.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_final_verdict_publish_status"] == "contract_failed" + assert publish_payload["linux_validation_final_verdict_publish_decision"] == "abort_publish" + assert publish_payload["linux_validation_final_verdict_publish_exit_code"] == 1 + assert "linux_validation_final_verdict_decision_mismatch" in publish_payload["structural_issues"] + assert "linux_validation_final_requires_follow_up_mismatch" in publish_payload["structural_issues"] + assert "linux_validation_final_channel_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_publish_gate_module() + final_verdict_payload = _sample_linux_validation_final_verdict_payload() + final_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + final_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + final_verdict_payload["terminal_verdict_exit_code"] = 1 + final_verdict_payload["terminal_verdict_channel"] = "follow_up" + final_verdict_payload["linux_validation_channel"] = "follow_up" + final_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + final_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + final_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + final_verdict_payload["linux_validation_final_verdict_status"] = "validated_with_follow_up" + final_verdict_payload["linux_validation_final_verdict_decision"] = ( + "accept_linux_validation_terminal_with_follow_up" + ) + final_verdict_payload["linux_validation_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_final_requires_follow_up"] = True + final_verdict_payload["linux_validation_final_channel"] = "follow_up" + final_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + final_verdict_payload["reason_codes"] = [ + "linux_validation_final_verdict_validated_with_follow_up" + ] + + final_verdict_path = tmp_path / "ci_workflow_linux_validation_final_verdict_output.json" + final_verdict_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + final_verdict_report = gate.load_linux_validation_final_verdict_report(final_verdict_path) + publish_payload = gate.build_linux_validation_final_verdict_publish_payload( + final_verdict_report, + source_path=final_verdict_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict_publish.md" + ), + ) + + assert outputs["workflow_linux_validation_final_verdict_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_linux_validation_final_verdict_publish_decision"] + == "announce_linux_validation_final_validated_with_follow_up" + ) + assert outputs["workflow_linux_validation_final_verdict_publish_exit_code"] == "1" + assert outputs["workflow_linux_validation_final_verdict_publish_should_notify"] == "true" + assert outputs["workflow_linux_validation_final_verdict_publish_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_final_verdict_publish_channel"] == "follow_up" + assert outputs["workflow_linux_validation_final_verdict_publish_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_final_verdict_publish_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_final_verdict_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_final_verdict_publish_report_json"].endswith( + "ci_workflow_linux_validation_final_verdict_publish.json" + ) + assert outputs["workflow_linux_validation_final_verdict_publish_report_markdown"].endswith( + "ci_workflow_linux_validation_final_verdict_publish.md" + ) diff --git a/tests/test_p2_lv_final_verdict_rt.py b/tests/test_p2_lv_final_verdict_rt.py new file mode 100644 index 0000000..a150b6e --- /dev/null +++ b/tests/test_p2_lv_final_verdict_rt.py @@ -0,0 +1,412 @@ +"""Contract tests for P2-64 Linux CI workflow Linux validation final verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_final_verdict_gate_module(): + script_path = ( + Path("scripts") / "run_p2_lv_final_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_final_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_verdict_publish_report": "/tmp/ci_workflow_linux_validation_verdict_publish.json", + "source_linux_validation_verdict_report": "/tmp/ci_workflow_linux_validation_verdict.json", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_verdict_status": "validated", + "linux_validation_verdict_decision": "accept_linux_validation", + "linux_validation_verdict_exit_code": 0, + "linux_validation_passed": True, + "linux_validation_verdict_requires_manual_action": False, + "linux_validation_verdict_channel": "release", + "linux_validation_verdict_attempted": False, + "linux_validation_verdict_timeout_seconds": 900, + "linux_validation_verdict_command": "", + "linux_validation_verdict_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_verdict_summary": ( + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation " + "linux_validation_verdict_status=validated " + "linux_validation_verdict_decision=accept_linux_validation" + ), + "linux_validation_verdict_publish_status": "published", + "linux_validation_verdict_publish_decision": "announce_linux_validation_passed", + "linux_validation_verdict_publish_exit_code": 0, + "linux_validation_verdict_publish_should_notify": True, + "linux_validation_verdict_publish_requires_manual_action": False, + "linux_validation_verdict_publish_channel": "release", + "linux_validation_verdict_publish_summary": ( + "linux_validation_verdict_status=validated " + "linux_validation_verdict_publish_status=published " + "linux_validation_verdict_publish_decision=announce_linux_validation_passed" + ), + "linux_validation_terminal_publish_status": "terminal_published", + "linux_validation_terminal_publish_decision": "announce_linux_validation_terminal_passed", + "linux_validation_terminal_publish_exit_code": 0, + "linux_validation_terminal_publish_should_notify": True, + "linux_validation_terminal_publish_ready_for_handoff": True, + "linux_validation_terminal_publish_requires_manual_action": False, + "linux_validation_terminal_publish_channel": "release", + "linux_validation_terminal_publish_summary": ( + "linux_validation_verdict_publish_status=published " + "linux_validation_terminal_publish_status=terminal_published " + "linux_validation_terminal_publish_decision=announce_linux_validation_terminal_passed" + ), + "reason_codes": ["linux_validation_terminal_publish_terminal_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "verdict", + "path": "/tmp/ci_workflow_linux_validation_verdict.json", + "exists": True, + }, + { + "source": "terminal_publish", + "path": "/tmp/ci_workflow_linux_validation_terminal_publish.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_final_verdict_payload_validated(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_gate_module() + terminal_payload = _sample_linux_validation_terminal_publish_payload() + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_publish.json" + report_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_publish_report(report_path) + final_payload = gate.build_linux_validation_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["linux_validation_final_verdict_status"] == "validated" + assert ( + final_payload["linux_validation_final_verdict_decision"] + == "accept_linux_validation_terminal" + ) + assert final_payload["linux_validation_final_verdict_exit_code"] == 0 + assert final_payload["linux_validation_final_should_accept"] is True + assert final_payload["linux_validation_final_requires_follow_up"] is False + assert final_payload["linux_validation_final_should_page_owner"] is False + assert final_payload["linux_validation_final_channel"] == "release" + assert final_payload["reason_codes"] == ["linux_validation_final_verdict_validated"] + + +def test_build_linux_validation_final_verdict_payload_validated_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_verdict_gate_module() + terminal_payload = _sample_linux_validation_terminal_publish_payload() + terminal_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["terminal_verdict_channel"] = "follow_up" + terminal_payload["linux_validation_channel"] = "follow_up" + terminal_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + terminal_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + terminal_payload["linux_validation_verdict_exit_code"] = 1 + terminal_payload["linux_validation_verdict_channel"] = "follow_up" + terminal_payload["linux_validation_verdict_publish_status"] = "published_with_follow_up" + terminal_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_passed_with_follow_up" + ) + terminal_payload["linux_validation_verdict_publish_exit_code"] = 1 + terminal_payload["linux_validation_verdict_publish_channel"] = "follow_up" + terminal_payload["linux_validation_terminal_publish_status"] = ( + "terminal_published_with_follow_up" + ) + terminal_payload["linux_validation_terminal_publish_decision"] = ( + "announce_linux_validation_terminal_passed_with_follow_up" + ) + terminal_payload["linux_validation_terminal_publish_exit_code"] = 1 + terminal_payload["linux_validation_terminal_publish_ready_for_handoff"] = True + terminal_payload["linux_validation_terminal_publish_requires_manual_action"] = False + terminal_payload["linux_validation_terminal_publish_channel"] = "follow_up" + terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_payload["reason_codes"] = [ + "linux_validation_terminal_publish_terminal_published_with_follow_up" + ] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_publish_follow_up.json" + report_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_publish_report(report_path) + final_payload = gate.build_linux_validation_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + final_payload["linux_validation_final_verdict_status"] + == "validated_with_follow_up" + ) + assert ( + final_payload["linux_validation_final_verdict_decision"] + == "accept_linux_validation_terminal_with_follow_up" + ) + assert final_payload["linux_validation_final_verdict_exit_code"] == 1 + assert final_payload["linux_validation_final_should_accept"] is True + assert final_payload["linux_validation_final_requires_follow_up"] is True + assert final_payload["linux_validation_final_should_page_owner"] is False + assert final_payload["linux_validation_final_channel"] == "follow_up" + assert "linux_validation_final_verdict_validated_with_follow_up" in final_payload["reason_codes"] + assert final_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_final_verdict_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_gate_module() + terminal_payload = _sample_linux_validation_terminal_publish_payload() + terminal_payload["terminal_verdict_status"] = "blocked" + terminal_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["terminal_verdict_should_proceed"] = False + terminal_payload["terminal_verdict_requires_manual_action"] = True + terminal_payload["terminal_verdict_channel"] = "blocker" + terminal_payload["linux_validation_should_dispatch"] = False + terminal_payload["linux_validation_requires_manual_action"] = True + terminal_payload["linux_validation_channel"] = "blocker" + terminal_payload["linux_validation_dispatch_status"] = "blocked" + terminal_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + terminal_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_payload["linux_validation_verdict_status"] = "blocked" + terminal_payload["linux_validation_verdict_decision"] = "hold_linux_validation_blocker" + terminal_payload["linux_validation_verdict_exit_code"] = 1 + terminal_payload["linux_validation_passed"] = False + terminal_payload["linux_validation_verdict_requires_manual_action"] = True + terminal_payload["linux_validation_verdict_channel"] = "blocker" + terminal_payload["linux_validation_verdict_publish_status"] = "blocked" + terminal_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_blocker" + ) + terminal_payload["linux_validation_verdict_publish_exit_code"] = 1 + terminal_payload["linux_validation_verdict_publish_requires_manual_action"] = True + terminal_payload["linux_validation_verdict_publish_channel"] = "blocker" + terminal_payload["linux_validation_terminal_publish_status"] = "terminal_blocked" + terminal_payload["linux_validation_terminal_publish_decision"] = ( + "announce_linux_validation_terminal_blocker" + ) + terminal_payload["linux_validation_terminal_publish_exit_code"] = 1 + terminal_payload["linux_validation_terminal_publish_ready_for_handoff"] = False + terminal_payload["linux_validation_terminal_publish_requires_manual_action"] = True + terminal_payload["linux_validation_terminal_publish_channel"] = "blocker" + terminal_payload["reason_codes"] = ["linux_validation_terminal_publish_terminal_blocked"] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_publish_blocked.json" + report_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_publish_report(report_path) + final_payload = gate.build_linux_validation_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["linux_validation_final_verdict_status"] == "blocked" + assert ( + final_payload["linux_validation_final_verdict_decision"] + == "escalate_linux_validation_terminal_blocker" + ) + assert final_payload["linux_validation_final_verdict_exit_code"] == 1 + assert final_payload["linux_validation_final_should_accept"] is False + assert final_payload["linux_validation_final_requires_follow_up"] is True + assert final_payload["linux_validation_final_should_page_owner"] is True + assert final_payload["linux_validation_final_channel"] == "blocker" + assert "linux_validation_final_verdict_blocked" in final_payload["reason_codes"] + + +def test_build_linux_validation_final_verdict_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_final_verdict_gate_module() + terminal_payload = _sample_linux_validation_terminal_publish_payload() + terminal_payload["linux_validation_terminal_publish_status"] = "terminal_published" + terminal_payload["linux_validation_terminal_publish_decision"] = ( + "announce_linux_validation_terminal_passed_with_follow_up" + ) + terminal_payload["linux_validation_terminal_publish_ready_for_handoff"] = False + terminal_payload["linux_validation_terminal_publish_requires_manual_action"] = True + terminal_payload["linux_validation_terminal_publish_channel"] = "follow_up" + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_publish_mismatch.json" + report_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_publish_report(report_path) + final_payload = gate.build_linux_validation_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert final_payload["linux_validation_final_verdict_status"] == "contract_failed" + assert ( + final_payload["linux_validation_final_verdict_decision"] + == "abort_linux_validation_terminal_verdict" + ) + assert final_payload["linux_validation_final_verdict_exit_code"] == 1 + assert ( + "linux_validation_terminal_publish_decision_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_publish_ready_for_handoff_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_publish_requires_manual_action_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_publish_channel_mismatch" + in final_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_final_verdict_gate_module() + terminal_payload = _sample_linux_validation_terminal_publish_payload() + terminal_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_payload["terminal_verdict_exit_code"] = 1 + terminal_payload["terminal_verdict_channel"] = "follow_up" + terminal_payload["linux_validation_channel"] = "follow_up" + terminal_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + terminal_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + terminal_payload["linux_validation_verdict_exit_code"] = 1 + terminal_payload["linux_validation_verdict_channel"] = "follow_up" + terminal_payload["linux_validation_verdict_publish_status"] = "published_with_follow_up" + terminal_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_passed_with_follow_up" + ) + terminal_payload["linux_validation_verdict_publish_exit_code"] = 1 + terminal_payload["linux_validation_verdict_publish_channel"] = "follow_up" + terminal_payload["linux_validation_terminal_publish_status"] = ( + "terminal_published_with_follow_up" + ) + terminal_payload["linux_validation_terminal_publish_decision"] = ( + "announce_linux_validation_terminal_passed_with_follow_up" + ) + terminal_payload["linux_validation_terminal_publish_exit_code"] = 1 + terminal_payload["linux_validation_terminal_publish_ready_for_handoff"] = True + terminal_payload["linux_validation_terminal_publish_requires_manual_action"] = False + terminal_payload["linux_validation_terminal_publish_channel"] = "follow_up" + terminal_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_payload["reason_codes"] = [ + "linux_validation_terminal_publish_terminal_published_with_follow_up" + ] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_publish_output.json" + report_path.write_text(json.dumps(terminal_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_publish_report(report_path) + final_payload = gate.build_linux_validation_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path(".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.json"), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_final_verdict.md" + ), + ) + + assert outputs["workflow_linux_validation_final_verdict_status"] == "validated_with_follow_up" + assert ( + outputs["workflow_linux_validation_final_verdict_decision"] + == "accept_linux_validation_terminal_with_follow_up" + ) + assert outputs["workflow_linux_validation_final_verdict_exit_code"] == "1" + assert outputs["workflow_linux_validation_final_should_accept"] == "true" + assert outputs["workflow_linux_validation_final_requires_follow_up"] == "true" + assert outputs["workflow_linux_validation_final_should_page_owner"] == "false" + assert outputs["workflow_linux_validation_final_channel"] == "follow_up" + assert ( + outputs["workflow_linux_validation_final_terminal_publish_status"] + == "terminal_published_with_follow_up" + ) + assert outputs["workflow_linux_validation_final_terminal_publish_channel"] == "follow_up" + assert outputs["workflow_linux_validation_final_follow_up_queue_url"].endswith("/issues/77") + assert outputs["workflow_linux_validation_final_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_final_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_final_report_json"].endswith( + "ci_workflow_linux_validation_final_verdict.json" + ) + assert outputs["workflow_linux_validation_final_report_markdown"].endswith( + "ci_workflow_linux_validation_final_verdict.md" + ) diff --git a/tests/test_p2_lv_td_completion_rt.py b/tests/test_p2_lv_td_completion_rt.py new file mode 100644 index 0000000..95ec1f1 --- /dev/null +++ b/tests/test_p2_lv_td_completion_rt.py @@ -0,0 +1,228 @@ +"""Contract tests for P2-72 Linux CI workflow Linux validation terminal dispatch completion gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_completion_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_completion_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_completion_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_trace_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_dispatch_trace_status": "run_tracking_ready", + "linux_validation_terminal_dispatch_trace_exit_code": 0, + "linux_validation_terminal_should_poll_workflow_run": True, + "linux_validation_terminal_run_id": 9988776655, + "linux_validation_terminal_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "linux_validation_terminal_repo_owner": "acme", + "linux_validation_terminal_repo_name": "demo", + "linux_validation_terminal_poll_command": ( + "gh run view 9988776655 --json " + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt --repo acme/demo" + ), + "linux_validation_terminal_poll_command_parts": [ + "gh", + "run", + "view", + "9988776655", + "--json", + "databaseId,status,conclusion,url,workflowName,createdAt,updatedAt", + "--repo", + "acme/demo", + ], + "linux_validation_terminal_poll_attempted": False, + "linux_validation_terminal_poll_returncode": None, + "linux_validation_terminal_poll_status": "", + "linux_validation_terminal_poll_conclusion": "", + "linux_validation_terminal_poll_url": "https://github.com/acme/demo/actions/runs/9988776655", + "linux_validation_terminal_poll_error_type": "", + "linux_validation_terminal_poll_error_message": "", + "linux_validation_terminal_poll_stdout_tail": "", + "linux_validation_terminal_poll_stderr_tail": "", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_run_tracking_ready"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def _success_poll_result() -> dict: + return { + "returncode": 0, + "stdout_tail": ( + '{"status":"completed","conclusion":"success",' + '"url":"https://github.com/acme/demo/actions/runs/9988776655"}' + ), + "stderr_tail": "", + "payload": { + "status": "completed", + "conclusion": "success", + "url": "https://github.com/acme/demo/actions/runs/9988776655", + }, + "error_type": "", + "error_message": "", + } + + +def test_build_linux_validation_terminal_dispatch_completion_payload_reaches_success(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_completion_gate_module() + payload = _sample_linux_validation_terminal_dispatch_trace_payload() + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_trace_report(report_path) + completion_payload = gate.build_linux_validation_terminal_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=3, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=lambda command_parts, cwd, timeout_seconds: _success_poll_result(), + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["linux_validation_terminal_dispatch_completion_status"] == "run_completed_success" + assert completion_payload["linux_validation_terminal_dispatch_completion_exit_code"] == 0 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 1 + assert completion_payload["poll_status"] == "completed" + assert completion_payload["poll_conclusion"] == "success" + assert completion_payload["reason_codes"] == ["linux_validation_terminal_dispatch_run_completed_success"] + + +def test_build_linux_validation_terminal_dispatch_completion_payload_timeout_without_allow_in_progress(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_completion_gate_module() + payload = _sample_linux_validation_terminal_dispatch_trace_payload() + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_trace.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_trace_report(report_path) + + def _in_progress_result(command_parts, cwd, timeout_seconds): + return { + "returncode": 0, + "stdout_tail": '{"status":"in_progress","conclusion":null}', + "stderr_tail": "", + "payload": {"status": "in_progress", "conclusion": None}, + "error_type": "", + "error_message": "", + } + + completion_payload = gate.build_linux_validation_terminal_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=2, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + poll_executor=_in_progress_result, + sleep_fn=lambda seconds: None, + ) + + assert completion_payload["linux_validation_terminal_dispatch_completion_status"] == "run_await_timeout" + assert completion_payload["linux_validation_terminal_dispatch_completion_exit_code"] == 1 + assert completion_payload["poll_attempted"] is True + assert completion_payload["poll_iterations"] == 2 + assert "linux_validation_terminal_dispatch_run_await_timeout" in completion_payload["reason_codes"] + + +def test_load_linux_validation_terminal_dispatch_trace_report_rejects_command_contract_mismatch(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_completion_gate_module() + payload = _sample_linux_validation_terminal_dispatch_trace_payload() + payload["linux_validation_terminal_poll_command_parts"] = [ + "gh", + "run", + "view", + "9988776655", + "--json", + "databaseId,status", + ] + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_trace_bad.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_linux_validation_terminal_dispatch_trace_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_completion_gate_module() + payload = _sample_linux_validation_terminal_dispatch_trace_payload() + payload["linux_validation_terminal_dispatch_trace_status"] = "run_completed_failure" + payload["linux_validation_terminal_dispatch_trace_exit_code"] = 1 + payload["linux_validation_terminal_should_poll_workflow_run"] = False + payload["linux_validation_terminal_run_id"] = None + payload["linux_validation_terminal_poll_command"] = "" + payload["linux_validation_terminal_poll_command_parts"] = [] + payload["reason_codes"] = ["linux_validation_terminal_dispatch_run_completed_with_failure"] + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_trace_terminal.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_trace_report(report_path) + completion_payload = gate.build_linux_validation_terminal_dispatch_completion_payload( + report, + source_path=report_path.resolve(), + project_root=tmp_path, + poll_interval_seconds=1, + max_polls=1, + poll_timeout_seconds=30, + dry_run=False, + allow_in_progress=False, + sleep_fn=lambda seconds: None, + ) + outputs = gate.build_github_output_values( + completion_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_completion.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_dispatch_completion_status"] == "run_completed_failure" + assert outputs["workflow_linux_validation_terminal_dispatch_completion_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_dispatch_completion_should_poll"] == "false" + assert outputs["workflow_linux_validation_terminal_dispatch_completion_poll_attempted"] == "false" + assert outputs["workflow_linux_validation_terminal_dispatch_completion_run_id"] == "" + assert outputs["workflow_linux_validation_terminal_dispatch_completion_report_json"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_completion.json" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_completion_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_completion.md" + ) diff --git a/tests/test_p2_lv_td_dispatch_rt.py b/tests/test_p2_lv_td_dispatch_rt.py new file mode 100644 index 0000000..c5f91f6 --- /dev/null +++ b/tests/test_p2_lv_td_dispatch_rt.py @@ -0,0 +1,347 @@ +"""Contract tests for P2-69 Linux CI workflow Linux validation terminal dispatch gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_dispatch_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_terminal_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_verdict.json", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "source_linux_validation_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_final_publish_archive.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_terminal_verdict_status": "ready_for_linux_validation", + "linux_validation_terminal_verdict_decision": "proceed_linux_validation", + "linux_validation_terminal_verdict_exit_code": 0, + "linux_validation_terminal_verdict_should_proceed": True, + "linux_validation_terminal_verdict_requires_manual_action": False, + "linux_validation_terminal_verdict_channel": "release", + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_terminal_verdict_summary": ( + "linux_validation_final_publish_archive_status=archived " + "linux_validation_terminal_verdict_status=ready_for_linux_validation " + "linux_validation_terminal_verdict_decision=proceed_linux_validation" + ), + "linux_validation_terminal_verdict_publish_status": "published", + "linux_validation_terminal_verdict_publish_decision": "announce_linux_validation_terminal_ready", + "linux_validation_terminal_verdict_publish_exit_code": 0, + "linux_validation_terminal_verdict_publish_should_notify": True, + "linux_validation_terminal_verdict_publish_requires_manual_action": False, + "linux_validation_terminal_verdict_publish_channel": "release", + "linux_validation_terminal_verdict_publish_summary": ( + "linux_validation_terminal_verdict_status=ready_for_linux_validation " + "linux_validation_terminal_verdict_publish_status=published " + "linux_validation_terminal_verdict_publish_decision=announce_linux_validation_terminal_ready" + ), + "reason_codes": ["linux_validation_terminal_verdict_publish_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "terminal_verdict_publish", + "path": "/tmp/ci_workflow_linux_validation_terminal_verdict_publish.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_terminal_dispatch_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_gate_module() + terminal_publish_payload = _sample_linux_validation_terminal_verdict_publish_payload() + + publish_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_publish.json" + publish_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_linux_validation_terminal_verdict_publish_report(publish_path) + dispatch_payload = gate.build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert dispatch_payload["linux_validation_terminal_dispatch_status"] == "ready_dry_run" + assert ( + dispatch_payload["linux_validation_terminal_dispatch_decision"] + == "dispatch_linux_validation_terminal" + ) + assert dispatch_payload["linux_validation_terminal_dispatch_exit_code"] == 0 + assert dispatch_payload["linux_validation_terminal_should_dispatch"] is True + assert dispatch_payload["linux_validation_terminal_dispatch_requires_manual_action"] is False + assert dispatch_payload["linux_validation_terminal_dispatch_channel"] == "release" + assert "linux_validation_terminal_dispatch_ready_dry_run" in dispatch_payload["reason_codes"] + + +def test_build_linux_validation_terminal_dispatch_payload_ready_with_follow_up_dry_run( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_gate_module() + terminal_publish_payload = _sample_linux_validation_terminal_verdict_publish_payload() + terminal_publish_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_publish_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_publish_payload["terminal_verdict_exit_code"] = 1 + terminal_publish_payload["terminal_verdict_channel"] = "follow_up" + terminal_publish_payload["linux_validation_channel"] = "follow_up" + terminal_publish_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_publish_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_publish_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_publish_payload["linux_validation_terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_requires_manual_action"] = False + terminal_publish_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + terminal_publish_payload["linux_validation_terminal_verdict_publish_status"] = "published_with_follow_up" + terminal_publish_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_ready_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] = False + terminal_publish_payload["linux_validation_terminal_verdict_publish_channel"] = "follow_up" + terminal_publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_publish_payload["reason_codes"] = [ + "linux_validation_terminal_verdict_publish_with_follow_up" + ] + + publish_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_publish_follow_up.json" + publish_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_linux_validation_terminal_verdict_publish_report(publish_path) + dispatch_payload = gate.build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert dispatch_payload["linux_validation_terminal_dispatch_status"] == "ready_with_follow_up_dry_run" + assert ( + dispatch_payload["linux_validation_terminal_dispatch_decision"] + == "dispatch_linux_validation_terminal_with_follow_up" + ) + assert dispatch_payload["linux_validation_terminal_dispatch_exit_code"] == 1 + assert dispatch_payload["linux_validation_terminal_should_dispatch"] is True + assert dispatch_payload["linux_validation_terminal_dispatch_requires_manual_action"] is False + assert dispatch_payload["linux_validation_terminal_dispatch_channel"] == "follow_up" + assert "linux_validation_terminal_dispatch_ready_with_follow_up_dry_run" in dispatch_payload["reason_codes"] + assert dispatch_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_gate_module() + terminal_publish_payload = _sample_linux_validation_terminal_verdict_publish_payload() + terminal_publish_payload["terminal_verdict_status"] = "blocked" + terminal_publish_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + terminal_publish_payload["terminal_verdict_exit_code"] = 1 + terminal_publish_payload["terminal_verdict_should_proceed"] = False + terminal_publish_payload["terminal_verdict_requires_manual_action"] = True + terminal_publish_payload["terminal_verdict_channel"] = "blocker" + terminal_publish_payload["linux_validation_should_dispatch"] = False + terminal_publish_payload["linux_validation_requires_manual_action"] = True + terminal_publish_payload["linux_validation_channel"] = "blocker" + terminal_publish_payload["linux_validation_dispatch_status"] = "blocked" + terminal_publish_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + terminal_publish_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_status"] = "blocked" + terminal_publish_payload["linux_validation_terminal_verdict_decision"] = ( + "halt_linux_validation_blocker" + ) + terminal_publish_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_should_proceed"] = False + terminal_publish_payload["linux_validation_terminal_verdict_requires_manual_action"] = True + terminal_publish_payload["linux_validation_terminal_verdict_channel"] = "blocker" + terminal_publish_payload["linux_validation_terminal_verdict_publish_status"] = "blocked" + terminal_publish_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_blocker" + ) + terminal_publish_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] = True + terminal_publish_payload["linux_validation_terminal_verdict_publish_channel"] = "blocker" + terminal_publish_payload["reason_codes"] = ["linux_validation_terminal_verdict_publish_blocked"] + + publish_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_publish_blocked.json" + publish_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_linux_validation_terminal_verdict_publish_report(publish_path) + dispatch_payload = gate.build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert dispatch_payload["linux_validation_terminal_dispatch_status"] == "blocked" + assert ( + dispatch_payload["linux_validation_terminal_dispatch_decision"] + == "hold_linux_validation_terminal_blocker" + ) + assert dispatch_payload["linux_validation_terminal_dispatch_exit_code"] == 1 + assert dispatch_payload["linux_validation_terminal_should_dispatch"] is False + assert dispatch_payload["linux_validation_terminal_dispatch_requires_manual_action"] is True + assert dispatch_payload["linux_validation_terminal_dispatch_channel"] == "blocker" + assert "linux_validation_terminal_dispatch_blocked" in dispatch_payload["reason_codes"] + + +def test_build_linux_validation_terminal_dispatch_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_gate_module() + terminal_publish_payload = _sample_linux_validation_terminal_verdict_publish_payload() + terminal_publish_payload["linux_validation_terminal_verdict_publish_status"] = "published" + terminal_publish_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_ready_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] = False + terminal_publish_payload["linux_validation_terminal_verdict_publish_channel"] = "follow_up" + + publish_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_publish_mismatch.json" + publish_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_linux_validation_terminal_verdict_publish_report(publish_path) + dispatch_payload = gate.build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_path.resolve(), + ) + + assert dispatch_payload["linux_validation_terminal_dispatch_status"] == "contract_failed" + assert dispatch_payload["linux_validation_terminal_dispatch_decision"] == "abort_linux_validation_terminal_dispatch" + assert dispatch_payload["linux_validation_terminal_dispatch_exit_code"] == 1 + assert "linux_validation_terminal_dispatch_upstream_decision_mismatch" in dispatch_payload["structural_issues"] + assert ( + "linux_validation_terminal_dispatch_upstream_requires_manual_action_mismatch" + in dispatch_payload["structural_issues"] + ) + assert "linux_validation_terminal_dispatch_upstream_channel_mismatch" in dispatch_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_gate_module() + terminal_publish_payload = _sample_linux_validation_terminal_verdict_publish_payload() + terminal_publish_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_publish_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_publish_payload["terminal_verdict_exit_code"] = 1 + terminal_publish_payload["terminal_verdict_channel"] = "follow_up" + terminal_publish_payload["linux_validation_channel"] = "follow_up" + terminal_publish_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_publish_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_publish_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_publish_payload["linux_validation_terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_requires_manual_action"] = False + terminal_publish_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + terminal_publish_payload["linux_validation_terminal_verdict_publish_status"] = "published_with_follow_up" + terminal_publish_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_ready_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] = False + terminal_publish_payload["linux_validation_terminal_verdict_publish_channel"] = "follow_up" + terminal_publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_publish_payload["reason_codes"] = [ + "linux_validation_terminal_verdict_publish_with_follow_up" + ] + + publish_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_publish_output.json" + publish_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + publish_report = gate.load_linux_validation_terminal_verdict_publish_report(publish_path) + dispatch_payload = gate.build_linux_validation_terminal_dispatch_payload( + publish_report, + source_path=publish_path.resolve(), + ) + outputs = gate.build_github_output_values( + dispatch_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_dispatch_status"] == "ready_with_follow_up_dry_run" + assert ( + outputs["workflow_linux_validation_terminal_dispatch_decision"] + == "dispatch_linux_validation_terminal_with_follow_up" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_dispatch_should_dispatch"] == "true" + assert outputs["workflow_linux_validation_terminal_dispatch_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_terminal_dispatch_channel"] == "follow_up" + assert outputs["workflow_linux_validation_terminal_dispatch_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_terminal_dispatch_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_report_json"].endswith( + "ci_workflow_linux_validation_terminal_dispatch.json" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_dispatch.md" + ) diff --git a/tests/test_p2_lv_td_execution_rt.py b/tests/test_p2_lv_td_execution_rt.py new file mode 100644 index 0000000..75d9666 --- /dev/null +++ b/tests/test_p2_lv_td_execution_rt.py @@ -0,0 +1,408 @@ +"""Contract tests for P2-70 Linux CI workflow Linux validation terminal dispatch execution gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_execution_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_execution_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_terminal_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_verdict_publish.json", + "source_linux_validation_terminal_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_verdict.json", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "source_linux_validation_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_final_publish_archive.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_terminal_verdict_status": "ready_for_linux_validation", + "linux_validation_terminal_verdict_decision": "proceed_linux_validation", + "linux_validation_terminal_verdict_exit_code": 0, + "linux_validation_terminal_verdict_should_proceed": True, + "linux_validation_terminal_verdict_requires_manual_action": False, + "linux_validation_terminal_verdict_channel": "release", + "linux_validation_terminal_verdict_publish_status": "published", + "linux_validation_terminal_verdict_publish_decision": "announce_linux_validation_terminal_ready", + "linux_validation_terminal_verdict_publish_exit_code": 0, + "linux_validation_terminal_verdict_publish_should_notify": True, + "linux_validation_terminal_verdict_publish_requires_manual_action": False, + "linux_validation_terminal_verdict_publish_channel": "release", + "linux_validation_terminal_verdict_publish_summary": ( + "linux_validation_terminal_verdict_status=ready_for_linux_validation " + "linux_validation_terminal_verdict_publish_status=published " + "linux_validation_terminal_verdict_publish_decision=announce_linux_validation_terminal_ready" + ), + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_terminal_verdict_summary": ( + "linux_validation_final_publish_archive_status=archived " + "linux_validation_terminal_verdict_status=ready_for_linux_validation " + "linux_validation_terminal_verdict_decision=proceed_linux_validation" + ), + "linux_validation_terminal_dispatch_status": "ready_dry_run", + "linux_validation_terminal_dispatch_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_requires_manual_action": False, + "linux_validation_terminal_dispatch_channel": "release", + "linux_validation_terminal_dispatch_summary": ( + "linux_validation_terminal_verdict_publish_status=published " + "linux_validation_terminal_dispatch_status=ready_dry_run " + "linux_validation_terminal_dispatch_decision=dispatch_linux_validation_terminal" + ), + "reason_codes": ["linux_validation_terminal_dispatch_ready_dry_run"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "terminal_dispatch", + "path": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_terminal_dispatch_execution_payload_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module() + dispatch_payload = _sample_linux_validation_terminal_dispatch_payload() + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch.json" + report_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_terminal_dispatch_report(report_path) + command_parts = gate.build_linux_validation_terminal_command_parts( + terminal_dispatch_report=dispatch_report, + python_executable="python", + linux_validation_terminal_command="", + ) + payload = gate.build_linux_validation_terminal_dispatch_execution_payload( + dispatch_report, + source_path=report_path.resolve(), + project_root=tmp_path, + linux_validation_terminal_timeout_seconds=7200, + command_parts=command_parts, + dry_run=True, + command_result=None, + ) + + assert payload["linux_validation_terminal_dispatch_execution_status"] == "ready_dry_run" + assert ( + payload["linux_validation_terminal_dispatch_execution_decision"] + == "dispatch_linux_validation_terminal" + ) + assert payload["linux_validation_terminal_dispatch_execution_exit_code"] == 0 + assert payload["linux_validation_terminal_dispatch_attempted"] is False + assert payload["linux_validation_terminal_should_dispatch"] is True + assert payload["linux_validation_terminal_dispatch_execution_channel"] == "release" + assert payload["linux_validation_terminal_command_parts"][:2] == ["python", "scripts/run_p2_linux_unified_pipeline_gate.py"] + + +def test_build_linux_validation_terminal_dispatch_execution_payload_dispatched_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module() + dispatch_payload = _sample_linux_validation_terminal_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_channel"] = "follow_up" + dispatch_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_dispatch_decision"] = "dispatch_linux_validation_with_follow_up" + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["linux_validation_terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["linux_validation_terminal_verdict_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_terminal_verdict_publish_status"] = "published_with_follow_up" + dispatch_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_ready_with_follow_up" + ) + dispatch_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_publish_channel"] = "follow_up" + dispatch_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + dispatch_payload["linux_validation_terminal_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_terminal_dispatch_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + dispatch_payload["linux_validation_terminal_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_dispatch_channel"] = "follow_up" + dispatch_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_ready_with_follow_up_dry_run" + ] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_follow_up.json" + report_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_terminal_dispatch_report(report_path) + command_parts = gate.build_linux_validation_terminal_command_parts( + terminal_dispatch_report=dispatch_report, + python_executable="python", + linux_validation_terminal_command="", + ) + payload = gate.build_linux_validation_terminal_dispatch_execution_payload( + dispatch_report, + source_path=report_path.resolve(), + project_root=tmp_path, + linux_validation_terminal_timeout_seconds=7200, + command_parts=command_parts, + dry_run=False, + command_result={ + "attempted": True, + "returncode": 0, + "stdout_tail": "ok", + "stderr_tail": "", + "error_type": "", + "error_message": "", + }, + ) + + assert payload["linux_validation_terminal_dispatch_execution_status"] == "dispatched" + assert ( + payload["linux_validation_terminal_dispatch_execution_decision"] + == "dispatch_linux_validation_terminal_with_follow_up" + ) + assert payload["linux_validation_terminal_dispatch_execution_exit_code"] == 1 + assert payload["linux_validation_terminal_dispatch_attempted"] is True + assert payload["linux_validation_terminal_dispatch_execution_requires_manual_action"] is False + assert payload["linux_validation_terminal_dispatch_execution_channel"] == "follow_up" + assert payload["follow_up_queue_url"].endswith("/issues/77") + assert "linux_validation_terminal_dispatch_execution_command_succeeded" in payload["reason_codes"] + + +def test_build_linux_validation_terminal_dispatch_execution_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module() + dispatch_payload = _sample_linux_validation_terminal_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "blocked" + dispatch_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_should_proceed"] = False + dispatch_payload["terminal_verdict_requires_manual_action"] = True + dispatch_payload["terminal_verdict_channel"] = "blocker" + dispatch_payload["linux_validation_should_dispatch"] = False + dispatch_payload["linux_validation_requires_manual_action"] = True + dispatch_payload["linux_validation_channel"] = "blocker" + dispatch_payload["linux_validation_dispatch_status"] = "blocked" + dispatch_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_status"] = "blocked" + dispatch_payload["linux_validation_terminal_verdict_decision"] = "halt_linux_validation_blocker" + dispatch_payload["linux_validation_terminal_verdict_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_should_proceed"] = False + dispatch_payload["linux_validation_terminal_verdict_requires_manual_action"] = True + dispatch_payload["linux_validation_terminal_verdict_channel"] = "blocker" + dispatch_payload["linux_validation_terminal_verdict_publish_status"] = "blocked" + dispatch_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_blocker" + ) + dispatch_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] = True + dispatch_payload["linux_validation_terminal_verdict_publish_channel"] = "blocker" + dispatch_payload["linux_validation_terminal_dispatch_status"] = "blocked" + dispatch_payload["linux_validation_terminal_dispatch_decision"] = ( + "hold_linux_validation_terminal_blocker" + ) + dispatch_payload["linux_validation_terminal_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_should_dispatch"] = False + dispatch_payload["linux_validation_terminal_dispatch_requires_manual_action"] = True + dispatch_payload["linux_validation_terminal_dispatch_channel"] = "blocker" + dispatch_payload["reason_codes"] = ["linux_validation_terminal_dispatch_blocked"] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_blocked.json" + report_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_terminal_dispatch_report(report_path) + command_parts = gate.build_linux_validation_terminal_command_parts( + terminal_dispatch_report=dispatch_report, + python_executable="python", + linux_validation_terminal_command="", + ) + payload = gate.build_linux_validation_terminal_dispatch_execution_payload( + dispatch_report, + source_path=report_path.resolve(), + project_root=tmp_path, + linux_validation_terminal_timeout_seconds=7200, + command_parts=command_parts, + dry_run=False, + command_result=None, + ) + + assert payload["linux_validation_terminal_dispatch_execution_status"] == "blocked" + assert ( + payload["linux_validation_terminal_dispatch_execution_decision"] + == "hold_linux_validation_terminal_blocker" + ) + assert payload["linux_validation_terminal_dispatch_execution_exit_code"] == 1 + assert payload["linux_validation_terminal_dispatch_attempted"] is False + assert payload["linux_validation_terminal_dispatch_execution_requires_manual_action"] is True + assert payload["linux_validation_terminal_dispatch_execution_channel"] == "blocker" + assert "linux_validation_terminal_dispatch_execution_blocked" in payload["reason_codes"] + + +def test_build_linux_validation_terminal_dispatch_execution_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module() + dispatch_payload = _sample_linux_validation_terminal_dispatch_payload() + dispatch_payload["linux_validation_terminal_dispatch_status"] = "ready_dry_run" + dispatch_payload["linux_validation_terminal_dispatch_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_mismatch.json" + report_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + try: + gate.load_linux_validation_terminal_dispatch_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_execution_gate_module() + dispatch_payload = _sample_linux_validation_terminal_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_channel"] = "follow_up" + dispatch_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_dispatch_decision"] = "dispatch_linux_validation_with_follow_up" + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["linux_validation_terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["linux_validation_terminal_verdict_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_terminal_verdict_publish_status"] = "published_with_follow_up" + dispatch_payload["linux_validation_terminal_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_ready_with_follow_up" + ) + dispatch_payload["linux_validation_terminal_verdict_publish_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_verdict_publish_channel"] = "follow_up" + dispatch_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + dispatch_payload["linux_validation_terminal_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_terminal_dispatch_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + dispatch_payload["linux_validation_terminal_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_terminal_dispatch_channel"] = "follow_up" + dispatch_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_ready_with_follow_up_dry_run" + ] + + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_output.json" + report_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_terminal_dispatch_report(report_path) + command_parts = gate.build_linux_validation_terminal_command_parts( + terminal_dispatch_report=dispatch_report, + python_executable="python", + linux_validation_terminal_command="", + ) + payload = gate.build_linux_validation_terminal_dispatch_execution_payload( + dispatch_report, + source_path=report_path.resolve(), + project_root=tmp_path, + linux_validation_terminal_timeout_seconds=7200, + command_parts=command_parts, + dry_run=True, + command_result=None, + ) + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_execution.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_dispatch_execution_status"] == ( + "ready_with_follow_up_dry_run" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_decision"] == ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_dispatch_execution_should_dispatch"] == "true" + assert outputs["workflow_linux_validation_terminal_dispatch_execution_attempted"] == "false" + assert ( + outputs["workflow_linux_validation_terminal_dispatch_execution_requires_manual_action"] + == "false" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_channel"] == "follow_up" + assert outputs["workflow_linux_validation_terminal_dispatch_execution_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_terminal_dispatch_execution_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_command_returncode"] == "" + assert outputs["workflow_linux_validation_terminal_dispatch_execution_report_json"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_execution.json" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_execution_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_execution.md" + ) diff --git a/tests/test_p2_lv_td_final_publish_archive_rt.py b/tests/test_p2_lv_td_final_publish_archive_rt.py new file mode 100644 index 0000000..a719e2d --- /dev/null +++ b/tests/test_p2_lv_td_final_publish_archive_rt.py @@ -0,0 +1,763 @@ +"""Contract tests for P2-76 Linux CI workflow Linux validation terminal dispatch final publish archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_final_publish_archive_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_completion_status": "run_completed_success", + "linux_validation_terminal_dispatch_completion_exit_code": 0, + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_dispatch_trace_status": "run_completed_success", + "linux_validation_terminal_dispatch_trace_exit_code": 0, + "should_poll_workflow_run": True, + "allow_in_progress": False, + "linux_validation_terminal_run_id": 9988776655, + "linux_validation_terminal_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "linux_validation_terminal_dispatch_terminal_publish_status": "terminal_published", + "linux_validation_terminal_dispatch_terminal_publish_decision": "announce_linux_validation_terminal_dispatch_completed", + "linux_validation_terminal_dispatch_terminal_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": True, + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_publish_summary": ( + "linux_validation_terminal_dispatch_completion_status=run_completed_success " + "linux_validation_terminal_dispatch_terminal_publish_status=terminal_published " + "linux_validation_terminal_dispatch_terminal_publish_decision=announce_linux_validation_terminal_dispatch_completed" + ), + "linux_validation_terminal_dispatch_final_verdict_status": "validated", + "linux_validation_terminal_dispatch_final_verdict_decision": "accept_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_final_verdict_exit_code": 0, + "linux_validation_terminal_dispatch_final_should_accept": True, + "linux_validation_terminal_dispatch_final_requires_follow_up": False, + "linux_validation_terminal_dispatch_final_should_page_owner": False, + "linux_validation_terminal_dispatch_final_channel": "release", + "linux_validation_terminal_dispatch_final_verdict_summary": ( + "linux_validation_terminal_dispatch_terminal_publish_status=terminal_published " + "linux_validation_terminal_dispatch_final_verdict_status=validated " + "linux_validation_terminal_dispatch_final_verdict_decision=accept_linux_validation_terminal_dispatch" + ), + "linux_validation_terminal_dispatch_final_verdict_publish_status": "published", + "linux_validation_terminal_dispatch_final_verdict_publish_decision": "announce_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_verdict_publish_exit_code": 0, + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify": True, + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner": False, + "linux_validation_terminal_dispatch_final_verdict_publish_channel": "release", + "linux_validation_terminal_dispatch_final_verdict_publish_summary": ( + "linux_validation_terminal_dispatch_final_verdict_status=validated " + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_verdict_publish_decision=announce_linux_validation_terminal_dispatch_validated" + ), + "reason_codes": ["linux_validation_terminal_dispatch_final_verdict_publish_validated"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_archived( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "archived" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_validated" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 0 + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "release" + ) + assert archive_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_final_publish_archive_archived" + ] + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_archived_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_execution_status"] = ( + "ready_with_follow_up_dry_run" + ) + publish_payload["linux_validation_terminal_dispatch_execution_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_published_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "follow_up" + publish_payload["linux_validation_terminal_dispatch_final_verdict_status"] = ( + "validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_should_accept"] = True + publish_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = False + publish_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = False + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "follow_up" + ) + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_publish_validated_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_follow_up.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "archived_with_follow_up" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 1 + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_final_publish_archive_archived_with_follow_up" + in archive_payload["reason_codes"] + ) + assert archive_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_in_progress( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_status"] = "in_progress" + publish_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "keep_linux_validation_terminal_dispatch_in_progress" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 0 + publish_payload["linux_validation_terminal_dispatch_final_should_accept"] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = False + publish_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "in_progress" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_in_progress" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = 0 + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = False + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "follow_up" + ) + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_publish_in_progress" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_in_progress.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "in_progress" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_in_progress" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 0 + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "follow_up" + ) + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_completion_status"] = "blocked" + publish_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + publish_payload["linux_validation_terminal_should_dispatch"] = False + publish_payload["linux_validation_terminal_dispatch_execution_status"] = "blocked" + publish_payload[ + "linux_validation_terminal_dispatch_execution_decision" + ] = "hold_linux_validation_terminal_blocker" + publish_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_attempted"] = False + publish_payload["linux_validation_terminal_dispatch_trace_status"] = "blocked" + publish_payload["linux_validation_terminal_dispatch_trace_exit_code"] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_blocked" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "blocker" + publish_payload["linux_validation_terminal_dispatch_final_verdict_status"] = "blocked" + publish_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "escalate_linux_validation_terminal_dispatch_blocker" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_should_accept"] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = True + publish_payload["linux_validation_terminal_dispatch_final_channel"] = "blocker" + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "blocked" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_blocker" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "blocker" + ) + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_publish_blocked" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_blocked.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "blocked" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_blocker" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 1 + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + is True + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_final_publish_archive_blocked" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_failed( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_status"] = "failed" + publish_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "escalate_linux_validation_terminal_dispatch_failure" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_should_accept"] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = True + publish_payload["linux_validation_terminal_dispatch_final_channel"] = "blocker" + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "failed" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_failure" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "blocker" + ) + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_publish_failed" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_failed.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "failed" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_failure" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 1 + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + is True + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_final_publish_archive_failed" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_final_publish_archive_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "published" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "follow_up" + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_mismatch.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] + == "contract_failed" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "abort_linux_validation_terminal_dispatch_final_publish_archive" + ) + assert ( + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_decision_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_should_notify_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_channel_mismatch" + in archive_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_status"] = ( + "validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_should_accept"] = True + publish_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + publish_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = False + publish_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] = ( + "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] = False + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] = ( + "follow_up" + ) + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_publish_validated_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_output.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_publish_report( + report_path + ) + archive_payload = gate.build_linux_validation_terminal_dispatch_final_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_status"] + == "archived_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_decision"] + == "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_exit_code"] + == "1" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_should_archive"] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_should_page_owner" + ] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_channel"] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_publish_status" + ] + == "published_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_publish_channel" + ] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_publish_archive_run_id"] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_report_json" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_publish_archive_report_markdown" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.md") diff --git a/tests/test_p2_lv_td_final_verdict_publish_rt.py b/tests/test_p2_lv_td_final_verdict_publish_rt.py new file mode 100644 index 0000000..0b9b057 --- /dev/null +++ b/tests/test_p2_lv_td_final_verdict_publish_rt.py @@ -0,0 +1,492 @@ +"""Contract tests for P2-75 Linux CI workflow Linux validation terminal dispatch final verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_final_verdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_final_verdict_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_completion_status": "run_completed_success", + "linux_validation_terminal_dispatch_completion_exit_code": 0, + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_dispatch_trace_status": "run_completed_success", + "linux_validation_terminal_dispatch_trace_exit_code": 0, + "should_poll_workflow_run": True, + "allow_in_progress": False, + "linux_validation_terminal_run_id": 9988776655, + "linux_validation_terminal_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "linux_validation_terminal_dispatch_terminal_publish_status": "terminal_published", + "linux_validation_terminal_dispatch_terminal_publish_decision": "announce_linux_validation_terminal_dispatch_completed", + "linux_validation_terminal_dispatch_terminal_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": True, + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_publish_summary": ( + "linux_validation_terminal_dispatch_completion_status=run_completed_success " + "linux_validation_terminal_dispatch_terminal_publish_status=terminal_published " + "linux_validation_terminal_dispatch_terminal_publish_decision=announce_linux_validation_terminal_dispatch_completed" + ), + "linux_validation_terminal_dispatch_final_verdict_status": "validated", + "linux_validation_terminal_dispatch_final_verdict_decision": "accept_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_final_verdict_exit_code": 0, + "linux_validation_terminal_dispatch_final_should_accept": True, + "linux_validation_terminal_dispatch_final_requires_follow_up": False, + "linux_validation_terminal_dispatch_final_should_page_owner": False, + "linux_validation_terminal_dispatch_final_channel": "release", + "linux_validation_terminal_dispatch_final_verdict_summary": ( + "linux_validation_terminal_dispatch_terminal_publish_status=terminal_published " + "linux_validation_terminal_dispatch_final_verdict_status=validated " + "linux_validation_terminal_dispatch_final_verdict_decision=accept_linux_validation_terminal_dispatch" + ), + "reason_codes": ["linux_validation_terminal_dispatch_final_verdict_validated"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_final_verdict_publish_payload_published( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module() + ) + final_verdict_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_payload() + ) + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict.json" + ) + report_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_report(report_path) + publish_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + == "published" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] + == "announce_linux_validation_terminal_dispatch_validated" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + == 0 + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_should_notify"] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] + is False + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] + == "release" + ) + assert publish_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_final_verdict_publish_validated" + ] + + +def test_build_linux_validation_terminal_dispatch_final_verdict_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module() + ) + final_verdict_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_payload() + ) + final_verdict_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_dispatch_execution_status"] = ( + "ready_with_follow_up_dry_run" + ) + final_verdict_payload["linux_validation_terminal_dispatch_execution_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + final_verdict_payload["follow_up_queue_url"] = ( + "https://github.com/acme/demo/issues/77" + ) + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_published_with_follow_up" + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "follow_up" + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_status"] = ( + "validated_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_dispatch_final_should_accept"] = True + final_verdict_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = False + final_verdict_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + final_verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_validated_with_follow_up" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_follow_up.json" + ) + report_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_report(report_path) + publish_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] + == "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + == 1 + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_should_notify"] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] + is False + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_validated_with_follow_up" + in publish_payload["reason_codes"] + ) + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_final_verdict_publish_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module() + ) + final_verdict_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_payload() + ) + final_verdict_payload["linux_validation_terminal_dispatch_completion_status"] = "blocked" + final_verdict_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_should_dispatch"] = False + final_verdict_payload["linux_validation_terminal_dispatch_execution_status"] = "blocked" + final_verdict_payload[ + "linux_validation_terminal_dispatch_execution_decision" + ] = "hold_linux_validation_terminal_blocker" + final_verdict_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_dispatch_attempted"] = False + final_verdict_payload["linux_validation_terminal_dispatch_trace_status"] = "blocked" + final_verdict_payload["linux_validation_terminal_dispatch_trace_exit_code"] = 1 + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_blocked" + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] = False + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] = True + final_verdict_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "blocker" + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_status"] = "blocked" + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "escalate_linux_validation_terminal_dispatch_blocker" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_dispatch_final_should_accept"] = False + final_verdict_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_channel"] = "blocker" + final_verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_blocked" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_blocked.json" + ) + report_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_report(report_path) + publish_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + == "blocked" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] + == "announce_linux_validation_terminal_dispatch_blocker" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + == 1 + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] + is True + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_channel"] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_publish_blocked" + in publish_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_final_verdict_publish_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module() + ) + final_verdict_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_payload() + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_status"] = "validated" + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_should_accept"] = False + final_verdict_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_mismatch.json" + ) + report_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_report(report_path) + publish_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_status"] + == "contract_failed" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_decision"] + == "abort_linux_validation_terminal_dispatch_final_verdict_publish" + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_final_verdict_decision_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_should_accept_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_requires_follow_up_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_should_page_owner_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_final_channel_mismatch" + in publish_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish_gate_module() + ) + final_verdict_payload = ( + _sample_linux_validation_terminal_dispatch_final_verdict_payload() + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_status"] = ( + "validated_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_decision"] = ( + "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + final_verdict_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] = 1 + final_verdict_payload["linux_validation_terminal_dispatch_final_should_accept"] = True + final_verdict_payload[ + "linux_validation_terminal_dispatch_final_requires_follow_up" + ] = True + final_verdict_payload["linux_validation_terminal_dispatch_final_should_page_owner"] = False + final_verdict_payload["linux_validation_terminal_dispatch_final_channel"] = "follow_up" + final_verdict_payload["follow_up_queue_url"] = ( + "https://github.com/acme/demo/issues/77" + ) + final_verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_verdict_validated_with_follow_up" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_verdict_output.json" + ) + report_path.write_text(json.dumps(final_verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_verdict_report(report_path) + publish_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_decision"] + == "announce_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_exit_code"] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_should_notify" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_should_page_owner" + ] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_channel"] + == "follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_final_status"] + == "validated_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_final_channel"] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_publish_run_id"] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_report_json" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_verdict_publish_report_markdown" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.md") diff --git a/tests/test_p2_lv_td_final_verdict_rt.py b/tests/test_p2_lv_td_final_verdict_rt.py new file mode 100644 index 0000000..04d94ca --- /dev/null +++ b/tests/test_p2_lv_td_final_verdict_rt.py @@ -0,0 +1,437 @@ +"""Contract tests for P2-74 Linux CI workflow Linux validation terminal dispatch final verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_final_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_publish_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_completion_status": "run_completed_success", + "linux_validation_terminal_dispatch_completion_exit_code": 0, + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_dispatch_trace_status": "run_completed_success", + "linux_validation_terminal_dispatch_trace_exit_code": 0, + "should_poll_workflow_run": True, + "allow_in_progress": False, + "linux_validation_terminal_run_id": 9988776655, + "linux_validation_terminal_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "linux_validation_terminal_dispatch_terminal_publish_status": "terminal_published", + "linux_validation_terminal_dispatch_terminal_publish_decision": "announce_linux_validation_terminal_dispatch_completed", + "linux_validation_terminal_dispatch_terminal_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff": True, + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_publish_summary": ( + "linux_validation_terminal_dispatch_completion_status=run_completed_success " + "linux_validation_terminal_dispatch_terminal_publish_status=terminal_published " + "linux_validation_terminal_dispatch_terminal_publish_decision=announce_linux_validation_terminal_dispatch_completed" + ), + "reason_codes": ["linux_validation_terminal_dispatch_terminal_publish_completed"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_final_verdict_payload_validated( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module() + terminal_publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_publish_payload() + ) + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json" + ) + report_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_publish_report( + report_path + ) + final_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_status"] + == "validated" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_decision"] + == "accept_linux_validation_terminal_dispatch" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] == 0 + ) + assert final_payload["linux_validation_terminal_dispatch_final_should_accept"] is True + assert ( + final_payload["linux_validation_terminal_dispatch_final_requires_follow_up"] + is False + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_should_page_owner"] + is False + ) + assert final_payload["linux_validation_terminal_dispatch_final_channel"] == "release" + assert final_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_final_verdict_validated" + ] + + +def test_build_linux_validation_terminal_dispatch_final_verdict_payload_validated_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module() + terminal_publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_publish_payload() + ) + terminal_publish_payload["linux_validation_terminal_dispatch_completion_status"] = ( + "run_completed_success" + ) + terminal_publish_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_dispatch_execution_status"] = ( + "ready_with_follow_up_dry_run" + ) + terminal_publish_payload["linux_validation_terminal_dispatch_execution_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + terminal_publish_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + terminal_publish_payload["follow_up_queue_url"] = ( + "https://github.com/acme/demo/issues/77" + ) + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_published_with_follow_up" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "follow_up" + terminal_publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_publish_with_follow_up" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_publish_follow_up.json" + ) + report_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_publish_report( + report_path + ) + final_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_status"] + == "validated_with_follow_up" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_decision"] + == "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] == 1 + ) + assert final_payload["linux_validation_terminal_dispatch_final_should_accept"] is True + assert ( + final_payload["linux_validation_terminal_dispatch_final_requires_follow_up"] + is True + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_should_page_owner"] + is False + ) + assert final_payload["linux_validation_terminal_dispatch_final_channel"] == "follow_up" + assert ( + "linux_validation_terminal_dispatch_final_verdict_validated_with_follow_up" + in final_payload["reason_codes"] + ) + assert final_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_final_verdict_payload_blocked( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module() + terminal_publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_publish_payload() + ) + terminal_publish_payload["linux_validation_terminal_dispatch_completion_status"] = "blocked" + terminal_publish_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_should_dispatch"] = False + terminal_publish_payload["linux_validation_terminal_dispatch_execution_status"] = "blocked" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_execution_decision" + ] = "hold_linux_validation_terminal_blocker" + terminal_publish_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + terminal_publish_payload["linux_validation_terminal_dispatch_attempted"] = False + terminal_publish_payload["linux_validation_terminal_dispatch_trace_status"] = "blocked" + terminal_publish_payload["linux_validation_terminal_dispatch_trace_exit_code"] = 1 + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_blocked" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] = False + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] = True + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "blocker" + terminal_publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_publish_blocked" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_publish_blocked.json" + ) + report_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_publish_report( + report_path + ) + final_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_status"] + == "blocked" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_decision"] + == "escalate_linux_validation_terminal_dispatch_blocker" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] == 1 + ) + assert final_payload["linux_validation_terminal_dispatch_final_should_accept"] is False + assert ( + final_payload["linux_validation_terminal_dispatch_final_requires_follow_up"] + is True + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_should_page_owner"] + is True + ) + assert final_payload["linux_validation_terminal_dispatch_final_channel"] == "blocker" + assert ( + "linux_validation_terminal_dispatch_final_verdict_blocked" + in final_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_final_verdict_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module() + terminal_publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_publish_payload() + ) + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_published" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] = False + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] = True + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "follow_up" + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_publish_mismatch.json" + ) + report_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_publish_report( + report_path + ) + final_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_status"] + == "contract_failed" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_decision"] + == "abort_linux_validation_terminal_dispatch_final_verdict" + ) + assert ( + final_payload["linux_validation_terminal_dispatch_final_verdict_exit_code"] == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_publish_decision_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action_mismatch" + in final_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_publish_channel_mismatch" + in final_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_final_verdict_gate_module() + terminal_publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_publish_payload() + ) + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] = "terminal_published_with_follow_up" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] = 1 + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] = "follow_up" + terminal_publish_payload["follow_up_queue_url"] = ( + "https://github.com/acme/demo/issues/77" + ) + terminal_publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_publish_with_follow_up" + ] + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_publish_output.json" + ) + report_path.write_text(json.dumps(terminal_publish_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_publish_report( + report_path + ) + final_payload = gate.build_linux_validation_terminal_dispatch_final_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + final_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_final_verdict.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_status"] + == "validated_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_decision"] + == "accept_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_verdict_exit_code"] + == "1" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_should_accept"] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_requires_follow_up" + ] + == "true" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_should_page_owner"] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_final_channel"] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_terminal_publish_status" + ] + == "terminal_published_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_final_terminal_publish_channel" + ] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_follow_up_queue_url" + ].endswith("/issues/77") + assert outputs["workflow_linux_validation_terminal_dispatch_final_run_id"] == "9988776655" + assert outputs["workflow_linux_validation_terminal_dispatch_final_run_url"].endswith( + "/actions/runs/9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_report_json" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict.json") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_final_report_markdown" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_final_verdict.md") diff --git a/tests/test_p2_lv_td_terminal_publish_rt.py b/tests/test_p2_lv_td_terminal_publish_rt.py new file mode 100644 index 0000000..6d73f6b --- /dev/null +++ b/tests/test_p2_lv_td_terminal_publish_rt.py @@ -0,0 +1,351 @@ +"""Contract tests for P2-73 Linux CI workflow Linux validation terminal dispatch terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_terminal_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_completion_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "project_root": "/tmp", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_dispatch_trace_status": "run_completed_success", + "linux_validation_terminal_dispatch_trace_exit_code": 0, + "linux_validation_terminal_dispatch_completion_status": "run_completed_success", + "linux_validation_terminal_dispatch_completion_exit_code": 0, + "dry_run": False, + "allow_in_progress": False, + "should_poll_workflow_run": True, + "linux_validation_terminal_run_id": 9988776655, + "linux_validation_terminal_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "linux_validation_terminal_repo_owner": "acme", + "linux_validation_terminal_repo_name": "demo", + "linux_validation_terminal_poll_command": "gh run view 9988776655 --json databaseId,status,conclusion,url --repo acme/demo", + "linux_validation_terminal_poll_command_parts": [ + "gh", + "run", + "view", + "9988776655", + "--json", + "databaseId,status,conclusion,url", + "--repo", + "acme/demo", + ], + "poll_interval_seconds": 20, + "max_polls": 15, + "poll_timeout_seconds": 600, + "poll_iterations": 1, + "poll_attempted": True, + "poll_returncode": 0, + "poll_status": "completed", + "poll_conclusion": "success", + "poll_url": "https://github.com/acme/demo/actions/runs/9988776655", + "poll_error_type": "", + "poll_error_message": "", + "poll_stdout_tail": '{"status":"completed","conclusion":"success"}', + "poll_stderr_tail": "", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_run_completed_success"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_publish_payload_terminal_published( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate_module() + ) + completion_payload = _sample_linux_validation_terminal_dispatch_completion_payload() + + completion_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_completion.json" + ) + completion_path.write_text(json.dumps(completion_payload, indent=2), encoding="utf-8") + + completion_report = gate.load_linux_validation_terminal_dispatch_completion_report( + completion_path + ) + terminal_publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report, + source_path=completion_path.resolve(), + ) + ) + + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] + == "terminal_published" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_completed" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] + == 0 + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_should_notify" + ] + is True + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] + is True + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] + is False + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_channel" + ] + == "release" + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_publish_payload_terminal_in_progress( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate_module() + ) + completion_payload = _sample_linux_validation_terminal_dispatch_completion_payload() + completion_payload["linux_validation_terminal_dispatch_completion_status"] = ( + "run_await_timeout" + ) + completion_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 0 + completion_payload["allow_in_progress"] = True + completion_payload["linux_validation_terminal_dispatch_trace_status"] = "run_in_progress" + completion_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_run_await_timeout" + ] + + completion_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_completion_timeout.json" + ) + completion_path.write_text(json.dumps(completion_payload, indent=2), encoding="utf-8") + + completion_report = gate.load_linux_validation_terminal_dispatch_completion_report( + completion_path + ) + terminal_publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report, + source_path=completion_path.resolve(), + ) + ) + + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] + == "terminal_in_progress" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_in_progress" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] + == 0 + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] + is False + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] + is False + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_publish_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate_module() + ) + completion_payload = _sample_linux_validation_terminal_dispatch_completion_payload() + completion_payload["linux_validation_terminal_dispatch_completion_status"] = ( + "run_completed_success" + ) + completion_payload["linux_validation_terminal_dispatch_completion_exit_code"] = 1 + + completion_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_completion_mismatch.json" + ) + completion_path.write_text(json.dumps(completion_payload, indent=2), encoding="utf-8") + + completion_report = gate.load_linux_validation_terminal_dispatch_completion_report( + completion_path + ) + terminal_publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report, + source_path=completion_path.resolve(), + ) + ) + + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_status" + ] + == "terminal_contract_failed" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_publish" + ) + assert ( + terminal_publish_payload[ + "linux_validation_terminal_dispatch_terminal_publish_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_completion_exit_code_mismatch_success" + in terminal_publish_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_publish_gate_module() + ) + completion_payload = _sample_linux_validation_terminal_dispatch_completion_payload() + completion_payload["linux_validation_terminal_dispatch_execution_status"] = ( + "ready_with_follow_up_dry_run" + ) + completion_payload["linux_validation_terminal_dispatch_execution_decision"] = ( + "dispatch_linux_validation_terminal_with_follow_up" + ) + completion_payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + completion_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + + completion_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_completion_output.json" + ) + completion_path.write_text(json.dumps(completion_payload, indent=2), encoding="utf-8") + + completion_report = gate.load_linux_validation_terminal_dispatch_completion_report( + completion_path + ) + terminal_publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_publish_payload( + completion_report, + source_path=completion_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + terminal_publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_publish_status"] + == "terminal_published_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_publish_decision"] + == "announce_linux_validation_terminal_dispatch_completed_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_publish_exit_code"] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_should_notify" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_ready_for_handoff" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_requires_manual_action" + ] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_publish_channel"] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_publish_run_id"] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_report_json" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_publish_report_markdown" + ].endswith("ci_workflow_linux_validation_terminal_dispatch_terminal_publish.md") diff --git a/tests/test_p2_lv_td_terminal_verdict_rt.py b/tests/test_p2_lv_td_terminal_verdict_rt.py new file mode 100644 index 0000000..552cd79 --- /dev/null +++ b/tests/test_p2_lv_td_terminal_verdict_rt.py @@ -0,0 +1,377 @@ +"""Contract tests for P2-77 Linux CI workflow Linux validation terminal dispatch terminal verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_terminal_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_final_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_final_publish_archive_decision": "archive_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_final_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_final_publish_archive_summary": ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_final_publish_archive_decision=archive_linux_validation_terminal_dispatch_validated" + ), + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_final_publish_archive_archived"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_payload_ready( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + == "ready_for_linux_validation_terminal_dispatch" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "proceed_linux_validation_terminal_dispatch" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] == 0 + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed" + ] + is True + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] + is False + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner" + ] + is False + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] + == "release" + ) + assert verdict_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_terminal_verdict_ready" + ] + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_payload_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = "follow_up" + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_final_publish_archive_archived_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_follow_up.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] == 1 + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed" + ] + is True + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] + == "follow_up" + ) + assert verdict_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_payload_in_progress( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "in_progress" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_in_progress" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 0 + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] = ( + False + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_in_progress.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + == "in_progress" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "hold_linux_validation_terminal_dispatch_in_progress" + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed" + ] + is False + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_payload_blocked( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "blocked" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_blocker" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] = ( + False + ) + archive_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] = True + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_should_page_owner"] = ( + True + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "blocker" + ) + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_blocked.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + == "blocked" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "halt_linux_validation_terminal_dispatch_blocker" + ) + assert ( + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] + is True + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_mismatch.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] + == "contract_failed" + ) + assert ( + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "abort_linux_validation_terminal_dispatch_terminal_verdict" + ) + assert ( + "linux_validation_terminal_dispatch_final_publish_archive_decision_mismatch" + in verdict_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_terminal_dispatch_final_publish_archive_payload() + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_final_publish_archive_output.json" + ) + report_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_final_publish_archive_report( + report_path + ) + verdict_payload = gate.build_linux_validation_terminal_dispatch_terminal_verdict_payload( + report, + source_path=report_path.resolve(), + ) + outputs = gate.build_github_output_values( + verdict_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_status"] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_decision"] + == "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_exit_code"] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_should_proceed" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_follow_up_queue_url" + ].endswith("/issues/77") + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_report_json"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json" + ) + ) diff --git a/tests/test_p2_lv_td_trace_rt.py b/tests/test_p2_lv_td_trace_rt.py new file mode 100644 index 0000000..cbfa706 --- /dev/null +++ b/tests/test_p2_lv_td_trace_rt.py @@ -0,0 +1,223 @@ +"""Contract tests for P2-71 Linux CI workflow Linux validation terminal dispatch trace gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_trace_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_trace_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_execution_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "project_root": "/tmp", + "linux_validation_terminal_dispatch_execution_status": "dispatched", + "linux_validation_terminal_dispatch_execution_decision": "dispatch_linux_validation_terminal", + "linux_validation_terminal_dispatch_execution_exit_code": 0, + "linux_validation_terminal_dispatch_execution_requires_manual_action": False, + "linux_validation_terminal_dispatch_execution_channel": "release", + "linux_validation_terminal_should_dispatch": True, + "linux_validation_terminal_dispatch_attempted": True, + "linux_validation_terminal_command": "python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure", + "linux_validation_terminal_command_parts": [ + "python", + "scripts/run_p2_linux_unified_pipeline_gate.py", + "--continue-on-failure", + ], + "linux_validation_terminal_dispatch_command_returncode": 0, + "linux_validation_terminal_dispatch_command_stdout_tail": ( + "created workflow run: https://github.com/acme/demo/actions/runs/9988776655" + ), + "linux_validation_terminal_dispatch_command_stderr_tail": "", + "linux_validation_terminal_dispatch_error_type": "", + "linux_validation_terminal_dispatch_error_message": "", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_execution_command_succeeded"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_trace_payload_extracts_run_reference( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module() + payload = _sample_linux_validation_terminal_dispatch_execution_payload() + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_execution.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + execution_report = gate.load_linux_validation_terminal_dispatch_execution_report(report_path) + trace_payload = gate.build_linux_validation_terminal_dispatch_trace_payload( + execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["linux_validation_terminal_dispatch_trace_status"] == "run_tracking_ready" + assert trace_payload["linux_validation_terminal_should_poll_workflow_run"] is True + assert trace_payload["linux_validation_terminal_run_id"] == 9988776655 + assert trace_payload["linux_validation_terminal_repo_owner"] == "acme" + assert trace_payload["linux_validation_terminal_repo_name"] == "demo" + assert trace_payload["linux_validation_terminal_dispatch_trace_exit_code"] == 0 + assert trace_payload["linux_validation_terminal_poll_attempted"] is False + assert trace_payload["linux_validation_terminal_poll_command_parts"][:3] == [ + "gh", + "run", + "view", + ] + + +def test_build_linux_validation_terminal_dispatch_trace_payload_handles_missing_run_reference( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module() + payload = _sample_linux_validation_terminal_dispatch_execution_payload() + payload["linux_validation_terminal_dispatch_command_stdout_tail"] = ( + "dispatch succeeded without URL" + ) + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_execution_missing_url.json" + ) + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + execution_report = gate.load_linux_validation_terminal_dispatch_execution_report(report_path) + trace_payload = gate.build_linux_validation_terminal_dispatch_trace_payload( + execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert ( + trace_payload["linux_validation_terminal_dispatch_trace_status"] + == "run_tracking_missing" + ) + assert trace_payload["linux_validation_terminal_should_poll_workflow_run"] is False + assert trace_payload["linux_validation_terminal_dispatch_trace_exit_code"] == 1 + assert ( + "linux_validation_terminal_dispatch_run_reference_missing" + in trace_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_trace_payload_dispatch_failed_passthrough( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module() + payload = _sample_linux_validation_terminal_dispatch_execution_payload() + payload["linux_validation_terminal_dispatch_execution_status"] = "dispatch_failed" + payload["linux_validation_terminal_dispatch_execution_exit_code"] = 1 + payload["linux_validation_terminal_dispatch_command_returncode"] = 1 + payload["linux_validation_terminal_dispatch_command_stdout_tail"] = "" + payload["linux_validation_terminal_dispatch_command_stderr_tail"] = "dispatch failed" + payload["reason_codes"] = ["linux_validation_terminal_dispatch_execution_command_failed"] + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_execution_failed.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + execution_report = gate.load_linux_validation_terminal_dispatch_execution_report(report_path) + trace_payload = gate.build_linux_validation_terminal_dispatch_trace_payload( + execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=True, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + + assert trace_payload["linux_validation_terminal_dispatch_trace_status"] == "dispatch_failed" + assert trace_payload["linux_validation_terminal_should_poll_workflow_run"] is False + assert trace_payload["linux_validation_terminal_run_id"] is None + assert trace_payload["linux_validation_terminal_dispatch_trace_exit_code"] == 1 + assert trace_payload["linux_validation_terminal_poll_attempted"] is False + + +def test_load_linux_validation_terminal_dispatch_execution_report_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module() + payload = _sample_linux_validation_terminal_dispatch_execution_payload() + payload["linux_validation_terminal_dispatch_execution_status"] = "blocked" + payload["linux_validation_terminal_should_dispatch"] = True + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_execution_mismatch.json" + ) + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + try: + gate.load_linux_validation_terminal_dispatch_execution_report(report_path) + raised = False + except ValueError: + raised = True + + assert raised + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_dispatch_trace_gate_module() + payload = _sample_linux_validation_terminal_dispatch_execution_payload() + report_path = tmp_path / "ci_workflow_linux_validation_terminal_dispatch_execution.json" + report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + execution_report = gate.load_linux_validation_terminal_dispatch_execution_report(report_path) + trace_payload = gate.build_linux_validation_terminal_dispatch_trace_payload( + execution_report, + source_path=report_path.resolve(), + gh_executable="gh", + poll_now=False, + dry_run=False, + project_root=tmp_path, + poll_timeout_seconds=300, + ) + outputs = gate.build_github_output_values( + trace_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_trace.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_dispatch_trace_status"] == ( + "run_tracking_ready" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_trace_should_poll"] == "true" + assert outputs["workflow_linux_validation_terminal_dispatch_trace_run_id"] == "9988776655" + assert outputs["workflow_linux_validation_terminal_dispatch_trace_run_url"].endswith( + "/actions/runs/9988776655" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_trace_exit_code"] == "0" + assert outputs["workflow_linux_validation_terminal_dispatch_trace_report_json"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_trace.json" + ) + assert outputs["workflow_linux_validation_terminal_dispatch_trace_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_dispatch_trace.md" + ) + diff --git a/tests/test_p2_lv_td_tverdict_closure_publish_archive_rt.py b/tests/test_p2_lv_td_tverdict_closure_publish_archive_rt.py new file mode 100644 index 0000000..621307d --- /dev/null +++ b/tests/test_p2_lv_td_tverdict_closure_publish_archive_rt.py @@ -0,0 +1,609 @@ +"""Contract tests for P2-82 Linux CI workflow Linux validation terminal dispatch terminal verdict closure publish archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_tverdict_closure_publish_archive_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() -> dict: + return { + "generated_at": "2026-05-04T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_terminal_verdict_closure_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json", + "source_linux_validation_terminal_dispatch_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_final_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_final_publish_archive_decision": "archive_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_final_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_final_publish_archive_summary": ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_final_publish_archive_decision=archive_linux_validation_terminal_dispatch_validated" + ), + "linux_validation_terminal_dispatch_terminal_verdict_closure_status": "ready_for_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision": "proceed_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed": True, + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_closure_summary": ( + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision=proceed_linux_validation_terminal_dispatch" + ), + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status": "published", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision": "announce_linux_validation_terminal_dispatch_ready", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_summary": ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status=published " + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision=announce_linux_validation_terminal_dispatch_ready" + ), + "gate_manifest_drift_status": "passed", + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload_archived( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "archived" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code" + ] + == 0 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel" + ] + == "release" + ) + assert archive_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_archived" + ] + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload_archived_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel" + ] = "follow_up" + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_follow_up.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "archived_with_follow_up" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code" + ] + == 1 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel" + ] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_archived_with_follow_up" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "blocked" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "halt_linux_validation_terminal_dispatch_blocker" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"] = ( + False + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] = True + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "blocker" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] = ( + "blocked" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel" + ] = "blocker" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_blocked" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_blocked.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "blocked" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_blocker" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code" + ] + == 1 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel" + ] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_blocked" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] = ( + "published" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel" + ] = "follow_up" + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_mismatch.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "contract_failed" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel_mismatch" + in archive_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] = 1 + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel" + ] = "follow_up" + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_output.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + ), + ) + + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "archived_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_exit_code" + ] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_archive" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_should_page_owner" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_publish_status" + ] + == "published_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_publish_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_gate_manifest_drift_status" + ] + == "passed" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_run_id" + ] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_json" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.json" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_report_markdown" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive.md" + ) diff --git a/tests/test_p2_lv_td_tverdict_closure_publish_rt.py b/tests/test_p2_lv_td_tverdict_closure_publish_rt.py new file mode 100644 index 0000000..b3a5389 --- /dev/null +++ b/tests/test_p2_lv_td_tverdict_closure_publish_rt.py @@ -0,0 +1,501 @@ +"""Contract tests for P2-81 Linux CI workflow Linux validation terminal dispatch terminal verdict closure publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_tverdict_closure_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_final_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_final_publish_archive_decision": "archive_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_final_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_final_publish_archive_summary": ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_final_publish_archive_decision=archive_linux_validation_terminal_dispatch_validated" + ), + "linux_validation_terminal_dispatch_terminal_verdict_closure_status": "ready_for_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision": "proceed_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed": True, + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_closure_summary": ( + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_terminal_verdict_closure_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision=proceed_linux_validation_terminal_dispatch" + ), + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_terminal_verdict_closure_ready"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload_published( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + == "published" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_ready" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] + == 0 + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify" + ] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] + is False + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] + is False + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + == "release" + ) + assert publish_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready" + ] + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "follow_up" + ) + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_follow_up.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + == "published_with_follow_up" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] + == 1 + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_ready_with_follow_up" + in publish_payload["reason_codes"] + ) + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = "blocked" + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] = ( + False + ) + verdict_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = "blocked" + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "halt_linux_validation_terminal_dispatch_blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"] = False + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = "blocker" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_blocked" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_blocked.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + == "blocked" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_blocker" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] + == 1 + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] + is True + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_blocked" + in publish_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "ready_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed"] = False + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "follow_up" + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_mismatch.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + == "contract_failed" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_verdict_closure_publish" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel_mismatch" + in publish_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_closure_payload() + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_closure_channel"] = ( + "follow_up" + ) + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_closure_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_output.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_closure_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_status"] + == "published_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_decision"] + == "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_exit_code"] + == "1" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_notify"] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_should_page_owner" + ] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_channel"] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_terminal_verdict_closure_status" + ] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_terminal_verdict_closure_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_gate_manifest_drift_status" + ] + == "passed" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_run_id"] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_json" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.json" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_report_markdown" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish.md" + ) + + + diff --git a/tests/test_p2_lv_td_tverdict_closure_rt.py b/tests/test_p2_lv_td_tverdict_closure_rt.py new file mode 100644 index 0000000..c329eac --- /dev/null +++ b/tests/test_p2_lv_td_tverdict_closure_rt.py @@ -0,0 +1,876 @@ +"""Contract tests for P2-80 Linux CI workflow Linux validation terminal dispatch terminal verdict closure gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module(): + script_path = Path("scripts") / "run_p2_lv_td_tverdict_closure_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_terminal_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json", + "source_linux_validation_terminal_dispatch_terminal_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json", + "source_linux_validation_terminal_dispatch_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_terminal_verdict_publish_status": "published", + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision": "announce_linux_validation_terminal_dispatch_ready", + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_publish_summary": ( + "linux_validation_terminal_dispatch_terminal_verdict_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=published " + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision=announce_linux_validation_terminal_dispatch_ready" + ), + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision": "archive_linux_validation_terminal_dispatch_ready", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_summary": ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=published " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status=archived " + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision=archive_linux_validation_terminal_dispatch_ready" + ), + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived" + ], + "structural_issues": [], + "missing_artifacts": [], + } + + +def _sample_gate_manifest_drift_payload() -> dict: + return { + "status": "passed", + "missing_runtime_tests": [], + "missing_manifest_entries": [], + "orphan_manifest_entries": [], + "structural_issues": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_ready( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "ready_for_linux_validation_terminal_dispatch" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "proceed_linux_validation_terminal_dispatch" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 0 + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "release" + ) + assert closure_payload["gate_manifest_drift_status"] == "passed" + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_ready_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] = "published_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] = 1 + archive_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] = ( + "follow_up" + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "archived_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "follow_up" + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived_with_follow_up" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_follow_up.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 1 + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "follow_up" + ) + assert closure_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_in_progress( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] = "in_progress" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_in_progress" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "in_progress" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_in_progress" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] = False + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "follow_up" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_in_progress" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_in_progress.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "in_progress" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "hold_linux_validation_terminal_dispatch_in_progress" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 0 + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "follow_up" + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] = "blocked" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = True + archive_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] = ( + "blocker" + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "blocked" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_blocker" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] = False + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "blocker" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_blocked" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_blocked.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "blocked" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "halt_linux_validation_terminal_dispatch_blocker" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 1 + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "blocker" + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_failed( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] = "failed" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_failure" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = True + archive_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] = ( + "blocker" + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "failed" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_failure" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] = False + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "blocker" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_failed" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_failed.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "failed" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "halt_linux_validation_terminal_dispatch_failure" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 1 + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + is False + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + is True + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "blocker" + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload_contract_failed_on_mismatch_and_drift( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "archived" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] = False + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] = True + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "follow_up" + + drift_payload = _sample_gate_manifest_drift_payload() + drift_payload["status"] = "failed" + drift_payload["missing_runtime_tests"] = ["test_missing_gate_runtime.py"] + drift_payload["missing_manifest_entries"] = ["tests/test_missing_gate_runtime.py"] + drift_payload["orphan_manifest_entries"] = ["tests/test_orphan_gate_runtime.py"] + drift_payload["structural_issues"] = ["missing_gate_runtime_tests"] + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_mismatch.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift_failed.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "contract_failed" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_verdict_closure" + ) + assert ( + closure_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision_mismatch" + in closure_payload["structural_issues"] + ) + assert "gate_manifest_drift_failed" in closure_payload["structural_issues"] + assert ( + "gate_manifest_drift_missing_runtime_tests" in closure_payload["structural_issues"] + ) + assert ( + "gate_manifest_drift_missing_manifest_entries" + in closure_payload["structural_issues"] + ) + assert ( + "gate_manifest_drift_orphan_manifest_entries" + in closure_payload["structural_issues"] + ) + assert ( + "gate_manifest_drift_structural_issues" in closure_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_module() + ) + archive_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload() + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_status" + ] = "published_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] = 1 + archive_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] = ( + "follow_up" + ) + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] = "archived_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] = "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] = 1 + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] = "follow_up" + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived_with_follow_up" + ] + + drift_payload = _sample_gate_manifest_drift_payload() + + archive_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_output.json" + ) + drift_path = tmp_path / "linux_gate_manifest_drift.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + drift_path.write_text(json.dumps(drift_payload, indent=2), encoding="utf-8") + + archive_report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report( + archive_path + ) + ) + drift_report = gate.load_gate_manifest_drift_report(drift_path) + closure_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_closure_payload( + archive_report, + drift_report, + archive_source_path=archive_path.resolve(), + drift_source_path=drift_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + closure_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md" + ), + ) + + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_status" + ] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_decision" + ] + == "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_exit_code" + ] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_should_proceed" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_should_page_owner" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_status" + ] + == "archived_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_publish_archive_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_status" + ] + == "passed" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_missing_runtime_tests" + ] + == "0" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_missing_manifest_entries" + ] + == "0" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_gate_manifest_drift_orphan_manifest_entries" + ] + == "0" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_run_id" + ] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_report_json" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.json" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_closure_report_markdown" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_closure.md" + ) diff --git a/tests/test_p2_lv_td_tverdict_publish_archive_rt.py b/tests/test_p2_lv_td_tverdict_publish_archive_rt.py new file mode 100644 index 0000000..c802832 --- /dev/null +++ b/tests/test_p2_lv_td_tverdict_publish_archive_rt.py @@ -0,0 +1,826 @@ +"""Contract tests for P2-79 Linux CI workflow Linux validation terminal dispatch terminal verdict publish archive gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_tverdict_publish_archive_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_terminal_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json", + "source_linux_validation_terminal_dispatch_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_final_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_final_publish_archive_decision": "archive_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_final_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_final_publish_archive_summary": ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_final_publish_archive_decision=archive_linux_validation_terminal_dispatch_validated" + ), + "linux_validation_terminal_dispatch_terminal_verdict_status": "ready_for_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_decision": "proceed_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed": True, + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_summary": ( + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_terminal_verdict_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_decision=proceed_linux_validation_terminal_dispatch" + ), + "linux_validation_terminal_dispatch_terminal_verdict_publish_status": "published", + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision": "announce_linux_validation_terminal_dispatch_ready", + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify": True, + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_publish_summary": ( + "linux_validation_terminal_dispatch_terminal_verdict_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_publish_status=published " + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision=announce_linux_validation_terminal_dispatch_ready" + ), + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_terminal_verdict_publish_ready"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_archived( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "archived" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 0 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "release" + ) + assert archive_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived" + ] + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_archived_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + 1 + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "follow_up" + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_follow_up.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "archived_with_follow_up" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 1 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_archived_with_follow_up" + in archive_payload["reason_codes"] + ) + assert archive_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_in_progress( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "in_progress" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "hold_linux_validation_terminal_dispatch_in_progress" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = ( + False + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = False + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] = ( + False + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "in_progress" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_in_progress" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + 0 + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "follow_up" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_in_progress" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_in_progress.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "in_progress" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_in_progress" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 0 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "follow_up" + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = "blocked" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "halt_linux_validation_terminal_dispatch_blocker" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = ( + False + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = True + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] = ( + True + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "blocker" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "blocked" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_blocker" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + 1 + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "blocker" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_blocked" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_blocked.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "blocked" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_blocker" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 1 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_blocked" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_failed( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = "failed" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "halt_linux_validation_terminal_dispatch_failure" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = ( + False + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = True + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] = ( + True + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "blocker" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "failed" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_failure" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + 1 + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "blocker" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_failed" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_failed.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "failed" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_failure" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 1 + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + is False + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + is True + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_failed" + in archive_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "published" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify" + ] = False + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] = True + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "follow_up" + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_mismatch.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "contract_failed" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_verdict_publish_archive" + ) + assert ( + archive_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner_mismatch" + in archive_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel_mismatch" + in archive_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_gate_module() + ) + publish_payload = ( + _sample_linux_validation_terminal_dispatch_terminal_verdict_publish_payload() + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] = ( + "published_with_follow_up" + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] = "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] = ( + 1 + ) + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_channel" + ] = "follow_up" + publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + publish_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_output.json" + ) + report_path.write_text(json.dumps(publish_payload, indent=2), encoding="utf-8") + + report = ( + gate.load_linux_validation_terminal_dispatch_terminal_verdict_publish_report( + report_path + ) + ) + archive_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_payload( + report, + source_path=report_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + archive_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md" + ), + ) + + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_status" + ] + == "archived_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_decision" + ] + == "archive_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_exit_code" + ] + == "1" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_archive" + ] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_should_page_owner" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_channel" + ] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_publish_status" + ] + == "published_with_follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_publish_channel" + ] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_run_id" + ] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_json" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.json" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive_report_markdown" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_archive.md" + ) diff --git a/tests/test_p2_lv_td_tverdict_publish_rt.py b/tests/test_p2_lv_td_tverdict_publish_rt.py new file mode 100644 index 0000000..9a78875 --- /dev/null +++ b/tests/test_p2_lv_td_tverdict_publish_rt.py @@ -0,0 +1,493 @@ +"""Contract tests for P2-78 Linux CI workflow Linux validation terminal dispatch terminal verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_td_tverdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() -> dict: + return { + "generated_at": "2026-05-03T00:00:00+00:00", + "source_linux_validation_terminal_dispatch_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_publish_archive.json", + "source_linux_validation_terminal_dispatch_final_verdict_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict_publish.json", + "source_linux_validation_terminal_dispatch_final_verdict_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_final_verdict.json", + "source_linux_validation_terminal_dispatch_terminal_publish_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_terminal_publish.json", + "source_linux_validation_terminal_dispatch_completion_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_completion.json", + "source_linux_validation_terminal_dispatch_trace_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_trace.json", + "source_linux_validation_terminal_dispatch_execution_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch_execution.json", + "source_linux_validation_terminal_dispatch_report": "/tmp/ci_workflow_linux_validation_terminal_dispatch.json", + "linux_validation_terminal_dispatch_final_publish_archive_status": "archived", + "linux_validation_terminal_dispatch_final_publish_archive_decision": "archive_linux_validation_terminal_dispatch_validated", + "linux_validation_terminal_dispatch_final_publish_archive_exit_code": 0, + "linux_validation_terminal_dispatch_final_publish_archive_should_archive": True, + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action": False, + "linux_validation_terminal_dispatch_final_publish_archive_should_page_owner": False, + "linux_validation_terminal_dispatch_final_publish_archive_channel": "release", + "linux_validation_terminal_dispatch_final_publish_archive_summary": ( + "linux_validation_terminal_dispatch_final_verdict_publish_status=published " + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_final_publish_archive_decision=archive_linux_validation_terminal_dispatch_validated" + ), + "linux_validation_terminal_dispatch_terminal_verdict_status": "ready_for_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_decision": "proceed_linux_validation_terminal_dispatch", + "linux_validation_terminal_dispatch_terminal_verdict_exit_code": 0, + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed": True, + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action": False, + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner": False, + "linux_validation_terminal_dispatch_terminal_verdict_channel": "release", + "linux_validation_terminal_dispatch_terminal_verdict_summary": ( + "linux_validation_terminal_dispatch_final_publish_archive_status=archived " + "linux_validation_terminal_dispatch_terminal_verdict_status=ready_for_linux_validation_terminal_dispatch " + "linux_validation_terminal_dispatch_terminal_verdict_decision=proceed_linux_validation_terminal_dispatch" + ), + "release_run_id": 9988776655, + "release_run_url": "https://github.com/acme/demo/actions/runs/9988776655", + "follow_up_queue_url": "", + "reason_codes": ["linux_validation_terminal_dispatch_terminal_verdict_ready"], + "structural_issues": [], + "missing_artifacts": [], + } + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload_published( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() + + report_path = ( + tmp_path / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + == "published" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_ready" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] + == 0 + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify" + ] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] + is False + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] + is False + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + == "release" + ) + assert publish_payload["reason_codes"] == [ + "linux_validation_terminal_dispatch_terminal_verdict_publish_ready" + ] + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = ( + "archived_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_validated_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_follow_up.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] + == 1 + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + == "follow_up" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_ready_with_follow_up" + in publish_payload["reason_codes"] + ) + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload_blocked( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_status"] = "blocked" + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_decision"] = ( + "archive_linux_validation_terminal_dispatch_blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_should_archive"] = ( + False + ) + verdict_payload[ + "linux_validation_terminal_dispatch_final_publish_archive_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_final_publish_archive_channel"] = ( + "blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = "blocked" + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "halt_linux_validation_terminal_dispatch_blocker" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = False + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = "blocker" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_blocked" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_blocked.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + == "blocked" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] + == "announce_linux_validation_terminal_dispatch_blocker" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] + == 1 + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] + is True + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] + is True + ) + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + == "blocker" + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_publish_blocked" + in publish_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload_contract_failed_on_mismatch( + tmp_path: Path, +): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "ready_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_should_proceed"] = False + verdict_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action" + ] = True + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_should_page_owner"] = ( + True + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_mismatch.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + + assert ( + publish_payload["linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + == "contract_failed" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_decision" + ] + == "abort_linux_validation_terminal_dispatch_terminal_verdict_publish" + ) + assert ( + publish_payload[ + "linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code" + ] + == 1 + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_decision_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_should_proceed_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_requires_manual_action_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_should_page_owner_mismatch" + in publish_payload["structural_issues"] + ) + assert ( + "linux_validation_terminal_dispatch_terminal_verdict_channel_mismatch" + in publish_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = ( + _load_ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_gate_module() + ) + verdict_payload = _sample_linux_validation_terminal_dispatch_terminal_verdict_payload() + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_decision"] = ( + "proceed_linux_validation_terminal_dispatch_with_follow_up" + ) + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_exit_code"] = 1 + verdict_payload["linux_validation_terminal_dispatch_terminal_verdict_channel"] = ( + "follow_up" + ) + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = [ + "linux_validation_terminal_dispatch_terminal_verdict_ready_with_follow_up" + ] + + report_path = ( + tmp_path + / "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_output.json" + ) + report_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + report = gate.load_linux_validation_terminal_dispatch_terminal_verdict_report( + report_path + ) + publish_payload = ( + gate.build_linux_validation_terminal_dispatch_terminal_verdict_publish_payload( + report, + source_path=report_path.resolve(), + ) + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_decision"] + == "announce_linux_validation_terminal_dispatch_ready_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_exit_code"] + == "1" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_should_notify"] + == "true" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_requires_manual_action" + ] + == "false" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_should_page_owner" + ] + == "false" + ) + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_channel"] + == "follow_up" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_terminal_verdict_status" + ] + == "ready_with_follow_up_for_linux_validation_terminal_dispatch" + ) + assert ( + outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_terminal_verdict_channel" + ] + == "follow_up" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_follow_up_queue_url" + ].endswith("/issues/77") + assert ( + outputs["workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_run_id"] + == "9988776655" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_run_url" + ].endswith("/actions/runs/9988776655") + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_report_json" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.json" + ) + assert outputs[ + "workflow_linux_validation_terminal_dispatch_terminal_verdict_publish_report_markdown" + ].endswith( + "ci_workflow_linux_validation_terminal_dispatch_terminal_verdict_publish.md" + ) + diff --git a/tests/test_p2_lv_terminal_publish_rt.py b/tests/test_p2_lv_terminal_publish_rt.py new file mode 100644 index 0000000..a09f813 --- /dev/null +++ b/tests/test_p2_lv_terminal_publish_rt.py @@ -0,0 +1,428 @@ +"""Contract tests for P2-63 Linux CI workflow Linux validation terminal publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_publish_gate_module(): + script_path = ( + Path("scripts") / "run_p2_lv_terminal_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_verdict_publish_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_linux_validation_verdict_report": "/tmp/ci_workflow_linux_validation_verdict.json", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_verdict_status": "validated", + "linux_validation_verdict_decision": "accept_linux_validation", + "linux_validation_verdict_exit_code": 0, + "linux_validation_passed": True, + "linux_validation_verdict_requires_manual_action": False, + "linux_validation_verdict_channel": "release", + "linux_validation_verdict_attempted": False, + "linux_validation_verdict_timeout_seconds": 900, + "linux_validation_verdict_command": "", + "linux_validation_verdict_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_verdict_summary": ( + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation " + "linux_validation_verdict_status=validated " + "linux_validation_verdict_decision=accept_linux_validation" + ), + "linux_validation_verdict_publish_status": "published", + "linux_validation_verdict_publish_decision": "announce_linux_validation_passed", + "linux_validation_verdict_publish_exit_code": 0, + "linux_validation_verdict_publish_should_notify": True, + "linux_validation_verdict_publish_requires_manual_action": False, + "linux_validation_verdict_publish_channel": "release", + "linux_validation_verdict_publish_summary": ( + "linux_validation_verdict_status=validated " + "linux_validation_verdict_publish_status=published " + "linux_validation_verdict_publish_decision=announce_linux_validation_passed" + ), + "reason_codes": ["linux_validation_verdict_publish_published"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "verdict", + "path": "/tmp/ci_workflow_linux_validation_verdict.json", + "exists": True, + }, + { + "source": "publish", + "path": "/tmp/ci_workflow_linux_validation_verdict_publish.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_terminal_publish_payload_terminal_published(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_publish_gate_module() + verdict_publish_payload = _sample_linux_validation_verdict_publish_payload() + + verdict_publish_path = tmp_path / "ci_workflow_linux_validation_verdict_publish.json" + verdict_publish_path.write_text( + json.dumps(verdict_publish_payload, indent=2), encoding="utf-8" + ) + + verdict_publish_report = gate.load_linux_validation_verdict_publish_report( + verdict_publish_path + ) + terminal_publish_payload = gate.build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_path.resolve(), + ) + + assert ( + terminal_publish_payload["linux_validation_terminal_publish_status"] + == "terminal_published" + ) + assert ( + terminal_publish_payload["linux_validation_terminal_publish_decision"] + == "announce_linux_validation_terminal_passed" + ) + assert terminal_publish_payload["linux_validation_terminal_publish_exit_code"] == 0 + assert terminal_publish_payload["linux_validation_terminal_publish_should_notify"] is True + assert terminal_publish_payload["linux_validation_terminal_publish_ready_for_handoff"] is True + assert ( + terminal_publish_payload["linux_validation_terminal_publish_requires_manual_action"] + is False + ) + assert terminal_publish_payload["linux_validation_terminal_publish_channel"] == "release" + assert ( + "linux_validation_terminal_publish_terminal_published" + in terminal_publish_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_publish_payload_terminal_published_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_publish_gate_module() + verdict_publish_payload = _sample_linux_validation_verdict_publish_payload() + verdict_publish_payload["terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation" + ) + verdict_publish_payload["terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + verdict_publish_payload["terminal_verdict_exit_code"] = 1 + verdict_publish_payload["terminal_verdict_channel"] = "follow_up" + verdict_publish_payload["linux_validation_channel"] = "follow_up" + verdict_publish_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + verdict_publish_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + verdict_publish_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + verdict_publish_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + verdict_publish_payload["linux_validation_verdict_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_channel"] = "follow_up" + verdict_publish_payload["linux_validation_verdict_publish_status"] = "published_with_follow_up" + verdict_publish_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_passed_with_follow_up" + ) + verdict_publish_payload["linux_validation_verdict_publish_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_publish_channel"] = "follow_up" + verdict_publish_payload["linux_validation_verdict_publish_summary"] = ( + "linux_validation_verdict_status=validated_with_follow_up " + "linux_validation_verdict_publish_status=published_with_follow_up " + "linux_validation_verdict_publish_decision=announce_linux_validation_passed_with_follow_up" + ) + verdict_publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_publish_payload["reason_codes"] = ["linux_validation_verdict_publish_with_follow_up"] + + verdict_publish_path = tmp_path / "ci_workflow_linux_validation_verdict_publish_follow_up.json" + verdict_publish_path.write_text( + json.dumps(verdict_publish_payload, indent=2), encoding="utf-8" + ) + + verdict_publish_report = gate.load_linux_validation_verdict_publish_report( + verdict_publish_path + ) + terminal_publish_payload = gate.build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_path.resolve(), + ) + + assert ( + terminal_publish_payload["linux_validation_terminal_publish_status"] + == "terminal_published_with_follow_up" + ) + assert ( + terminal_publish_payload["linux_validation_terminal_publish_decision"] + == "announce_linux_validation_terminal_passed_with_follow_up" + ) + assert terminal_publish_payload["linux_validation_terminal_publish_exit_code"] == 1 + assert terminal_publish_payload["linux_validation_terminal_publish_should_notify"] is True + assert terminal_publish_payload["linux_validation_terminal_publish_ready_for_handoff"] is True + assert ( + terminal_publish_payload["linux_validation_terminal_publish_requires_manual_action"] + is False + ) + assert terminal_publish_payload["linux_validation_terminal_publish_channel"] == "follow_up" + assert ( + "linux_validation_terminal_publish_terminal_published_with_follow_up" + in terminal_publish_payload["reason_codes"] + ) + assert terminal_publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_publish_payload_terminal_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_publish_gate_module() + verdict_publish_payload = _sample_linux_validation_verdict_publish_payload() + verdict_publish_payload["terminal_verdict_status"] = "blocked" + verdict_publish_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + verdict_publish_payload["terminal_verdict_exit_code"] = 1 + verdict_publish_payload["terminal_verdict_should_proceed"] = False + verdict_publish_payload["terminal_verdict_requires_manual_action"] = True + verdict_publish_payload["terminal_verdict_channel"] = "blocker" + verdict_publish_payload["linux_validation_should_dispatch"] = False + verdict_publish_payload["linux_validation_requires_manual_action"] = True + verdict_publish_payload["linux_validation_channel"] = "blocker" + verdict_publish_payload["linux_validation_dispatch_status"] = "blocked" + verdict_publish_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + verdict_publish_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_status"] = "blocked" + verdict_publish_payload["linux_validation_verdict_decision"] = ( + "hold_linux_validation_blocker" + ) + verdict_publish_payload["linux_validation_verdict_exit_code"] = 1 + verdict_publish_payload["linux_validation_passed"] = False + verdict_publish_payload["linux_validation_verdict_requires_manual_action"] = True + verdict_publish_payload["linux_validation_verdict_channel"] = "blocker" + verdict_publish_payload["linux_validation_verdict_publish_status"] = "blocked" + verdict_publish_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_blocker" + ) + verdict_publish_payload["linux_validation_verdict_publish_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_publish_requires_manual_action"] = True + verdict_publish_payload["linux_validation_verdict_publish_channel"] = "blocker" + verdict_publish_payload["reason_codes"] = ["linux_validation_verdict_publish_blocked"] + + verdict_publish_path = tmp_path / "ci_workflow_linux_validation_verdict_publish_blocked.json" + verdict_publish_path.write_text( + json.dumps(verdict_publish_payload, indent=2), encoding="utf-8" + ) + + verdict_publish_report = gate.load_linux_validation_verdict_publish_report( + verdict_publish_path + ) + terminal_publish_payload = gate.build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_path.resolve(), + ) + + assert ( + terminal_publish_payload["linux_validation_terminal_publish_status"] + == "terminal_blocked" + ) + assert ( + terminal_publish_payload["linux_validation_terminal_publish_decision"] + == "announce_linux_validation_terminal_blocker" + ) + assert terminal_publish_payload["linux_validation_terminal_publish_exit_code"] == 1 + assert terminal_publish_payload["linux_validation_terminal_publish_should_notify"] is True + assert terminal_publish_payload["linux_validation_terminal_publish_ready_for_handoff"] is False + assert ( + terminal_publish_payload["linux_validation_terminal_publish_requires_manual_action"] + is True + ) + assert terminal_publish_payload["linux_validation_terminal_publish_channel"] == "blocker" + assert ( + "linux_validation_terminal_publish_terminal_blocked" + in terminal_publish_payload["reason_codes"] + ) + + +def test_build_linux_validation_terminal_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_publish_gate_module() + verdict_publish_payload = _sample_linux_validation_verdict_publish_payload() + verdict_publish_payload["linux_validation_verdict_publish_status"] = "published" + verdict_publish_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_passed_with_follow_up" + ) + verdict_publish_payload["linux_validation_verdict_publish_requires_manual_action"] = True + verdict_publish_payload["linux_validation_verdict_publish_channel"] = "follow_up" + + verdict_publish_path = tmp_path / "ci_workflow_linux_validation_verdict_publish_mismatch.json" + verdict_publish_path.write_text( + json.dumps(verdict_publish_payload, indent=2), encoding="utf-8" + ) + + verdict_publish_report = gate.load_linux_validation_verdict_publish_report( + verdict_publish_path + ) + terminal_publish_payload = gate.build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_path.resolve(), + ) + + assert ( + terminal_publish_payload["linux_validation_terminal_publish_status"] + == "terminal_contract_failed" + ) + assert ( + terminal_publish_payload["linux_validation_terminal_publish_decision"] + == "abort_terminal_publish" + ) + assert terminal_publish_payload["linux_validation_terminal_publish_exit_code"] == 1 + assert ( + "linux_validation_verdict_publish_decision_mismatch" + in terminal_publish_payload["structural_issues"] + ) + assert ( + "linux_validation_verdict_publish_requires_manual_action_mismatch" + in terminal_publish_payload["structural_issues"] + ) + assert ( + "linux_validation_verdict_publish_channel_mismatch" + in terminal_publish_payload["structural_issues"] + ) + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_publish_gate_module() + verdict_publish_payload = _sample_linux_validation_verdict_publish_payload() + verdict_publish_payload["terminal_verdict_status"] = ( + "ready_with_follow_up_for_linux_validation" + ) + verdict_publish_payload["terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + verdict_publish_payload["terminal_verdict_exit_code"] = 1 + verdict_publish_payload["terminal_verdict_channel"] = "follow_up" + verdict_publish_payload["linux_validation_channel"] = "follow_up" + verdict_publish_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + verdict_publish_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + verdict_publish_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + verdict_publish_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + verdict_publish_payload["linux_validation_verdict_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_channel"] = "follow_up" + verdict_publish_payload["linux_validation_verdict_publish_status"] = "published_with_follow_up" + verdict_publish_payload["linux_validation_verdict_publish_decision"] = ( + "announce_linux_validation_passed_with_follow_up" + ) + verdict_publish_payload["linux_validation_verdict_publish_exit_code"] = 1 + verdict_publish_payload["linux_validation_verdict_publish_channel"] = "follow_up" + verdict_publish_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_publish_payload["reason_codes"] = ["linux_validation_verdict_publish_with_follow_up"] + + verdict_publish_path = tmp_path / "ci_workflow_linux_validation_verdict_publish_output.json" + verdict_publish_path.write_text( + json.dumps(verdict_publish_payload, indent=2), encoding="utf-8" + ) + + verdict_publish_report = gate.load_linux_validation_verdict_publish_report( + verdict_publish_path + ) + terminal_publish_payload = gate.build_linux_validation_terminal_publish_payload( + verdict_publish_report, + source_path=verdict_publish_path.resolve(), + ) + outputs = gate.build_github_output_values( + terminal_publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_publish.md" + ), + ) + + assert ( + outputs["workflow_linux_validation_terminal_publish_status"] + == "terminal_published_with_follow_up" + ) + assert ( + outputs["workflow_linux_validation_terminal_publish_decision"] + == "announce_linux_validation_terminal_passed_with_follow_up" + ) + assert outputs["workflow_linux_validation_terminal_publish_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_publish_should_notify"] == "true" + assert outputs["workflow_linux_validation_terminal_publish_ready_for_handoff"] == "true" + assert ( + outputs["workflow_linux_validation_terminal_publish_requires_manual_action"] == "false" + ) + assert outputs["workflow_linux_validation_terminal_publish_channel"] == "follow_up" + assert outputs["workflow_linux_validation_terminal_publish_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_terminal_publish_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_terminal_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_terminal_publish_report_json"].endswith( + "ci_workflow_linux_validation_terminal_publish.json" + ) + assert outputs["workflow_linux_validation_terminal_publish_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_publish.md" + ) diff --git a/tests/test_p2_lv_terminal_verdict_publish_rt.py b/tests/test_p2_lv_terminal_verdict_publish_rt.py new file mode 100644 index 0000000..49b00f2 --- /dev/null +++ b/tests/test_p2_lv_terminal_verdict_publish_rt.py @@ -0,0 +1,328 @@ +"""Contract tests for P2-68 Linux CI workflow Linux validation terminal verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_terminal_verdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_terminal_verdict_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "source_linux_validation_final_publish_archive_report": "/tmp/ci_workflow_linux_validation_final_publish_archive.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_terminal_verdict_status": "ready_for_linux_validation", + "linux_validation_terminal_verdict_decision": "proceed_linux_validation", + "linux_validation_terminal_verdict_exit_code": 0, + "linux_validation_terminal_verdict_should_proceed": True, + "linux_validation_terminal_verdict_requires_manual_action": False, + "linux_validation_terminal_verdict_channel": "release", + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_terminal_verdict_summary": ( + "linux_validation_final_publish_archive_status=archived " + "linux_validation_terminal_verdict_status=ready_for_linux_validation " + "linux_validation_terminal_verdict_decision=proceed_linux_validation" + ), + "reason_codes": ["linux_validation_terminal_verdict_ready_for_linux_validation"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "terminal_verdict", + "path": "/tmp/ci_workflow_linux_validation_terminal_verdict.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_terminal_verdict_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module() + terminal_verdict_payload = _sample_linux_validation_terminal_verdict_payload() + + terminal_verdict_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict.json" + terminal_verdict_path.write_text(json.dumps(terminal_verdict_payload, indent=2), encoding="utf-8") + + terminal_verdict_report = gate.load_linux_validation_terminal_verdict_report(terminal_verdict_path) + publish_payload = gate.build_linux_validation_terminal_verdict_publish_payload( + terminal_verdict_report, + source_path=terminal_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_terminal_verdict_publish_status"] == "published" + assert ( + publish_payload["linux_validation_terminal_verdict_publish_decision"] + == "announce_linux_validation_terminal_ready" + ) + assert publish_payload["linux_validation_terminal_verdict_publish_exit_code"] == 0 + assert publish_payload["linux_validation_terminal_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_terminal_verdict_publish_channel"] == "release" + assert "linux_validation_terminal_verdict_publish_published" in publish_payload["reason_codes"] + + +def test_build_linux_validation_terminal_verdict_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module() + terminal_verdict_payload = _sample_linux_validation_terminal_verdict_payload() + terminal_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_verdict_payload["terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["terminal_verdict_channel"] = "follow_up" + terminal_verdict_payload["linux_validation_channel"] = "follow_up" + terminal_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_verdict_payload["linux_validation_terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + terminal_verdict_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_requires_manual_action"] = False + terminal_verdict_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + terminal_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_verdict_payload["reason_codes"] = [ + "linux_validation_terminal_verdict_ready_with_follow_up_for_linux_validation" + ] + + terminal_verdict_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_follow_up.json" + terminal_verdict_path.write_text(json.dumps(terminal_verdict_payload, indent=2), encoding="utf-8") + + terminal_verdict_report = gate.load_linux_validation_terminal_verdict_report(terminal_verdict_path) + publish_payload = gate.build_linux_validation_terminal_verdict_publish_payload( + terminal_verdict_report, + source_path=terminal_verdict_path.resolve(), + ) + + assert ( + publish_payload["linux_validation_terminal_verdict_publish_status"] + == "published_with_follow_up" + ) + assert ( + publish_payload["linux_validation_terminal_verdict_publish_decision"] + == "announce_linux_validation_terminal_ready_with_follow_up" + ) + assert publish_payload["linux_validation_terminal_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_terminal_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] + is False + ) + assert publish_payload["linux_validation_terminal_verdict_publish_channel"] == "follow_up" + assert "linux_validation_terminal_verdict_publish_with_follow_up" in publish_payload["reason_codes"] + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_verdict_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module() + terminal_verdict_payload = _sample_linux_validation_terminal_verdict_payload() + terminal_verdict_payload["terminal_verdict_status"] = "blocked" + terminal_verdict_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + terminal_verdict_payload["terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["terminal_verdict_should_proceed"] = False + terminal_verdict_payload["terminal_verdict_requires_manual_action"] = True + terminal_verdict_payload["terminal_verdict_channel"] = "blocker" + terminal_verdict_payload["linux_validation_should_dispatch"] = False + terminal_verdict_payload["linux_validation_requires_manual_action"] = True + terminal_verdict_payload["linux_validation_channel"] = "blocker" + terminal_verdict_payload["linux_validation_dispatch_status"] = "blocked" + terminal_verdict_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + terminal_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_status"] = "blocked" + terminal_verdict_payload["linux_validation_terminal_verdict_decision"] = ( + "halt_linux_validation_blocker" + ) + terminal_verdict_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_should_proceed"] = False + terminal_verdict_payload["linux_validation_terminal_verdict_requires_manual_action"] = True + terminal_verdict_payload["linux_validation_terminal_verdict_channel"] = "blocker" + terminal_verdict_payload["reason_codes"] = ["linux_validation_terminal_verdict_blocked"] + + terminal_verdict_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_blocked.json" + terminal_verdict_path.write_text(json.dumps(terminal_verdict_payload, indent=2), encoding="utf-8") + + terminal_verdict_report = gate.load_linux_validation_terminal_verdict_report(terminal_verdict_path) + publish_payload = gate.build_linux_validation_terminal_verdict_publish_payload( + terminal_verdict_report, + source_path=terminal_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_terminal_verdict_publish_status"] == "blocked" + assert ( + publish_payload["linux_validation_terminal_verdict_publish_decision"] + == "announce_linux_validation_terminal_blocker" + ) + assert publish_payload["linux_validation_terminal_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_terminal_verdict_publish_should_notify"] is True + assert ( + publish_payload["linux_validation_terminal_verdict_publish_requires_manual_action"] + is True + ) + assert publish_payload["linux_validation_terminal_verdict_publish_channel"] == "blocker" + assert "linux_validation_terminal_verdict_publish_blocked" in publish_payload["reason_codes"] + + +def test_build_linux_validation_terminal_verdict_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module() + terminal_verdict_payload = _sample_linux_validation_terminal_verdict_payload() + terminal_verdict_payload["linux_validation_terminal_verdict_status"] = "ready_for_linux_validation" + terminal_verdict_payload["linux_validation_terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + terminal_verdict_payload["linux_validation_terminal_verdict_requires_manual_action"] = False + terminal_verdict_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + + terminal_verdict_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_mismatch.json" + terminal_verdict_path.write_text(json.dumps(terminal_verdict_payload, indent=2), encoding="utf-8") + + terminal_verdict_report = gate.load_linux_validation_terminal_verdict_report(terminal_verdict_path) + publish_payload = gate.build_linux_validation_terminal_verdict_publish_payload( + terminal_verdict_report, + source_path=terminal_verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_terminal_verdict_publish_status"] == "contract_failed" + assert publish_payload["linux_validation_terminal_verdict_publish_decision"] == "abort_publish" + assert publish_payload["linux_validation_terminal_verdict_publish_exit_code"] == 1 + assert "linux_validation_terminal_verdict_decision_mismatch" in publish_payload["structural_issues"] + assert ( + "linux_validation_terminal_verdict_requires_manual_action_mismatch" + in publish_payload["structural_issues"] + ) + assert "linux_validation_terminal_verdict_channel_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_verdict_publish_gate_module() + terminal_verdict_payload = _sample_linux_validation_terminal_verdict_payload() + terminal_verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + terminal_verdict_payload["terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["terminal_verdict_channel"] = "follow_up" + terminal_verdict_payload["linux_validation_channel"] = "follow_up" + terminal_verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + terminal_verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + terminal_verdict_payload["linux_validation_dispatch_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + terminal_verdict_payload["linux_validation_terminal_verdict_decision"] = ( + "proceed_linux_validation_with_follow_up" + ) + terminal_verdict_payload["linux_validation_terminal_verdict_exit_code"] = 1 + terminal_verdict_payload["linux_validation_terminal_verdict_requires_manual_action"] = False + terminal_verdict_payload["linux_validation_terminal_verdict_channel"] = "follow_up" + terminal_verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + terminal_verdict_payload["reason_codes"] = [ + "linux_validation_terminal_verdict_ready_with_follow_up_for_linux_validation" + ] + + terminal_verdict_path = tmp_path / "ci_workflow_linux_validation_terminal_verdict_output.json" + terminal_verdict_path.write_text(json.dumps(terminal_verdict_payload, indent=2), encoding="utf-8") + + terminal_verdict_report = gate.load_linux_validation_terminal_verdict_report(terminal_verdict_path) + publish_payload = gate.build_linux_validation_terminal_verdict_publish_payload( + terminal_verdict_report, + source_path=terminal_verdict_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict_publish.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_verdict_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_linux_validation_terminal_verdict_publish_decision"] + == "announce_linux_validation_terminal_ready_with_follow_up" + ) + assert outputs["workflow_linux_validation_terminal_verdict_publish_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_verdict_publish_should_notify"] == "true" + assert outputs["workflow_linux_validation_terminal_verdict_publish_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_terminal_verdict_publish_channel"] == "follow_up" + assert outputs["workflow_linux_validation_terminal_verdict_publish_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_terminal_verdict_publish_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_terminal_verdict_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_terminal_verdict_publish_report_json"].endswith( + "ci_workflow_linux_validation_terminal_verdict_publish.json" + ) + assert outputs["workflow_linux_validation_terminal_verdict_publish_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_verdict_publish.md" + ) + + diff --git a/tests/test_p2_lv_terminal_verdict_rt.py b/tests/test_p2_lv_terminal_verdict_rt.py new file mode 100644 index 0000000..c83e4de --- /dev/null +++ b/tests/test_p2_lv_terminal_verdict_rt.py @@ -0,0 +1,305 @@ +"""Contract tests for P2-67 Linux CI workflow Linux validation terminal verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_terminal_verdict_gate_module(): + script_path = ( + Path("scripts") + / "run_p2_lv_terminal_verdict_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_terminal_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_final_publish_archive_payload() -> dict: + return { + "generated_at": "2026-05-02T00:00:00+00:00", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_final_publish_archive_status": "archived", + "linux_validation_final_publish_archive_decision": "archive_release_shipped", + "linux_validation_final_publish_archive_exit_code": 0, + "linux_validation_final_publish_archive_should_archive": True, + "linux_validation_final_publish_archive_requires_manual_action": False, + "linux_validation_final_publish_archive_channel": "release", + "linux_validation_final_publish_archive_attempted": False, + "linux_validation_final_publish_archive_timeout_seconds": 900, + "linux_validation_final_publish_archive_command": "", + "linux_validation_final_publish_archive_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_final_publish_archive_summary": ( + "linux_validation_final_verdict_publish_status=published " + "linux_validation_final_publish_archive_status=archived " + "linux_validation_final_publish_archive_decision=archive_release_shipped" + ), + "reason_codes": ["linux_validation_final_publish_archive_archived"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "final_publish_archive", + "path": "/tmp/ci_workflow_linux_validation_final_publish_archive.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_terminal_verdict_payload_ready_for_linux_validation( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_final_publish_archive_payload() + + archive_path = tmp_path / "ci_workflow_linux_validation_final_publish_archive.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_linux_validation_final_publish_archive_report(archive_path) + payload = gate.build_linux_validation_terminal_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert payload["linux_validation_terminal_verdict_status"] == "ready_for_linux_validation" + assert payload["linux_validation_terminal_verdict_decision"] == "proceed_linux_validation" + assert payload["linux_validation_terminal_verdict_exit_code"] == 0 + assert payload["linux_validation_terminal_verdict_should_proceed"] is True + assert payload["linux_validation_terminal_verdict_requires_manual_action"] is False + assert payload["linux_validation_terminal_verdict_channel"] == "release" + assert "linux_validation_terminal_verdict_ready_for_linux_validation" in payload["reason_codes"] + + +def test_build_linux_validation_terminal_verdict_payload_ready_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_final_publish_archive_payload() + archive_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + archive_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + archive_payload["terminal_verdict_exit_code"] = 1 + archive_payload["terminal_verdict_channel"] = "follow_up" + archive_payload["linux_validation_channel"] = "follow_up" + archive_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + archive_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + archive_payload["linux_validation_dispatch_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_status"] = "archived_with_follow_up" + archive_payload["linux_validation_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + archive_payload["linux_validation_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_requires_manual_action"] = False + archive_payload["linux_validation_final_publish_archive_channel"] = "follow_up" + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["reason_codes"] = [ + "linux_validation_final_publish_archive_archived_with_follow_up" + ] + + archive_path = tmp_path / "ci_workflow_linux_validation_final_publish_archive_follow_up.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_linux_validation_final_publish_archive_report(archive_path) + payload = gate.build_linux_validation_terminal_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert payload["linux_validation_terminal_verdict_status"] == "ready_with_follow_up_for_linux_validation" + assert payload["linux_validation_terminal_verdict_decision"] == "proceed_linux_validation_with_follow_up" + assert payload["linux_validation_terminal_verdict_exit_code"] == 1 + assert payload["linux_validation_terminal_verdict_should_proceed"] is True + assert payload["linux_validation_terminal_verdict_requires_manual_action"] is False + assert payload["linux_validation_terminal_verdict_channel"] == "follow_up" + assert ( + "linux_validation_terminal_verdict_ready_with_follow_up_for_linux_validation" + in payload["reason_codes"] + ) + assert payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_terminal_verdict_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_final_publish_archive_payload() + archive_payload["terminal_verdict_status"] = "blocked" + archive_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + archive_payload["terminal_verdict_exit_code"] = 1 + archive_payload["terminal_verdict_should_proceed"] = False + archive_payload["terminal_verdict_requires_manual_action"] = True + archive_payload["terminal_verdict_channel"] = "blocker" + archive_payload["linux_validation_should_dispatch"] = False + archive_payload["linux_validation_requires_manual_action"] = True + archive_payload["linux_validation_channel"] = "blocker" + archive_payload["linux_validation_dispatch_status"] = "blocked" + archive_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + archive_payload["linux_validation_dispatch_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_status"] = "blocked" + archive_payload["linux_validation_final_publish_archive_decision"] = "archive_release_blocker" + archive_payload["linux_validation_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_should_archive"] = False + archive_payload["linux_validation_final_publish_archive_requires_manual_action"] = True + archive_payload["linux_validation_final_publish_archive_channel"] = "blocker" + archive_payload["reason_codes"] = ["linux_validation_final_publish_archive_blocked"] + + archive_path = tmp_path / "ci_workflow_linux_validation_final_publish_archive_blocked.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_linux_validation_final_publish_archive_report(archive_path) + payload = gate.build_linux_validation_terminal_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert payload["linux_validation_terminal_verdict_status"] == "blocked" + assert payload["linux_validation_terminal_verdict_decision"] == "halt_linux_validation_blocker" + assert payload["linux_validation_terminal_verdict_exit_code"] == 1 + assert payload["linux_validation_terminal_verdict_should_proceed"] is False + assert payload["linux_validation_terminal_verdict_requires_manual_action"] is True + assert payload["linux_validation_terminal_verdict_channel"] == "blocker" + assert "linux_validation_terminal_verdict_blocked" in payload["reason_codes"] + + +def test_build_linux_validation_terminal_verdict_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_final_publish_archive_payload() + archive_payload["linux_validation_final_publish_archive_status"] = "archived" + archive_payload["linux_validation_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + archive_payload["linux_validation_final_publish_archive_requires_manual_action"] = False + archive_payload["linux_validation_final_publish_archive_channel"] = "follow_up" + + archive_path = tmp_path / "ci_workflow_linux_validation_final_publish_archive_mismatch.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_linux_validation_final_publish_archive_report(archive_path) + payload = gate.build_linux_validation_terminal_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + + assert payload["linux_validation_terminal_verdict_status"] == "contract_failed" + assert payload["linux_validation_terminal_verdict_decision"] == "abort_linux_validation" + assert payload["linux_validation_terminal_verdict_exit_code"] == 1 + assert "linux_validation_final_publish_archive_decision_mismatch" in payload["structural_issues"] + assert "linux_validation_final_publish_archive_requires_manual_action_mismatch" in payload["structural_issues"] + assert "linux_validation_final_publish_archive_channel_mismatch" in payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_terminal_verdict_gate_module() + archive_payload = _sample_linux_validation_final_publish_archive_payload() + archive_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + archive_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + archive_payload["terminal_verdict_exit_code"] = 1 + archive_payload["terminal_verdict_channel"] = "follow_up" + archive_payload["linux_validation_channel"] = "follow_up" + archive_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + archive_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + archive_payload["linux_validation_dispatch_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_status"] = "archived_with_follow_up" + archive_payload["linux_validation_final_publish_archive_decision"] = ( + "archive_release_shipped_with_follow_up" + ) + archive_payload["linux_validation_final_publish_archive_exit_code"] = 1 + archive_payload["linux_validation_final_publish_archive_requires_manual_action"] = False + archive_payload["linux_validation_final_publish_archive_channel"] = "follow_up" + archive_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + archive_payload["reason_codes"] = [ + "linux_validation_final_publish_archive_archived_with_follow_up" + ] + + archive_path = tmp_path / "ci_workflow_linux_validation_final_publish_archive_output.json" + archive_path.write_text(json.dumps(archive_payload, indent=2), encoding="utf-8") + + archive_report = gate.load_linux_validation_final_publish_archive_report(archive_path) + payload = gate.build_linux_validation_terminal_verdict_payload( + archive_report, + source_path=archive_path.resolve(), + ) + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_terminal_verdict.md" + ), + ) + + assert outputs["workflow_linux_validation_terminal_verdict_status"] == "ready_with_follow_up_for_linux_validation" + assert outputs["workflow_linux_validation_terminal_verdict_decision"] == "proceed_linux_validation_with_follow_up" + assert outputs["workflow_linux_validation_terminal_verdict_exit_code"] == "1" + assert outputs["workflow_linux_validation_terminal_verdict_should_proceed"] == "true" + assert outputs["workflow_linux_validation_terminal_verdict_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_terminal_verdict_channel"] == "follow_up" + assert outputs["workflow_linux_validation_terminal_verdict_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_terminal_verdict_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_terminal_verdict_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_terminal_verdict_report_json"].endswith( + "ci_workflow_linux_validation_terminal_verdict.json" + ) + assert outputs["workflow_linux_validation_terminal_verdict_report_markdown"].endswith( + "ci_workflow_linux_validation_terminal_verdict.md" + ) diff --git a/tests/test_p2_lv_verdict_publish_rt.py b/tests/test_p2_lv_verdict_publish_rt.py new file mode 100644 index 0000000..7ec2980 --- /dev/null +++ b/tests/test_p2_lv_verdict_publish_rt.py @@ -0,0 +1,309 @@ +"""Contract tests for P2-62 Linux CI workflow Linux validation verdict publish gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_verdict_publish_gate_module(): + script_path = ( + Path("scripts") / "run_p2_lv_verdict_publish_gate.py" + ) + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_verdict_publish_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_verdict_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_linux_validation_dispatch_report": "/tmp/ci_workflow_linux_validation_dispatch.json", + "project_root": "/tmp", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_dispatch_command_returncode": None, + "linux_validation_verdict_status": "validated", + "linux_validation_verdict_decision": "accept_linux_validation", + "linux_validation_verdict_exit_code": 0, + "linux_validation_passed": True, + "linux_validation_verdict_requires_manual_action": False, + "linux_validation_verdict_channel": "release", + "linux_validation_verdict_attempted": False, + "linux_validation_verdict_timeout_seconds": 900, + "linux_validation_verdict_command": "", + "linux_validation_verdict_command_parts": [], + "dry_run": True, + "verdict_command_returncode": None, + "verdict_command_stdout_tail": "", + "verdict_command_stderr_tail": "", + "verdict_error_type": "", + "verdict_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "linux_validation_verdict_summary": ( + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation " + "linux_validation_verdict_status=validated " + "linux_validation_verdict_decision=accept_linux_validation" + ), + "reason_codes": ["linux_validation_verdict_validated"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "dispatch", + "path": "/tmp/ci_workflow_linux_validation_dispatch.json", + "exists": True, + }, + { + "source": "terminal_verdict", + "path": "/tmp/ci_workflow_terminal_verdict.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_verdict_publish_payload_published(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_verdict_publish_gate_module() + verdict_payload = _sample_linux_validation_verdict_payload() + + verdict_path = tmp_path / "ci_workflow_linux_validation_verdict.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_linux_validation_verdict_report(verdict_path) + publish_payload = gate.build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_verdict_publish_status"] == "published" + assert ( + publish_payload["linux_validation_verdict_publish_decision"] + == "announce_linux_validation_passed" + ) + assert publish_payload["linux_validation_verdict_publish_exit_code"] == 0 + assert publish_payload["linux_validation_verdict_publish_should_notify"] is True + assert publish_payload["linux_validation_verdict_publish_requires_manual_action"] is False + assert publish_payload["linux_validation_verdict_publish_channel"] == "release" + assert "linux_validation_verdict_publish_published" in publish_payload["reason_codes"] + + +def test_build_linux_validation_verdict_publish_payload_published_with_follow_up( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_publish_gate_module() + verdict_payload = _sample_linux_validation_verdict_payload() + verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + verdict_payload["terminal_verdict_exit_code"] = 1 + verdict_payload["terminal_verdict_channel"] = "follow_up" + verdict_payload["linux_validation_channel"] = "follow_up" + verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + verdict_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + verdict_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + verdict_payload["linux_validation_verdict_exit_code"] = 1 + verdict_payload["linux_validation_verdict_channel"] = "follow_up" + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = ["linux_validation_verdict_validated_with_follow_up"] + + verdict_path = tmp_path / "ci_workflow_linux_validation_verdict_follow_up.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_linux_validation_verdict_report(verdict_path) + publish_payload = gate.build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_verdict_publish_status"] == "published_with_follow_up" + assert ( + publish_payload["linux_validation_verdict_publish_decision"] + == "announce_linux_validation_passed_with_follow_up" + ) + assert publish_payload["linux_validation_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_verdict_publish_should_notify"] is True + assert publish_payload["linux_validation_verdict_publish_requires_manual_action"] is False + assert publish_payload["linux_validation_verdict_publish_channel"] == "follow_up" + assert "linux_validation_verdict_publish_with_follow_up" in publish_payload["reason_codes"] + assert publish_payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_verdict_publish_payload_blocked(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_verdict_publish_gate_module() + verdict_payload = _sample_linux_validation_verdict_payload() + verdict_payload["terminal_verdict_status"] = "blocked" + verdict_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + verdict_payload["terminal_verdict_exit_code"] = 1 + verdict_payload["terminal_verdict_should_proceed"] = False + verdict_payload["terminal_verdict_requires_manual_action"] = True + verdict_payload["terminal_verdict_channel"] = "blocker" + verdict_payload["linux_validation_should_dispatch"] = False + verdict_payload["linux_validation_requires_manual_action"] = True + verdict_payload["linux_validation_channel"] = "blocker" + verdict_payload["linux_validation_dispatch_status"] = "blocked" + verdict_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + verdict_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_payload["linux_validation_verdict_status"] = "blocked" + verdict_payload["linux_validation_verdict_decision"] = "hold_linux_validation_blocker" + verdict_payload["linux_validation_verdict_exit_code"] = 1 + verdict_payload["linux_validation_passed"] = False + verdict_payload["linux_validation_verdict_requires_manual_action"] = True + verdict_payload["linux_validation_verdict_channel"] = "blocker" + verdict_payload["reason_codes"] = ["linux_validation_verdict_blocked"] + + verdict_path = tmp_path / "ci_workflow_linux_validation_verdict_blocked.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_linux_validation_verdict_report(verdict_path) + publish_payload = gate.build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_verdict_publish_status"] == "blocked" + assert ( + publish_payload["linux_validation_verdict_publish_decision"] + == "announce_linux_validation_blocker" + ) + assert publish_payload["linux_validation_verdict_publish_exit_code"] == 1 + assert publish_payload["linux_validation_verdict_publish_should_notify"] is True + assert publish_payload["linux_validation_verdict_publish_requires_manual_action"] is True + assert publish_payload["linux_validation_verdict_publish_channel"] == "blocker" + assert "linux_validation_verdict_publish_blocked" in publish_payload["reason_codes"] + + +def test_build_linux_validation_verdict_publish_payload_rejects_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_publish_gate_module() + verdict_payload = _sample_linux_validation_verdict_payload() + verdict_payload["linux_validation_verdict_status"] = "validated" + verdict_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + verdict_payload["linux_validation_verdict_requires_manual_action"] = True + verdict_payload["linux_validation_verdict_channel"] = "follow_up" + + verdict_path = tmp_path / "ci_workflow_linux_validation_verdict_mismatch.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_linux_validation_verdict_report(verdict_path) + publish_payload = gate.build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + + assert publish_payload["linux_validation_verdict_publish_status"] == "contract_failed" + assert publish_payload["linux_validation_verdict_publish_decision"] == "abort_publish" + assert publish_payload["linux_validation_verdict_publish_exit_code"] == 1 + assert "linux_validation_verdict_decision_mismatch" in publish_payload["structural_issues"] + assert ( + "linux_validation_verdict_requires_manual_action_mismatch" + in publish_payload["structural_issues"] + ) + assert "linux_validation_verdict_channel_mismatch" in publish_payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_verdict_publish_gate_module() + verdict_payload = _sample_linux_validation_verdict_payload() + verdict_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + verdict_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + verdict_payload["terminal_verdict_exit_code"] = 1 + verdict_payload["terminal_verdict_channel"] = "follow_up" + verdict_payload["linux_validation_channel"] = "follow_up" + verdict_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + verdict_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + verdict_payload["linux_validation_dispatch_exit_code"] = 1 + verdict_payload["linux_validation_verdict_status"] = "validated_with_follow_up" + verdict_payload["linux_validation_verdict_decision"] = ( + "accept_linux_validation_with_follow_up" + ) + verdict_payload["linux_validation_verdict_exit_code"] = 1 + verdict_payload["linux_validation_verdict_channel"] = "follow_up" + verdict_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + verdict_payload["reason_codes"] = ["linux_validation_verdict_validated_with_follow_up"] + + verdict_path = tmp_path / "ci_workflow_linux_validation_verdict_output.json" + verdict_path.write_text(json.dumps(verdict_payload, indent=2), encoding="utf-8") + + verdict_report = gate.load_linux_validation_verdict_report(verdict_path) + publish_payload = gate.build_linux_validation_verdict_publish_payload( + verdict_report, + source_path=verdict_path.resolve(), + ) + outputs = gate.build_github_output_values( + publish_payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict_publish.md" + ), + ) + + assert outputs["workflow_linux_validation_verdict_publish_status"] == "published_with_follow_up" + assert ( + outputs["workflow_linux_validation_verdict_publish_decision"] + == "announce_linux_validation_passed_with_follow_up" + ) + assert outputs["workflow_linux_validation_verdict_publish_exit_code"] == "1" + assert outputs["workflow_linux_validation_verdict_publish_should_notify"] == "true" + assert outputs["workflow_linux_validation_verdict_publish_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_verdict_publish_channel"] == "follow_up" + assert outputs["workflow_linux_validation_verdict_publish_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_verdict_publish_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_verdict_publish_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_verdict_publish_report_json"].endswith( + "ci_workflow_linux_validation_verdict_publish.json" + ) + assert outputs["workflow_linux_validation_verdict_publish_report_markdown"].endswith( + "ci_workflow_linux_validation_verdict_publish.md" + ) diff --git a/tests/test_p2_lv_verdict_rt.py b/tests/test_p2_lv_verdict_rt.py new file mode 100644 index 0000000..929d3bc --- /dev/null +++ b/tests/test_p2_lv_verdict_rt.py @@ -0,0 +1,320 @@ +"""Contract tests for P2-61 Linux CI workflow Linux validation verdict gate.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_ci_workflow_linux_validation_verdict_gate_module(): + script_path = Path("scripts") / "run_p2_lv_verdict_gate.py" + spec = importlib.util.spec_from_file_location( + "p2_linux_ci_workflow_linux_validation_verdict_gate", + script_path, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _sample_linux_validation_dispatch_payload() -> dict: + return { + "generated_at": "2026-05-01T00:00:00+00:00", + "source_terminal_verdict_report": "/tmp/ci_workflow_terminal_verdict.json", + "project_root": "/tmp", + "source_release_final_publish_archive_report": "/tmp/ci_workflow_release_final_publish_archive.json", + "source_gate_manifest_drift_report": "/tmp/linux_gate_manifest_drift.json", + "release_final_publish_archive_status": "archived", + "gate_manifest_drift_status": "passed", + "gate_manifest_drift_missing_runtime_tests": [], + "gate_manifest_drift_missing_manifest_entries": [], + "gate_manifest_drift_orphan_manifest_entries": [], + "terminal_verdict_status": "ready_for_linux_validation", + "terminal_verdict_decision": "proceed_linux_validation", + "terminal_verdict_exit_code": 0, + "terminal_verdict_should_proceed": True, + "terminal_verdict_requires_manual_action": False, + "terminal_verdict_channel": "release", + "linux_validation_should_dispatch": True, + "linux_validation_requires_manual_action": False, + "linux_validation_channel": "release", + "linux_validation_dispatch_status": "ready_dry_run", + "linux_validation_dispatch_decision": "dispatch_linux_validation", + "linux_validation_dispatch_exit_code": 0, + "linux_validation_dispatch_attempted": False, + "linux_validation_timeout_seconds": 7200, + "linux_validation_command": "python scripts/run_p2_linux_unified_pipeline_gate.py --continue-on-failure", + "linux_validation_command_parts": [ + "python", + "scripts/run_p2_linux_unified_pipeline_gate.py", + "--continue-on-failure", + ], + "dry_run": True, + "command_returncode": None, + "command_stdout_tail": "", + "command_stderr_tail": "", + "dispatch_error_type": "", + "dispatch_error_message": "", + "release_run_id": 1234567890, + "release_run_url": "https://github.com/acme/demo/actions/runs/1234567890", + "follow_up_queue_url": "", + "linux_validation_dispatch_summary": ( + "terminal_verdict_status=ready_for_linux_validation " + "terminal_verdict_decision=proceed_linux_validation " + "linux_validation_dispatch_status=ready_dry_run " + "linux_validation_dispatch_decision=dispatch_linux_validation" + ), + "reason_codes": ["linux_validation_dispatch_ready_dry_run"], + "structural_issues": [], + "missing_artifacts": [], + "evidence_manifest": [ + { + "source": "final_publish_archive", + "path": "/tmp/ci_workflow_release_final_publish_archive.json", + "exists": True, + }, + { + "source": "manifest_drift", + "path": "/tmp/linux_gate_manifest_drift.json", + "exists": True, + }, + { + "source": "terminal_verdict", + "path": "/tmp/ci_workflow_terminal_verdict.json", + "exists": True, + }, + ], + } + + +def test_build_linux_validation_verdict_payload_validated_from_ready_dry_run(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + + assert payload["linux_validation_verdict_status"] == "validated" + assert payload["linux_validation_verdict_decision"] == "accept_linux_validation" + assert payload["linux_validation_verdict_exit_code"] == 0 + assert payload["linux_validation_verdict_should_accept"] is True + assert payload["linux_validation_verdict_requires_manual_action"] is False + assert payload["linux_validation_verdict_channel"] == "release" + assert payload["linux_validation_dispatch_status"] == "ready_dry_run" + assert payload["linux_validation_dispatch_attempted"] is False + + +def test_build_linux_validation_verdict_payload_validated_with_follow_up_from_dry_run( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_channel"] = "follow_up" + dispatch_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + dispatch_payload["reason_codes"] = ["linux_validation_dispatch_ready_with_follow_up_dry_run"] + + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch_follow_up.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + + assert payload["linux_validation_verdict_status"] == "validated_with_follow_up" + assert ( + payload["linux_validation_verdict_decision"] + == "accept_linux_validation_with_follow_up" + ) + assert payload["linux_validation_verdict_exit_code"] == 1 + assert payload["linux_validation_verdict_should_accept"] is True + assert payload["linux_validation_verdict_requires_manual_action"] is False + assert payload["linux_validation_verdict_channel"] == "follow_up" + assert payload["follow_up_queue_url"].endswith("/issues/77") + + +def test_build_linux_validation_verdict_payload_validation_failed_from_dispatch_failed( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_payload["dry_run"] = False + dispatch_payload["linux_validation_dispatch_status"] = "dispatch_failed" + dispatch_payload["linux_validation_dispatch_decision"] = "dispatch_linux_validation" + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_dispatch_attempted"] = True + dispatch_payload["command_returncode"] = 124 + dispatch_payload["dispatch_error_type"] = "timeout" + dispatch_payload["dispatch_error_message"] = "linux validation command timeout after 7200s" + dispatch_payload["reason_codes"] = ["linux_validation_dispatch_timeout"] + + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch_failed.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + + assert payload["linux_validation_verdict_status"] == "validation_failed" + assert payload["linux_validation_verdict_decision"] == "escalate_linux_validation_failure" + assert payload["linux_validation_verdict_exit_code"] == 1 + assert payload["linux_validation_verdict_should_accept"] is False + assert payload["linux_validation_verdict_requires_manual_action"] is True + assert payload["linux_validation_verdict_channel"] == "blocker" + assert "linux_validation_verdict_dispatch_timeout" in payload["reason_codes"] + + +def test_build_linux_validation_verdict_payload_blocked_from_dispatch_blocked( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "blocked" + dispatch_payload["terminal_verdict_decision"] = "halt_linux_validation_blocker" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_should_proceed"] = False + dispatch_payload["terminal_verdict_requires_manual_action"] = True + dispatch_payload["terminal_verdict_channel"] = "blocker" + dispatch_payload["linux_validation_should_dispatch"] = False + dispatch_payload["linux_validation_requires_manual_action"] = True + dispatch_payload["linux_validation_channel"] = "blocker" + dispatch_payload["linux_validation_dispatch_status"] = "blocked" + dispatch_payload["linux_validation_dispatch_decision"] = "hold_linux_validation_blocker" + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["linux_validation_dispatch_attempted"] = False + dispatch_payload["reason_codes"] = ["linux_validation_dispatch_blocked"] + + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch_blocked.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + + assert payload["linux_validation_verdict_status"] == "blocked" + assert payload["linux_validation_verdict_decision"] == "hold_linux_validation_blocker" + assert payload["linux_validation_verdict_exit_code"] == 1 + assert payload["linux_validation_verdict_should_accept"] is False + assert payload["linux_validation_verdict_requires_manual_action"] is True + assert payload["linux_validation_verdict_channel"] == "blocker" + assert "linux_validation_verdict_blocked" in payload["reason_codes"] + + +def test_build_linux_validation_verdict_payload_rejects_dispatch_contract_mismatch( + tmp_path: Path, +): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_payload["linux_validation_dispatch_status"] = "dispatched" + dispatch_payload["linux_validation_dispatch_decision"] = "dispatch_linux_validation_with_follow_up" + dispatch_payload["linux_validation_channel"] = "release" + dispatch_payload["linux_validation_dispatch_attempted"] = False + dispatch_payload["command_returncode"] = None + dispatch_payload["evidence_manifest"][0]["exists"] = False + + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch_mismatch.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + + assert payload["linux_validation_verdict_status"] == "contract_failed" + assert payload["linux_validation_verdict_decision"] == "abort_linux_validation_verdict" + assert payload["linux_validation_verdict_exit_code"] == 1 + assert payload["linux_validation_verdict_should_accept"] is False + assert payload["linux_validation_verdict_requires_manual_action"] is True + assert payload["linux_validation_verdict_channel"] == "blocker" + assert "linux_validation_dispatch_decision_mismatch" in payload["structural_issues"] + assert "linux_validation_dispatch_attempted_mismatch" in payload["structural_issues"] + assert "linux_validation_dispatch_evidence_missing" in payload["structural_issues"] + + +def test_build_github_output_values_contract(tmp_path: Path): + gate = _load_ci_workflow_linux_validation_verdict_gate_module() + dispatch_payload = _sample_linux_validation_dispatch_payload() + dispatch_payload["terminal_verdict_status"] = "ready_with_follow_up_for_linux_validation" + dispatch_payload["terminal_verdict_decision"] = "proceed_linux_validation_with_follow_up" + dispatch_payload["terminal_verdict_exit_code"] = 1 + dispatch_payload["terminal_verdict_channel"] = "follow_up" + dispatch_payload["linux_validation_channel"] = "follow_up" + dispatch_payload["linux_validation_dispatch_status"] = "ready_with_follow_up_dry_run" + dispatch_payload["linux_validation_dispatch_decision"] = ( + "dispatch_linux_validation_with_follow_up" + ) + dispatch_payload["linux_validation_dispatch_exit_code"] = 1 + dispatch_payload["follow_up_queue_url"] = "https://github.com/acme/demo/issues/77" + dispatch_payload["reason_codes"] = ["linux_validation_dispatch_ready_with_follow_up_dry_run"] + + dispatch_path = tmp_path / "ci_workflow_linux_validation_dispatch_outputs.json" + dispatch_path.write_text(json.dumps(dispatch_payload, indent=2), encoding="utf-8") + + dispatch_report = gate.load_linux_validation_dispatch_report(dispatch_path) + payload = gate.build_linux_validation_verdict_payload( + dispatch_report, + source_path=dispatch_path.resolve(), + ) + outputs = gate.build_github_output_values( + payload, + output_json=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.json" + ), + output_markdown=Path( + ".claude/reports/linux_unified_gate/ci_workflow_linux_validation_verdict.md" + ), + ) + + assert outputs["workflow_linux_validation_verdict_status"] == "validated_with_follow_up" + assert ( + outputs["workflow_linux_validation_verdict_decision"] + == "accept_linux_validation_with_follow_up" + ) + assert outputs["workflow_linux_validation_verdict_exit_code"] == "1" + assert outputs["workflow_linux_validation_verdict_should_accept"] == "true" + assert outputs["workflow_linux_validation_verdict_requires_manual_action"] == "false" + assert outputs["workflow_linux_validation_verdict_channel"] == "follow_up" + assert outputs["workflow_linux_validation_verdict_dispatch_status"] == "ready_with_follow_up_dry_run" + assert outputs["workflow_linux_validation_verdict_dispatch_attempted"] == "false" + assert outputs["workflow_linux_validation_verdict_command_returncode"] == "" + assert ( + outputs["workflow_linux_validation_verdict_terminal_status"] + == "ready_with_follow_up_for_linux_validation" + ) + assert outputs["workflow_linux_validation_verdict_gate_manifest_drift_status"] == "passed" + assert outputs["workflow_linux_validation_verdict_follow_up_queue_url"].endswith( + "/issues/77" + ) + assert outputs["workflow_linux_validation_verdict_run_id"] == "1234567890" + assert outputs["workflow_linux_validation_verdict_run_url"].endswith( + "/actions/runs/1234567890" + ) + assert outputs["workflow_linux_validation_verdict_report_json"].endswith( + "ci_workflow_linux_validation_verdict.json" + ) + assert outputs["workflow_linux_validation_verdict_report_markdown"].endswith( + "ci_workflow_linux_validation_verdict.md" + ) diff --git a/tests/test_p2_multi_agent_supervisor_gate_runtime.py b/tests/test_p2_multi_agent_supervisor_gate_runtime.py new file mode 100644 index 0000000..688fb28 --- /dev/null +++ b/tests/test_p2_multi_agent_supervisor_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-01 multi-agent supervisor gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_multi_agent_supervisor_gate_module(): + script_path = Path("scripts") / "run_p2_multi_agent_supervisor_gate.py" + spec = importlib.util.spec_from_file_location("p2_multi_agent_supervisor_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_multi_agent_supervisor_gate_contract(): + gate = _load_p2_multi_agent_supervisor_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_multi_agent_supervisor_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_tools_registry_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_p2_org_policy_audit_gate_runtime.py b/tests/test_p2_org_policy_audit_gate_runtime.py new file mode 100644 index 0000000..aced4bf --- /dev/null +++ b/tests/test_p2_org_policy_audit_gate_runtime.py @@ -0,0 +1,29 @@ +"""Contract tests for P2-05 org policy and audit gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_p2_org_policy_audit_gate_module(): + script_path = Path("scripts") / "run_p2_org_policy_audit_gate.py" + spec = importlib.util.spec_from_file_location("p2_org_policy_audit_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_p2_org_policy_audit_gate_contract(): + gate = _load_p2_org_policy_audit_gate_module() + commands = gate.build_gate_commands(Path.cwd()) + + assert len(commands) == 4 + assert commands[0] == "python -m pytest -q -c pytest.ini tests/test_org_policy_audit_runtime.py" + assert commands[-1] == "python -m pytest -q -c pytest.ini tests/test_main_runtime.py" + for command in commands: + assert command.startswith("python -m pytest -q -c pytest.ini ") + test_file = command.split("pytest.ini ", 1)[1] + assert Path(test_file).is_file() diff --git a/tests/test_phase0_gate_runtime.py b/tests/test_phase0_gate_runtime.py new file mode 100644 index 0000000..64ce4fd --- /dev/null +++ b/tests/test_phase0_gate_runtime.py @@ -0,0 +1,37 @@ +"""Contract tests for Phase 0 regression gate script.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_phase0_gate_module(): + script_path = Path("scripts") / "run_phase0_gate.py" + spec = importlib.util.spec_from_file_location("phase0_gate", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_phase0_gate_manifest_references_existing_files(): + gate = _load_phase0_gate_module() + assert gate.PHASE0_GATE_TEST_FILES + for test_file in gate.PHASE0_GATE_TEST_FILES: + assert Path(test_file).is_file() + + +def test_phase0_gate_builds_pytest_command_contract(): + gate = _load_phase0_gate_module() + command = gate.build_phase0_pytest_command( + python_executable="python-custom", + extra_pytest_args=["-k", "phase0", "--maxfail", "1"], + ) + + assert command[:5] == ["python-custom", "-m", "pytest", "-c", "pytest.ini"] + assert command[5 : 5 + len(gate.PHASE0_GATE_TEST_FILES)] == list( + gate.PHASE0_GATE_TEST_FILES + ) + assert command[-4:] == ["-k", "phase0", "--maxfail", "1"] diff --git a/tests/test_phase0_regression_contract.py b/tests/test_phase0_regression_contract.py new file mode 100644 index 0000000..ae72936 --- /dev/null +++ b/tests/test_phase0_regression_contract.py @@ -0,0 +1,46 @@ +"""Contract tests for Phase 0 regression gate organization.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +pytestmark = pytest.mark.phase0_gate + + +def _load_phase0_gate_module(): + script_path = Path("scripts") / "run_phase0_gate.py" + spec = importlib.util.spec_from_file_location("phase0_gate_script", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_phase0_gate_manifest_references_existing_test_files(): + gate = _load_phase0_gate_module() + assert gate.PHASE0_GATE_TEST_FILES + for test_file in gate.PHASE0_GATE_TEST_FILES: + assert Path(test_file).is_file() + + +def test_phase0_gate_build_command_contains_pytest_ini_and_manifest_files(): + gate = _load_phase0_gate_module() + command = gate.build_phase0_pytest_command( + python_executable="python-custom", + extra_pytest_args=["-k", "runtime"], + ) + + assert command[:5] == ["python-custom", "-m", "pytest", "-c", "pytest.ini"] + for test_file in gate.PHASE0_GATE_TEST_FILES: + assert test_file in command + assert command[-2:] == ["-k", "runtime"] + + +def test_phase0_gate_marker_registered_in_pytest_ini(): + content = Path("pytest.ini").read_text(encoding="utf-8") + assert "phase0_gate:" in content diff --git a/tests/test_plugins_runtime.py b/tests/test_plugins_runtime.py new file mode 100644 index 0000000..b2236c8 --- /dev/null +++ b/tests/test_plugins_runtime.py @@ -0,0 +1,124 @@ +"""Runtime tests for local plugin loading.""" + +from __future__ import annotations + +import json +import shutil +import uuid +from pathlib import Path + +import pytest + +from claude_code.plugins import create_plugin_manager + + +def _safe_rmtree(path: Path) -> None: + """Best-effort recursive delete for temp test directories.""" + if path.exists(): + shutil.rmtree(path, ignore_errors=True) + + +def _cleanup_plugin_temp_root(root: Path) -> None: + """Remove plugin_* temp children under tests/.tmp_plugins.""" + if not root.exists(): + return + for child in root.iterdir(): + if child.is_dir() and child.name.startswith("plugin_"): + _safe_rmtree(child) + + +@pytest.fixture +def plugin_temp_root() -> Path: + """Workspace-scoped temp root used by plugin runtime tests.""" + root = Path("tests") / ".tmp_plugins" + root.mkdir(parents=True, exist_ok=True) + _cleanup_plugin_temp_root(root) + yield root + _cleanup_plugin_temp_root(root) + if root.exists() and not any(root.iterdir()): + root.rmdir() + + +@pytest.fixture +def plugin_case_factory(plugin_temp_root: Path): + """Factory fixture that tracks plugin temp dirs and enforces teardown cleanup.""" + created: list[Path] = [] + + def _create() -> Path: + case_dir = plugin_temp_root / f"plugin_{uuid.uuid4().hex}" + case_dir.mkdir(parents=True, exist_ok=True) + created.append(case_dir) + return case_dir + + yield _create + + for case_dir in created: + _safe_rmtree(case_dir) + for case_dir in created: + assert not case_dir.exists() + + +@pytest.mark.asyncio +async def test_local_plugin_loader_instantiates_plugin_entrypoint(plugin_case_factory): + base = plugin_case_factory() + + (base / "plugin.json").write_text( + json.dumps( + { + "id": "local-demo", + "name": "Local Demo", + "version": "0.1.0", + "description": "runtime test plugin", + "entry": "plugin.py", + } + ), + encoding="utf-8", + ) + (base / "plugin.py").write_text( + "\n".join( + [ + "class DemoPlugin:", + " def get_tools(self):", + " return ['tool-a']", + " def get_commands(self):", + " return ['cmd-a']", + " def get_hooks(self):", + " return {'event.a': [lambda payload: payload]}", + "", + "def get_plugin():", + " return DemoPlugin()", + ] + ), + encoding="utf-8", + ) + + manager = create_plugin_manager() + plugin = await manager.load_plugin( + "local-demo", + { + "id": "local-demo", + "type": "local", + "path": str(base), + }, + ) + + assert plugin is not None + assert plugin.instance is not None + assert manager.get_all_tools() == ["tool-a"] + assert manager.get_all_commands() == ["cmd-a"] + assert "event.a" in manager.get_all_hooks() + + +def test_plugin_temp_cleanup_is_idempotent(plugin_temp_root: Path): + stale_one = plugin_temp_root / "plugin_stale_one" + stale_two = plugin_temp_root / "plugin_stale_two" + stale_one.mkdir(parents=True, exist_ok=True) + stale_two.mkdir(parents=True, exist_ok=True) + (stale_one / "marker.txt").write_text("x", encoding="utf-8") + (stale_two / "marker.txt").write_text("y", encoding="utf-8") + + _cleanup_plugin_temp_root(plugin_temp_root) + _cleanup_plugin_temp_root(plugin_temp_root) + + leftovers = [path for path in plugin_temp_root.glob("plugin_*") if path.is_dir()] + assert leftovers == [] diff --git a/tests/test_query_engine_runtime.py b/tests/test_query_engine_runtime.py index c8d6b40..d02470b 100644 --- a/tests/test_query_engine_runtime.py +++ b/tests/test_query_engine_runtime.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from pathlib import Path import pytest from claude_code.engine.query import QueryConfig, QueryEngine, ToolCallResult @@ -121,6 +122,28 @@ async def execute( return ToolResult(content="should-not-run") +class _WorkingDirTool(Tool): + @property + def name(self) -> str: + return "workdir_tool" + + @property + def description(self) -> str: + return "returns working directory from tool context" + + @property + def input_schema(self) -> dict: + return {"type": "object", "properties": {}, "required": []} + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=context.working_directory) + + class _SleepToolA(Tool): @property def name(self) -> str: @@ -212,6 +235,26 @@ async def test_query_engine_executes_tool_use_blocks(): assert tool_results[0].result.content == "hello" +@pytest.mark.asyncio +async def test_query_engine_propagates_configured_working_directory_to_tools(): + registry = ToolRegistry() + registry.register(_WorkingDirTool()) + configured_workdir = str(Path.cwd() / "custom_workdir_for_test") + engine = QueryEngine( + api_client=_FakeAPIClient("workdir_tool"), + config=QueryConfig(tools=[_WorkingDirTool()], working_directory=configured_workdir), + tool_registry=registry, + ) + + tool_results: list[ToolCallResult] = [] + async for event in engine.query("report cwd"): + if isinstance(event, ToolCallResult): + tool_results.append(event) + + assert tool_results + assert tool_results[0].result.content == configured_workdir + + @pytest.mark.asyncio async def test_query_engine_enforces_plan_mode_permissions(): registry = ToolRegistry() diff --git a/tests/test_resume_runtime.py b/tests/test_resume_runtime.py new file mode 100644 index 0000000..8dc14d7 --- /dev/null +++ b/tests/test_resume_runtime.py @@ -0,0 +1,129 @@ +"""Runtime tests for /resume command and session recovery flow.""" + +from __future__ import annotations + +import io +from types import SimpleNamespace + +import pytest +from rich.console import Console + +from claude_code.commands.base import CommandContext +from claude_code.commands.compact import ResumeCommand +from claude_code.engine.query import QueryConfig, QueryEngine +from claude_code.engine.session import SessionManager +from claude_code.repl import REPL, REPLConfig + + +class _NoopAPIClient: + async def create_message_streaming(self, messages, options): + if False: + yield None + + async def create_message(self, messages, options): + class _Response: + content = [{"type": "text", "text": "noop"}] + + return _Response() + + +def _context(tmp_path, engine) -> CommandContext: + return CommandContext( + working_directory=str(tmp_path), + console=Console(file=io.StringIO(), force_terminal=False, width=120), + engine=engine, + session={"id": "ctx-session"}, + ) + + +@pytest.mark.asyncio +async def test_resume_command_lists_recent_sessions(tmp_path): + manager = SessionManager(storage_dir=tmp_path) + session = manager.create_session() + session.add_user_message("hello") + session.add_assistant_message("world") + + engine = SimpleNamespace(session_manager=manager) + result = await ResumeCommand().execute("", _context(tmp_path, engine)) + + assert result.success + assert session.id in str(result.content) + assert "Use /resume " in str(result.content) + + +@pytest.mark.asyncio +async def test_resume_command_restores_target_session(tmp_path): + manager = SessionManager(storage_dir=tmp_path) + session = manager.create_session() + session.metadata.working_directory = str(tmp_path) + session.add_user_message("u1") + session.add_assistant_message("a1") + session.save() + + engine = QueryEngine(api_client=_NoopAPIClient(), config=QueryConfig()) + engine.session_manager = manager + + result = await ResumeCommand().execute(session.id, _context(tmp_path, engine)) + + assert result.success + assert engine.config.session_id == session.id + assert engine.config.working_directory == str(tmp_path) + assert len(engine.messages) == 2 + assert "Session ID" in str(result.content) + + +@pytest.mark.asyncio +async def test_resume_command_reports_missing_or_unreadable_session(tmp_path): + # Corrupted session file should be treated as unreadable and fail gracefully. + (tmp_path / "broken.json").write_text("{not-valid-json", encoding="utf-8") + manager = SessionManager(storage_dir=tmp_path) + + engine = QueryEngine(api_client=_NoopAPIClient(), config=QueryConfig()) + engine.session_manager = manager + + result = await ResumeCommand().execute("broken", _context(tmp_path, engine)) + + assert result.success is False + assert "not found or unreadable" in str(result.error) + + +@pytest.mark.asyncio +async def test_query_engine_resume_session_without_candidates_returns_false(tmp_path): + manager = SessionManager(storage_dir=tmp_path) + engine = QueryEngine(api_client=_NoopAPIClient(), config=QueryConfig()) + engine.session_manager = manager + + resumed = await engine.resume_session() + assert resumed is False + + +@pytest.mark.asyncio +async def test_repl_resume_command_calls_engine_resume_session(tmp_path): + class _StubEngine: + def __init__(self) -> None: + self.called = None + self.messages = [] + self.tool_results = [] + self.config = SimpleNamespace( + session_id="active-session", + working_directory=str(tmp_path), + ) + + async def resume_session(self, session_id: str) -> bool: + self.called = session_id + self.config.session_id = session_id + self.messages = [object(), object()] + return True + + engine = _StubEngine() + repl = REPL( + engine=engine, + config=REPLConfig(working_directory=str(tmp_path), welcome_message=False), + ) + repl._commands = {"resume": ResumeCommand()} + repl._console = Console(file=io.StringIO(), force_terminal=False, width=120) + + handled = await repl.run_command("/resume my-session") + + assert handled is True + assert engine.called == "my-session" diff --git a/tests/test_review_command_runtime.py b/tests/test_review_command_runtime.py new file mode 100644 index 0000000..094a6e7 --- /dev/null +++ b/tests/test_review_command_runtime.py @@ -0,0 +1,154 @@ +"""Runtime tests for /review command and review service behavior.""" + +from __future__ import annotations + +import io +import subprocess +from pathlib import Path + +import pytest +from rich.console import Console + +from claude_code.commands.base import CommandContext +from claude_code.commands.review import ReviewCommand +from claude_code.services.review_service import ReviewService + + +def _context(tmp_path: Path) -> CommandContext: + return CommandContext( + working_directory=str(tmp_path), + console=Console(file=io.StringIO(), force_terminal=False, width=120), + engine=None, + session=None, + config=None, + ) + + +def _run_git(cwd: Path, *args: str) -> None: + subprocess.run( + ["git", *args], + cwd=cwd, + check=True, + capture_output=True, + text=True, + ) + + +def _init_git_repo(repo: Path) -> None: + _run_git(repo, "init") + _run_git(repo, "config", "user.email", "review-test@example.com") + _run_git(repo, "config", "user.name", "review-test") + + +@pytest.mark.asyncio +async def test_review_command_reports_no_changes_for_clean_repo(tmp_path): + _init_git_repo(tmp_path) + baseline = tmp_path / "sample.py" + baseline.write_text("def ok():\n return 1\n", encoding="utf-8") + _run_git(tmp_path, "add", "sample.py") + _run_git(tmp_path, "commit", "-m", "baseline") + + result = await ReviewCommand().execute("", _context(tmp_path)) + + assert result.success + assert "No changes found to review." in str(result.content) + + +@pytest.mark.asyncio +async def test_review_command_returns_error_for_missing_file(tmp_path): + result = await ReviewCommand().execute("missing.py", _context(tmp_path)) + + assert result.success is False + assert "File not found: missing.py" in str(result.error) + + +def test_review_service_detects_findings_for_specified_files(tmp_path): + target = tmp_path / "risk.py" + target.write_text( + "\n".join( + [ + "import subprocess", + "subprocess.run('echo hello', shell=True)", + "api_key = 'hardcoded-demo'", + "print('debug log')", + "# TODO: remove unsafe code", + ] + ) + + "\n", + encoding="utf-8", + ) + + result = ReviewService().review_files(str(tmp_path), ["risk.py"]) + + assert result.files_reviewed == ["risk.py"] + assert len(result.findings) >= 4 + severities = {finding.severity for finding in result.findings} + assert "high" in severities + assert "low" in severities + + +@pytest.mark.asyncio +async def test_review_command_reviews_current_git_diff(tmp_path): + _init_git_repo(tmp_path) + target = tmp_path / "module.py" + target.write_text("def safe():\n return 0\n", encoding="utf-8") + _run_git(tmp_path, "add", "module.py") + _run_git(tmp_path, "commit", "-m", "baseline") + + target.write_text("def safe():\n print('debug')\n return 0\n", encoding="utf-8") + + result = await ReviewCommand().execute("", _context(tmp_path)) + + assert result.success + assert "Files Reviewed: 1" in str(result.content) + assert "module.py" in str(result.content) + assert "[LOW]" in str(result.content) + + +def test_review_service_reviews_only_changed_lines_in_git_diff(tmp_path): + _init_git_repo(tmp_path) + target = tmp_path / "script.py" + target.write_text( + "\n".join( + [ + "import subprocess", + "def run():", + " return 1", + ] + ) + + "\n", + encoding="utf-8", + ) + _run_git(tmp_path, "add", "script.py") + _run_git(tmp_path, "commit", "-m", "baseline") + + target.write_text( + "\n".join( + [ + "import subprocess", + "def run():", + " subprocess.run('echo risky', shell=True)", + ] + ) + + "\n", + encoding="utf-8", + ) + + result = ReviewService().review_git_changes(str(tmp_path)) + + assert "script.py" in result.files_reviewed + assert any("shell=True" in finding.issue for finding in result.findings) + + +@pytest.mark.asyncio +async def test_review_command_accepts_quoted_file_path(tmp_path): + nested = tmp_path / "sub dir" + nested.mkdir(parents=True, exist_ok=True) + target = nested / "quoted.py" + target.write_text("try:\n pass\nexcept Exception:\n pass\n", encoding="utf-8") + + result = await ReviewCommand().execute('"sub dir/quoted.py"', _context(tmp_path)) + + assert result.success + assert "sub dir/quoted.py" in str(result.content) + assert "Findings:" in str(result.content) diff --git a/tests/test_runtime_bootstrap.py b/tests/test_runtime_bootstrap.py new file mode 100644 index 0000000..76d87e5 --- /dev/null +++ b/tests/test_runtime_bootstrap.py @@ -0,0 +1,63 @@ +"""Runtime bootstrap tests for unified entrypoint assembly.""" + +from __future__ import annotations + +import pytest + +import claude_code.main as main_mod +from claude_code.mcp.server import create_mcp_server + + +@pytest.mark.asyncio +async def test_runtime_entrypoints_reuse_create_engine(monkeypatch): + calls: list[tuple[str | None, str | None]] = [] + + class _FakeEngine: + async def query(self, user_input: str): + yield {"type": "text", "content": "ok"} + + class _FakeREPL: + def __init__(self, engine, config): + self.engine = engine + self.config = config + + async def run(self): + return None + + class _FakePipeMode: + def __init__(self, engine): + self.engine = engine + + async def run(self): + return 0 + + def _fake_create_engine(model=None, working_dir=None): + calls.append((model, working_dir)) + return _FakeEngine() + + monkeypatch.setattr(main_mod, "create_engine", _fake_create_engine) + monkeypatch.setattr(main_mod, "REPL", _FakeREPL) + monkeypatch.setattr(main_mod, "PipeMode", _FakePipeMode) + monkeypatch.setattr(main_mod.os, "getcwd", lambda: "D:/workspace") + + await main_mod.run_repl(model="claude-a") + await main_mod.run_pipe_mode(model="claude-b") + await main_mod.run_single_query(query="hello", model="claude-c") + + assert calls == [ + ("claude-a", "D:/workspace"), + ("claude-b", "D:/workspace"), + ("claude-c", "D:/workspace"), + ] + + +@pytest.mark.asyncio +async def test_create_mcp_server_uses_injected_tool_registry(): + sentinel_registry = object() + server = await create_mcp_server( + working_directory="D:/workspace", + server_name="runtime-test", + tool_registry=sentinel_registry, + ) + + assert server._tool_registry is sentinel_registry diff --git a/tests/test_server_runtime.py b/tests/test_server_runtime.py new file mode 100644 index 0000000..ae73d97 --- /dev/null +++ b/tests/test_server_runtime.py @@ -0,0 +1,423 @@ +"""Runtime tests for daemon/API control-plane.""" + +from __future__ import annotations + +import asyncio +from contextlib import contextmanager +from pathlib import Path +from types import SimpleNamespace +import time + +import pytest + +from claude_code.engine.session import SessionManager +from claude_code.services.event_journal import EventJournal +from claude_code.server.control_plane import ( + ControlPlaneClient, + ControlPlaneDaemon, + DaemonResponseError, + DaemonServerConfig, + DaemonUnavailableError, +) +from claude_code.tasks.manager import TaskManager +from claude_code.tasks.queue import InMemoryTaskQueue +from claude_code.tools.base import Tool, ToolContext, ToolResult +from claude_code.tools.registry import ToolRegistry + + +class _EchoTool(Tool): + @property + def name(self) -> str: + return "echo_tool" + + @property + def description(self) -> str: + return "Echo input text." + + @property + def input_schema(self) -> dict: + return { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + } + + async def execute( + self, + input_data: dict, + context: ToolContext, + on_progress=None, + ) -> ToolResult: + return ToolResult(content=str(input_data.get("text", "")), is_error=False, tool_use_id="echo") + + def validate_input(self, input_data: dict) -> tuple[bool, str | None]: + if "text" not in input_data: + return False, "missing text" + return True, None + + +class _FakeQueryEngine: + def __init__(self, session_manager: SessionManager, *, delay_seconds: float = 0.0) -> None: + self._session_manager = session_manager + self._delay_seconds = delay_seconds + self.received_queries: list[str] = [] + self._messages: list[object] = [] + self.config = SimpleNamespace( + model="claude-test-model", + permission_mode="default", + always_allow=[], + always_deny=[], + session_id=None, + working_directory=None, + ) + + async def query(self, user_input: str): + self.received_queries.append(user_input) + if self._delay_seconds > 0: + await asyncio.sleep(self._delay_seconds) + text = f"echo:{user_input}" + yield {"type": "text", "content": text} + assistant_message = SimpleNamespace( + role="assistant", + content=[{"type": "text", "text": text}], + ) + self._messages.append(assistant_message) + yield assistant_message + yield {"type": "stop_reason", "reason": "end_turn"} + + async def resume_session(self, session_id: str | None = None) -> bool: + if session_id: + session = self._session_manager.load_session(session_id) + if session is None: + return False + self.config.session_id = session.id + return True + sessions = self._session_manager.list_sessions() + if not sessions: + return False + self.config.session_id = sessions[0].id + return True + + def get_messages(self) -> list[object]: + return list(self._messages) + + +def _build_runtime(tmp_path: Path, *, query_delay_seconds: float = 0.0) -> tuple[SimpleNamespace, str]: + session_dir = tmp_path / "sessions" + session_manager = SessionManager(storage_dir=session_dir) + session = session_manager.create_session() + session.add_user_message("bootstrap message") + + tool_registry = ToolRegistry(lazy=False) + tool_registry.register(_EchoTool()) + + query_engine = _FakeQueryEngine(session_manager, delay_seconds=query_delay_seconds) + query_engine.config.session_id = session.id + query_engine.config.working_directory = str(tmp_path) + event_journal = EventJournal(tmp_path / ".claude" / "event_journal.jsonl") + query_engine.event_journal = event_journal + + runtime = SimpleNamespace( + working_directory=str(tmp_path), + query_engine=query_engine, + session_manager=session_manager, + task_manager=TaskManager(task_queue=InMemoryTaskQueue()), + tool_registry=tool_registry, + event_journal=event_journal, + ) + return runtime, session.id + + +@contextmanager +def _running_daemon(runtime: SimpleNamespace, *, request_timeout_seconds: float = 3.0): + daemon = ControlPlaneDaemon( + runtime, + DaemonServerConfig( + host="127.0.0.1", + port=0, + request_timeout_seconds=request_timeout_seconds, + ), + ) + daemon.start() + client = ControlPlaneClient(daemon.base_url, timeout_seconds=0.2) + last_error: Exception | None = None + for _ in range(40): + try: + client.health() + last_error = None + break + except Exception as exc: # pragma: no cover - startup race mitigation only + last_error = exc + time.sleep(0.05) + if last_error is not None: + daemon.stop() + raise last_error + try: + yield daemon + finally: + daemon.stop() + + +def test_control_plane_validation_rejects_empty_query(tmp_path: Path): + runtime, _ = _build_runtime(tmp_path) + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=2.0) + with pytest.raises(DaemonResponseError) as exc_info: + client.query("") + assert exc_info.value.status_code == 400 + assert exc_info.value.code == "validation_error" + + +def test_control_plane_service_endpoints_cover_session_task_tool_artifact(tmp_path: Path): + runtime, session_id = _build_runtime(tmp_path) + note_path = tmp_path / "note.txt" + note_path.write_text("daemon artifact", encoding="utf-8") + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + + health = client.health() + assert health["status"] == "ok" + + sessions = client.list_sessions()["sessions"] + assert sessions + session_payload = client.get_session(session_id)["session"] + assert session_payload["id"] == session_id + + task_payload = client.create_bash_task( + "echo daemon-control-plane", + background=False, + wait_timeout=2.0, + )["task"] + assert task_payload["id"] + task_detail = client.get_task(task_payload["id"])["task"] + assert task_detail["id"] == task_payload["id"] + assert task_detail["status"] in {"completed", "failed", "timeout"} + + tool_payload = client.execute_tool("echo_tool", {"text": "tool-ok"}) + assert tool_payload["is_error"] is False + assert tool_payload["content"] == "tool-ok" + + artifact = client.read_artifact("note.txt") + assert artifact["truncated"] is False + assert "daemon artifact" in artifact["content"] + + +def test_control_plane_client_server_runtime_query_chain(tmp_path: Path): + runtime, _ = _build_runtime(tmp_path) + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + result = client.query("hello daemon") + assert "echo:hello daemon" in result["output"] + assert runtime.query_engine.received_queries[-1] == "hello daemon" + + +def test_control_plane_failure_modes_daemon_down_timeout_and_session_not_found(tmp_path: Path): + down_client = ControlPlaneClient("http://127.0.0.1:9", timeout_seconds=0.2) + with pytest.raises(DaemonUnavailableError): + down_client.health() + + slow_runtime, _ = _build_runtime(tmp_path / "slow", query_delay_seconds=0.3) + with _running_daemon(slow_runtime, request_timeout_seconds=0.05) as daemon: + slow_client = ControlPlaneClient(daemon.base_url, timeout_seconds=2.0) + with pytest.raises(DaemonResponseError) as timeout_exc: + slow_client.query("slow") + assert timeout_exc.value.status_code == 504 + assert timeout_exc.value.code == "timeout" + + with pytest.raises(DaemonResponseError) as session_exc: + slow_client.get_session("session-missing") + assert session_exc.value.status_code == 404 + assert session_exc.value.code == "session_not_found" + + +def test_control_plane_events_endpoints(tmp_path: Path): + runtime, session_id = _build_runtime(tmp_path) + runtime.event_journal.append_event( + event_type="query.started", + payload={"input": "hello"}, + session_id=session_id, + conversation_id="conv-1", + source="query_engine", + ) + runtime.event_journal.append_event( + event_type="tool.completed", + payload={"tool_name": "echo_tool"}, + session_id=session_id, + conversation_id="conv-1", + source="query_engine", + ) + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + listed = client.list_events(session_id=session_id, limit=10) + assert listed["events"] + assert listed["events"][0]["session_id"] == session_id + + replayed = client.replay_events(session_id=session_id, event_type="tool.completed", limit=10) + assert replayed["events"] + assert all(item["event_type"] == "tool.completed" for item in replayed["events"]) + + +def test_control_plane_github_ci_workflow_endpoint(tmp_path: Path, monkeypatch): + runtime, _ = _build_runtime(tmp_path) + + captured: dict[str, object] = {} + + class _FakeWorkflowService: + def run_workflow(self, **kwargs): + captured.update(kwargs) + return { + "workflow_id": "wf-1", + "status": "completed", + "steps": [], + "working_directory": kwargs["working_directory"], + "headless_ci": kwargs["headless_ci"], + } + + import claude_code.server.control_plane as control_plane_mod + + monkeypatch.setattr(control_plane_mod, "GitHubCIWorkflowService", _FakeWorkflowService) + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + payload = client.run_github_ci_workflow( + issue_reference="#124", + plan_summary="link issue to PR", + branch_name="codex/p2-04-124", + base_branch="main", + ci_commands=["python -m pytest -q"], + headless_ci=True, + allow_dirty_repo=False, + push_remote=False, + create_pull_request=False, + ) + + assert payload["workflow"]["status"] == "completed" + assert captured["working_directory"] == str(tmp_path) + assert captured["issue_reference"] == "#124" + assert captured["create_pull_request"] is False + + +def test_control_plane_github_ci_workflow_failure_maps_error(tmp_path: Path, monkeypatch): + runtime, _ = _build_runtime(tmp_path) + + from claude_code.services.github_ci_workflow import GitHubCIWorkflowError + import claude_code.server.control_plane as control_plane_mod + + class _FailingWorkflowService: + def run_workflow(self, **kwargs): + raise GitHubCIWorkflowError( + "network down", + code="network_failure", + status_code=503, + details={"reason": "dns"}, + step="pr", + ) + + monkeypatch.setattr(control_plane_mod, "GitHubCIWorkflowService", _FailingWorkflowService) + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + with pytest.raises(DaemonResponseError) as exc_info: + client.run_github_ci_workflow( + issue_reference="#125", + push_remote=True, + create_pull_request=True, + ) + + assert exc_info.value.status_code == 503 + assert exc_info.value.code == "network_failure" + + +def test_control_plane_org_policy_audit_endpoint(tmp_path: Path): + runtime, _ = _build_runtime(tmp_path) + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + evaluate = client.evaluate_org_policy( + tool_name="write", + operation="repo:edit", + payload={"content": "hello", "token": "sk-secret"}, + actor="dev-bot", + policies=[ + { + "id": "approve-write", + "effect": "require_approval", + "tool_pattern": "write", + "operation_pattern": "*", + "priority": 10, + } + ], + create_approval=True, + ) + decision = evaluate["policy_decision"] + assert decision["allowed"] is False + assert decision["decision"] == "approval_pending" + approval_id = decision["approval"]["approval_id"] + assert decision["payload_redacted"]["token"] == "***REDACTED***" + + listed = client.list_org_approvals(status="pending", limit=10) + assert listed["approvals"] + assert listed["approvals"][0]["approval_id"] == approval_id + + decided = client.decide_org_approval( + approval_id=approval_id, + decision="approve", + decided_by="sec-admin", + reason="approved for release", + ) + assert decided["approval"]["status"] == "approved" + + allowed = client.evaluate_org_policy( + tool_name="write", + operation="repo:edit", + payload={"content": "hello", "token": "sk-secret"}, + actor="dev-bot", + create_approval=True, + ) + assert allowed["policy_decision"]["allowed"] is True + assert allowed["policy_decision"]["decision"] == "allowed_by_approval" + + audit_events = client.list_org_audit_events( + event_type="org_policy.evaluated", + limit=20, + ) + assert audit_events["events"] + + report = client.get_org_policy_report(limit=50) + summary = report["report"]["summary"] + assert summary["rule_count"] >= 1 + assert summary["approval_total"] >= 1 + + +def test_control_plane_org_policy_audit_failure_maps_error(tmp_path: Path): + runtime, _ = _build_runtime(tmp_path) + + with _running_daemon(runtime) as daemon: + client = ControlPlaneClient(daemon.base_url, timeout_seconds=3.0) + + with pytest.raises(DaemonResponseError) as missing_exc: + client.decide_org_approval( + approval_id="apr-missing", + decision="approve", + decided_by="sec-admin", + ) + assert missing_exc.value.status_code == 404 + assert missing_exc.value.code == "approval_not_found" + + with pytest.raises(DaemonResponseError) as validation_exc: + client.evaluate_org_policy( + tool_name="write", + operation="repo:edit", + payload={"a": 1}, + policies=[ + { + "id": "bad-rule", + "effect": "unsupported", + "tool_pattern": "write", + } + ], + ) + assert validation_exc.value.status_code == 400 + assert validation_exc.value.code == "validation_error" diff --git a/tests/test_services_mcp_runtime.py b/tests/test_services_mcp_runtime.py new file mode 100644 index 0000000..b36b36c --- /dev/null +++ b/tests/test_services_mcp_runtime.py @@ -0,0 +1,114 @@ +"""Runtime tests for MCP protocol/client minimal flow.""" + +from __future__ import annotations + +import asyncio + +import pytest + +from claude_code.services.mcp.client import ( + MCPClient, + MCPConnectionConfig, + MCPConnectionState, + MCPProtocol, + MCPTransport, + MCPTransportType, +) + + +@pytest.mark.asyncio +async def test_mcp_protocol_send_request_uses_sender_and_resolves_response(): + protocol = MCPProtocol() + + async def _sender(message): + protocol.handle_response( + { + "jsonrpc": "2.0", + "id": message["id"], + "result": {"ok": True, "method": message["method"]}, + } + ) + + protocol.set_sender(_sender) + result = await protocol.send_request("ping") + + assert result == {"ok": True, "method": "ping"} + + +class _FakeTransport(MCPTransport): + def __init__(self) -> None: + self._queue: asyncio.Queue = asyncio.Queue() + self.connected = False + self.disconnected = False + + async def connect(self) -> None: + self.connected = True + + async def disconnect(self) -> None: + self.disconnected = True + + async def send(self, message: dict) -> dict: + method = message.get("method") + if method == "initialize": + return {"jsonrpc": "2.0", "id": message["id"], "result": {"capabilities": {}}} + if method == "tools/list": + return { + "jsonrpc": "2.0", + "id": message["id"], + "result": {"tools": [{"name": "demo", "description": "d", "inputSchema": {}}]}, + } + if method == "resources/list": + return { + "jsonrpc": "2.0", + "id": message["id"], + "result": {"resources": [{"uri": "file://a", "name": "a"}]}, + } + if method == "tools/call": + return { + "jsonrpc": "2.0", + "id": message["id"], + "result": {"ok": True}, + } + if method == "resources/read": + return { + "jsonrpc": "2.0", + "id": message["id"], + "result": {"contents": [{"text": "abc"}]}, + } + return {"jsonrpc": "2.0", "id": message["id"], "result": {}} + + async def receive(self) -> dict: + return await self._queue.get() + + async def send_request(self, method: str, params: dict | None = None) -> dict: + raise RuntimeError("unused in this test") + + +@pytest.mark.asyncio +async def test_mcp_client_connects_and_loads_tools_with_transport_sender(monkeypatch): + config = MCPConnectionConfig( + server_name="demo", + command="unused", + transport=MCPTransportType.STDIO, + ) + client = MCPClient(config) + fake_transport = _FakeTransport() + monkeypatch.setattr(client, "_create_transport", lambda: fake_transport) + + await client.connect() + try: + assert client.state == MCPConnectionState.CONNECTED + assert len(client.tools) == 1 + assert client.tools[0].name == "demo" + assert len(client.resources) == 1 + + tool_result = await client.call_tool("demo", {"x": 1}) + assert tool_result == {"ok": True} + resource_result = await client.read_resource("file://a") + assert resource_result == {"contents": [{"text": "abc"}]} + finally: + await client.disconnect() + + assert fake_transport.connected + assert fake_transport.disconnected + diff --git a/tests/test_session_history_memory_runtime.py b/tests/test_session_history_memory_runtime.py new file mode 100644 index 0000000..6fc8db0 --- /dev/null +++ b/tests/test_session_history_memory_runtime.py @@ -0,0 +1,132 @@ +"""Runtime tests for session/history/memory boundary contracts.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from claude_code.engine.query import QueryConfig, QueryEngine +from claude_code.engine.session import SessionManager +from claude_code.services.history_manager import HistoryManager +from claude_code.services.memory_service import SessionMemory + + +class _SingleTurnApiClient: + """Deterministic API client that emits one assistant text response.""" + + async def create_message_streaming(self, messages, options): + yield SimpleNamespace( + type="message", + content={"content": [{"type": "text", "text": "ack"}]}, + usage=None, + error=None, + ) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "ack"}]) + + +@pytest.mark.asyncio +async def test_query_engine_persists_live_messages_only_to_session(tmp_path): + session_manager = SessionManager(storage_dir=tmp_path / "sessions") + history_manager = HistoryManager(storage_path=tmp_path / "history.json") + memory = SessionMemory(storage_dir=tmp_path / "memory") + + engine = QueryEngine(api_client=_SingleTurnApiClient(), config=QueryConfig()) + engine.session_manager = session_manager + engine.history_manager = history_manager + engine.memory = memory + + current_session = session_manager.ensure_current_session() + engine.config.session_id = current_session.id + engine.config.working_directory = str(tmp_path) + + async for _ in engine.query("hello"): + pass + + reloaded_session = session_manager.load_session(current_session.id) + assert reloaded_session is not None + assert len(reloaded_session.messages) >= 2 + assert reloaded_session.messages[0].role == "user" + assert reloaded_session.messages[1].role == "assistant" + + assert history_manager.get_entries() == [] + assert await memory.export() == {} + + +@pytest.mark.asyncio +async def test_resume_archives_previous_session_into_history(tmp_path): + session_manager = SessionManager(storage_dir=tmp_path / "sessions") + history_manager = HistoryManager(storage_path=tmp_path / "history.json") + + session_one = session_manager.create_session() + session_one.metadata.working_directory = str(tmp_path / "project-a") + session_one.add_user_message("u1") + session_one.add_assistant_message("a1") + session_one.save() + + session_two = session_manager.create_session() + session_two.metadata.working_directory = str(tmp_path / "project-b") + session_two.add_user_message("u2") + session_two.add_assistant_message("a2") + session_two.save() + + session_manager.switch_session(session_one.id) + + engine = QueryEngine(api_client=_SingleTurnApiClient(), config=QueryConfig()) + engine.session_manager = session_manager + engine.history_manager = history_manager + engine.config.session_id = session_one.id + engine.messages = [] + + resumed = await engine.resume_session(session_two.id) + + assert resumed is True + assert engine.config.session_id == session_two.id + assert len(engine.messages) == len(session_two.messages) + + archived = history_manager.get_session_entries(session_one.id) + assert len(archived) == len(session_one.messages) + assert all(entry.metadata.get("session_id") == session_one.id for entry in archived) + + +def test_history_archive_is_idempotent_for_same_session_messages(tmp_path): + history_manager = HistoryManager(storage_path=tmp_path / "history.json") + messages = [ + { + "id": "m1", + "role": "user", + "content": "hello", + "timestamp": 1.0, + "metadata": {}, + }, + { + "id": "m2", + "role": "assistant", + "content": "world", + "timestamp": 2.0, + "metadata": {}, + }, + ] + + first = history_manager.archive_session_messages("s-1", messages) + second = history_manager.archive_session_messages("s-1", messages) + + assert first == 2 + assert second == 0 + assert len(history_manager.get_session_entries("s-1")) == 2 + + +@pytest.mark.asyncio +async def test_memory_knowledge_api_is_separate_from_session_history(tmp_path): + memory = SessionMemory(storage_dir=tmp_path / "memory") + + await memory.set_knowledge( + "project:architecture", + {"decision": "use sqlite"}, + ) + keys = await memory.list_knowledge_keys() + + assert keys == ["project:architecture"] + assert await memory.get("project:architecture") == {"decision": "use sqlite"} diff --git a/tests/test_sqlite_runtime_state_backend.py b/tests/test_sqlite_runtime_state_backend.py new file mode 100644 index 0000000..7daff85 --- /dev/null +++ b/tests/test_sqlite_runtime_state_backend.py @@ -0,0 +1,193 @@ +"""Runtime tests for P1-03 SQLite state backend contracts.""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path +from types import SimpleNamespace + +import pytest + +import claude_code.main as main_mod +from claude_code.engine.query import QueryConfig, QueryEngine +from claude_code.engine.session import SessionManager, SQLiteSessionStore +from claude_code.services.event_journal import ( + EVENT_JOURNAL_SCHEMA_KEY, + EVENT_JOURNAL_SCHEMA_VERSION, + SQLiteEventJournal, +) +from claude_code.tasks.factory import create_runtime_repository +from claude_code.tasks.repository import ( + RUNTIME_TASKS_SCHEMA_KEY, + RUNTIME_TASKS_SCHEMA_VERSION, + SQLiteRuntimeTaskRepository, +) +from claude_code.tasks.types import BashTask, TaskResult, TaskStatus +from claude_code.tools.registry import ToolRegistry + + +class _SingleTurnApiClient: + async def create_message_streaming(self, messages, options): + yield SimpleNamespace( + type="message", + content={"content": [{"type": "text", "text": "ack"}]}, + usage=None, + error=None, + ) + + async def create_message(self, messages, options): + return SimpleNamespace(content=[{"type": "text", "text": "ack"}]) + + +def test_sqlite_runtime_repository_init_and_crud_contract(tmp_path: Path): + repo = create_runtime_repository(tmp_path, "sqlite") + assert isinstance(repo, SQLiteRuntimeTaskRepository) + + task = BashTask(id="task-sqlite-crud", command="echo sqlite", status=TaskStatus.COMPLETED) + task.result = TaskResult(code=0, stdout="ok", stderr="") + repo.upsert_task(task) + + loaded = repo.get_task_record(task.id) + assert loaded is not None + assert loaded["status"] == "completed" + assert loaded["result"]["stdout"] == "ok" + + listed = repo.list_task_records() + assert any(item["id"] == task.id for item in listed) + + +def test_sqlite_runtime_repository_schema_version_guard(tmp_path: Path): + repo = create_runtime_repository(tmp_path, "sqlite") + assert isinstance(repo, SQLiteRuntimeTaskRepository) + + with sqlite3.connect(repo.db_path, timeout=5.0) as conn: + conn.execute( + "UPDATE runtime_tasks_metadata SET value = ? WHERE key = ?", + (str(RUNTIME_TASKS_SCHEMA_VERSION + 1), RUNTIME_TASKS_SCHEMA_KEY), + ) + + with pytest.raises(RuntimeError, match="newer than this binary"): + create_runtime_repository(tmp_path, "sqlite") + + +def test_sqlite_event_journal_schema_version_guard(tmp_path: Path): + db_path = tmp_path / "runtime_state.db" + journal = SQLiteEventJournal(db_path) + journal.append_event(event_type="query.started", payload={"input": "x"}) + + with sqlite3.connect(db_path, timeout=5.0) as conn: + conn.execute( + "UPDATE journal_metadata SET value = ? WHERE key = ?", + (str(EVENT_JOURNAL_SCHEMA_VERSION + 1), EVENT_JOURNAL_SCHEMA_KEY), + ) + + with pytest.raises(RuntimeError, match="newer than this binary"): + SQLiteEventJournal(db_path) + + +def test_sqlite_session_store_persist_and_load(tmp_path: Path): + db_path = tmp_path / "runtime_state.db" + session_store = SQLiteSessionStore(db_path) + manager = SessionManager.from_store(session_store) + + session = manager.create_session() + session.add_user_message("hello") + session.add_assistant_message("world") + session.save() + + reloaded = manager.load_session(session.id) + assert reloaded is not None + assert len(reloaded.messages) == 2 + assert reloaded.messages[0].role == "user" + assert reloaded.messages[1].role == "assistant" + + +@pytest.mark.asyncio +async def test_query_chain_with_sqlite_event_journal_and_session_store(tmp_path: Path): + db_path = tmp_path / "runtime_state.db" + session_store = SQLiteSessionStore(db_path) + manager = SessionManager.from_store(session_store) + session = manager.create_session() + + journal = SQLiteEventJournal(db_path) + engine = QueryEngine( + api_client=_SingleTurnApiClient(), + config=QueryConfig(session_id=session.id, working_directory=str(tmp_path)), + tool_registry=ToolRegistry(lazy=False), + ) + engine.session_manager = manager + engine.event_journal = journal + + async for _ in engine.query("hello sqlite"): + pass + + queried = journal.query_events(session_id=session.id) + event_types = [item.event_type for item in queried] + assert "query.started" in event_types + assert "message.user" in event_types + assert "message.assistant" in event_types + assert "query.completed" in event_types + + +def test_create_runtime_wires_sqlite_state_backend(monkeypatch): + class _FakeSession: + def __init__(self): + self.id = "session-xyz" + self.metadata = type( + "_Metadata", + (), + {"working_directory": "D:/init", "model": "claude-init"}, + )() + + class _FakeSessionManager: + def __init__(self): + self.session = _FakeSession() + + @classmethod + def from_store(cls, store): + return cls() + + def ensure_current_session(self): + return self.session + + class _FakeQueryEngine: + def __init__(self, api_client, config, tool_registry): + self.api_client = api_client + self.config = config + self.tool_registry = tool_registry + + captured = {} + fake_api = object() + fake_registry = object() + fake_history = object() + fake_task_manager = object() + fake_hooks = object() + fake_memory = object() + fake_event_journal = object() + fake_application = object() + + monkeypatch.setattr(main_mod, "get_config", lambda: main_mod.Config(model="claude-runtime")) + monkeypatch.setattr(main_mod, "setup_api_client", lambda app_config=None: fake_api) + monkeypatch.setattr(main_mod, "create_default_registry", lambda: fake_registry) + monkeypatch.setattr(main_mod, "QueryEngine", _FakeQueryEngine) + monkeypatch.setattr(main_mod, "SessionManager", _FakeSessionManager) + monkeypatch.setattr(main_mod, "SQLiteSessionStore", lambda db_path=None: object()) + monkeypatch.setattr(main_mod, "HistoryManager", lambda storage_path=None: fake_history) + monkeypatch.setattr(main_mod, "SQLiteEventJournal", lambda db_path=None: fake_event_journal) + + def _fake_task_manager_factory(config, event_journal): + captured["runtime_backend"] = config.runtime_backend + captured["event_journal"] = event_journal + return fake_task_manager + + monkeypatch.setattr(main_mod, "create_task_manager_with_event_journal", _fake_task_manager_factory) + monkeypatch.setattr(main_mod, "HooksManager", lambda config_path=None: fake_hooks) + monkeypatch.setattr(main_mod, "get_memory", lambda: fake_memory) + monkeypatch.setattr(main_mod, "_create_application", lambda app_config: fake_application) + + runtime = main_mod.create_runtime(model="claude-custom", working_dir="D:/runtime") + + assert runtime.event_journal is fake_event_journal + assert runtime.query_engine.event_journal is fake_event_journal + assert captured["runtime_backend"] == "sqlite" + assert captured["event_journal"] is fake_event_journal diff --git a/tests/test_tasks_backend_contract.py b/tests/test_tasks_backend_contract.py index 0ec57d2..ca3b942 100644 --- a/tests/test_tasks_backend_contract.py +++ b/tests/test_tasks_backend_contract.py @@ -10,7 +10,12 @@ import pytest from claude_code.tasks.queue import InMemoryTaskQueue, QueueItem -from claude_code.tasks.repository import create_file_runtime_task_repository +from claude_code.tasks.repository import ( + RUNTIME_TASKS_SCHEMA_KEY, + RUNTIME_TASKS_SCHEMA_VERSION, + create_file_runtime_task_repository, + create_sqlite_runtime_task_repository, +) from claude_code.tasks.types import BashTask, TaskResult, TaskStatus @@ -77,3 +82,43 @@ def test_runtime_repository_contract_upsert_get_delete(): repo.delete_task(task.id) assert repo.get_task_record(task.id) is None + + +def test_sqlite_runtime_repository_contract_upsert_get_delete(): + with _temp_runtime_workdir() as workdir: + repo = create_sqlite_runtime_task_repository(workdir) + + task = BashTask( + id="task-sqlite-123", + command="echo sqlite", + status=TaskStatus.COMPLETED, + ) + task.result = TaskResult(code=0, stdout="ok-sqlite", stderr="") + + repo.upsert_task(task) + record = repo.get_task_record(task.id) + assert record is not None + assert record["id"] == "task-sqlite-123" + assert record["status"] == "completed" + assert record["result"]["stdout"] == "ok-sqlite" + + repo.delete_task(task.id) + assert repo.get_task_record(task.id) is None + + +def test_sqlite_runtime_repository_schema_version_guard(tmp_path: Path): + workdir = tmp_path / "runtime-schema-guard" + workdir.mkdir(parents=True, exist_ok=True) + repo = create_sqlite_runtime_task_repository(workdir) + db_path = repo.db_path + + import sqlite3 + + with sqlite3.connect(db_path, timeout=5.0) as conn: + conn.execute( + "UPDATE runtime_tasks_metadata SET value = ? WHERE key = ?", + (str(RUNTIME_TASKS_SCHEMA_VERSION + 1), RUNTIME_TASKS_SCHEMA_KEY), + ) + + with pytest.raises(RuntimeError, match="newer than this binary"): + create_sqlite_runtime_task_repository(workdir) diff --git a/tests/test_tasks_factory_runtime.py b/tests/test_tasks_factory_runtime.py index fb37243..27c4945 100644 --- a/tests/test_tasks_factory_runtime.py +++ b/tests/test_tasks_factory_runtime.py @@ -16,6 +16,7 @@ create_task_queue, ) from claude_code.tasks.queue import InMemoryTaskQueue +from claude_code.tasks.repository import SQLiteRuntimeTaskRepository @contextmanager @@ -40,14 +41,15 @@ def test_factory_redis_backend_not_yet_wired(): def test_factory_creates_runtime_repository_and_manager(): with _temp_runtime_workdir() as workdir: - repo = create_runtime_repository(workdir, "file") + repo = create_runtime_repository(workdir, "sqlite") assert repo is not None + assert isinstance(repo, SQLiteRuntimeTaskRepository) manager = create_task_manager( TaskBackendConfig( working_directory=workdir, queue_backend="memory", - runtime_backend="file", + runtime_backend="sqlite", ) ) stats = manager.get_stats() diff --git a/tests/test_tasks_manager_runtime.py b/tests/test_tasks_manager_runtime.py index 2225112..34fe4ac 100644 --- a/tests/test_tasks_manager_runtime.py +++ b/tests/test_tasks_manager_runtime.py @@ -13,7 +13,10 @@ from claude_code.tasks.manager import TaskManager, TaskStateTransitionError from claude_code.tasks.queue import InMemoryTaskQueue -from claude_code.tasks.repository import create_file_runtime_task_repository +from claude_code.tasks.repository import ( + create_file_runtime_task_repository, + create_sqlite_runtime_task_repository, +) from claude_code.tasks.types import TaskResult, TaskStatus, BashTask, create_task_from_dict @@ -201,6 +204,36 @@ async def _ok_executor(_task): assert runtime_repo.get_task_record(task.id) is None +@pytest.mark.asyncio +async def test_sqlite_runtime_repository_persists_task_lifecycle(): + with _temp_runtime_workdir() as workdir: + runtime_repo = create_sqlite_runtime_task_repository(workdir) + manager = TaskManager( + task_queue=InMemoryTaskQueue(), + runtime_repository=runtime_repo, + ) + + task = await manager.create_bash_task(command="echo persist sqlite") + pending_record = runtime_repo.get_task_record(task.id) + assert pending_record is not None + assert pending_record["status"] == "pending" + + async def _ok_executor(_task): + return TaskResult(code=0, stdout="persisted-sqlite") + + await manager.start_task(task.id, executor=_ok_executor) + await manager.wait_for_task(task.id, timeout=1.0) + + completed_record = runtime_repo.get_task_record(task.id) + assert completed_record is not None + assert completed_record["status"] == "completed" + assert completed_record["result"]["stdout"] == "persisted-sqlite" + + cleared = manager.clear_completed() + assert cleared == 1 + assert runtime_repo.get_task_record(task.id) is None + + def test_create_task_from_dict_handles_bash_description(): payload = { "id": "task-1", @@ -247,3 +280,37 @@ async def test_manager_recovers_inflight_task_as_failed(): record = runtime_repo.get_task_record(task.id) assert record is not None assert record["status"] == "failed" + + +@pytest.mark.asyncio +async def test_manager_recovers_inflight_task_as_failed_with_sqlite_repo(): + with _temp_runtime_workdir() as workdir: + runtime_repo = create_sqlite_runtime_task_repository(workdir) + seed_manager = TaskManager( + task_queue=InMemoryTaskQueue(), + runtime_repository=runtime_repo, + ) + + task = await seed_manager.create_bash_task( + command="echo recover sqlite", + idempotency_key="recover-sqlite-key", + ) + task.status = TaskStatus.RUNNING + task.started_at = datetime.now() + runtime_repo.upsert_task(task) + + recovered_manager = TaskManager( + task_queue=InMemoryTaskQueue(), + runtime_repository=runtime_repo, + ) + + recovered = recovered_manager.get_task(task.id) + assert recovered is not None + assert recovered.status == TaskStatus.FAILED + assert "recovered after process restart" in (recovered.error or "") + assert recovered_manager.get_stats()["recovered"] >= 1 + assert recovered_manager.get_stats()["idempotency_keys"] == 1 + + record = runtime_repo.get_task_record(task.id) + assert record is not None + assert record["status"] == "failed" diff --git a/tests/test_tasks_middleware_runtime.py b/tests/test_tasks_middleware_runtime.py index bfb2c34..5ab98f1 100644 --- a/tests/test_tasks_middleware_runtime.py +++ b/tests/test_tasks_middleware_runtime.py @@ -19,6 +19,7 @@ from claude_code.tasks.types import TaskResult, TaskStatus from claude_code.tools.base import ToolContext from claude_code.tools.control import TaskControlListTool, TaskOutputTool, TaskStopTool +from claude_code.tools.cron import CronDeleteTool, CronListTool, ScheduleCronTool from claude_code.tools.cron.create import CronCreateTool, _SESSION_ONLY_TASKS from claude_code.tools.workflow import TaskListTool, TaskUpdateTool from claude_code.utils.task_store import load_tasks, mutate_tasks @@ -148,6 +149,32 @@ async def test_cron_create_honors_durable_flag(): assert len(persisted) == 1 +@pytest.mark.asyncio +async def test_schedule_tools_share_scheduled_tasks_storage(): + with _temp_workdir() as workdir: + _SESSION_ONLY_TASKS.clear() + context = _tool_context(workdir) + schedule_file = workdir / ".claude" / "scheduled_tasks.json" + + create_result = await ScheduleCronTool().execute( + {"command": "echo hi", "schedule": "0 * * * *", "name": "hourly"}, + context, + ) + assert not create_result.is_error + assert schedule_file.exists() + + list_result = await CronListTool().execute({}, context) + assert not list_result.is_error + assert "hourly" in str(list_result.content) + assert "Durable: True" in str(list_result.content) + + delete_result = await CronDeleteTool().execute({"name": "hourly"}, context) + assert not delete_result.is_error + + payload = json.loads(schedule_file.read_text(encoding="utf-8")) + assert "hourly" not in payload + + @pytest.mark.asyncio async def test_tasks_command_uses_unified_task_schema(): with _temp_workdir() as workdir: diff --git a/tests/test_tools_registry_runtime.py b/tests/test_tools_registry_runtime.py new file mode 100644 index 0000000..f798038 --- /dev/null +++ b/tests/test_tools_registry_runtime.py @@ -0,0 +1,41 @@ +"""Runtime regression tests for the default tool registry.""" + +from __future__ import annotations + +import claude_code.tools.registry as registry_mod + + +def test_create_default_registry_is_module_lazy(monkeypatch): + imported_modules: list[str] = [] + real_import = registry_mod.importlib.import_module + + def _tracked_import(module_name: str, package: str | None = None): + imported_modules.append(module_name) + return real_import(module_name, package) + + monkeypatch.setattr(registry_mod.importlib, "import_module", _tracked_import) + + registry = registry_mod.create_default_registry() + assert imported_modules == [] + + bash_tool = registry.get("bash") + assert bash_tool is not None + assert bash_tool.name == "bash" + assert "claude_code.tools.builtin.bash" in imported_modules + + +def test_create_default_registry_includes_expected_tool_names(): + registry = registry_mod.create_default_registry() + names = registry.get_names() + + assert "bash" in names + assert "cron_create" in names + assert "lsp" in names + + +def test_default_registry_resolves_lsp_tool(): + registry = registry_mod.create_default_registry() + lsp_tool = registry.get("lsp") + + assert lsp_tool is not None + assert lsp_tool.name == "lsp"