升级 Trellis 并完成项目审查(07-26-trellis-upgrade-project-audit)

这个提交包含在:
2026-07-26 18:10:49 +08:00
父节点 b3dadfa1bf
当前提交 f223f559ad
共修改 77 个文件,包含 5850 行新增和 1327 行删除
+65 -51
查看文件
@@ -1,77 +1,91 @@
{
"__version": 2,
"hashes": {
".agents/skills/trellis-continue/SKILL.md": "aba3e18dc4a4d893ab9b3e3bb830acd111c72aacf319a059955ef9e3097e1117",
".agents/skills/trellis-continue/SKILL.md": "7723ccf49fbf19d8f086cacc7a080bd8be8db6fc70a32908b80f68efa318d7bf",
".agents/skills/trellis-finish-work/SKILL.md": "161060fbcd44f787440d3a5c297a9f5223ea7774bb3021a50e376875a9ac5b2d",
".agents/skills/trellis-before-dev/SKILL.md": "208ad3fd5131fa0da603d4bc354a29826967397f5aeef483fa0564113df13e9e",
".agents/skills/trellis-brainstorm/SKILL.md": "babf6e6e4d0a59d6b7284ff09364d224a3dc299a98911f3a32d0bbcf2c0c21f2",
".agents/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f",
".agents/skills/trellis-check/SKILL.md": "a3f17aef687aa3b475d12ee64c3293e5491bb7474336be2c0f9ec22042f13b6e",
".agents/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd",
".agents/skills/trellis-brainstorm/SKILL.md": "a0f226ddcb8a3e846acd2a35d121996e9ca55165ce76202095d0b65e2b48a5e8",
".agents/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde",
".agents/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282",
".agents/skills/trellis-update-spec/SKILL.md": "003ce08a3404aeb50998029392c4d4e57b626edf526d3ebd585032bb92dcbb96",
".agents/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5",
".agents/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c",
".agents/skills/trellis-meta/references/customize-local/change-context-loading.md": "c7be53e038eac99ff4d0abb3fafc67cfdfd90352744ce09ad11e7ef5085cf933",
".agents/skills/trellis-meta/references/customize-local/change-hooks.md": "91892f2cff53ae003736007e95172945acabf48b2fc889bd627cd2406ce449c4",
".agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea",
".agents/skills/trellis-meta/references/customize-local/change-spec-structure.md": "a3fc9da294448b4a3525ae1ab7c8c9b895ef8f3b53bcf37a13ce6afbdc040fe6",
".agents/skills/trellis-meta/references/customize-local/change-agents.md": "4216ac3cc570038fd8ce3319a85932bb537fea9e0a7f4d11fa315cfa645c9c85",
".agents/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf",
".agents/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358",
".agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "5b942e9f512e049a75e8dd9e8cc4d49786f6da61b22afc82bb447acf808f9fe2",
".agents/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841",
".agents/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d",
".agents/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb",
".agents/skills/trellis-meta/references/customize-local/overview.md": "1a406c24b4c5737cf517ead5ecb0846c20b0648a117008d3f5a47614fc1793ca",
".agents/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375",
".agents/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f",
".agents/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f",
".agents/skills/trellis-meta/references/local-architecture/spec-system.md": "8996504201e2a5460516948fe29c4ce7ed6b960f15da8c514855d2d467046985",
".agents/skills/trellis-meta/references/local-architecture/task-system.md": "a6f905e53fe5a7fa5be8a4bbe1d102bd90b69c6e63a1a3be51ac3f7f164f1d21",
".agents/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519",
".agents/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4",
".agents/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084",
".agents/skills/trellis-meta/references/local-architecture/generated-files.md": "7eb2d452eddb4f4226f7578c2ec6d5ee0434ed172ba4c36107cc8bdff7554dc6",
".agents/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6",
".agents/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47",
".agents/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a",
".agents/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6",
".agents/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd",
".agents/skills/trellis-meta/references/platform-files/agents.md": "ffee78fc3c29114c7409ca4805ebd51d44d8a6acb630e9f73d47013e10db5ac5",
".agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462",
".agents/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d",
".agents/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212",
".agents/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f",
".agents/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34",
".agents/skills/trellis-spec-bootstarp/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e",
".agents/skills/trellis-spec-bootstarp/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6",
".agents/skills/trellis-spec-bootstarp/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d",
".agents/skills/trellis-spec-bootstarp/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d",
".agents/skills/trellis-spec-bootstarp/SKILL.md": "81f400092b21392161e7d9dfa9111c9be36c81bc8641d252ed28e08373449ac0",
".agents/skills/trellis-start/SKILL.md": "a88faaac9fe1b9fa7441473a72ff88ead0022504eeb38492ecef85db75a559e7",
".codex/agents/trellis-check.toml": "e6781803094ef836869b68fb00b28f0785e9f97091affb5e5bd7b13ab406d6c6",
".codex/agents/trellis-implement.toml": "b84884c8fe46ecc032ddb287d7aac4ac9553a7c49ab7b6a40ae44a08f6725b58",
".codex/agents/trellis-research.toml": "5492f7f6ab8bdea975b0e853bf171b050f7ddf6c2079ac770ed912c48d815eae",
".codex/hooks/session-start.py": "bc3bd73ad89fc872c14efc8a548f4d7ff0b0afd4bb667e9db49fe468dcafd326",
".codex/hooks/inject-workflow-state.py": "fe4cca4db7ca8c252f614efca16fa588009a4238542a2c9aa85167409f6f9a4c",
".codex/hooks.json": "522ba3c488c100027783e52ecff84c0bd799852dd77ad3f1936e86db105f01d6",
".codex/config.toml": "4224eb7df6802a623cb1bee522aed0a23ba6be862b90f1b597a313fc16864b06",
".agents/skills/trellis-meta/references/platform-files/agents.md": "9f41349b78f7ae64698a38490a317882561f3805b26a79bda587a13d03fda245",
".agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "154af08f7ee8afe7a704968ec0b9fc21e905b7cfb03871c9dbafcdd6cc654319",
".agents/skills/trellis-meta/references/platform-files/overview.md": "1aec9087ccedd56a213af877b5db474131aa4b4789b6ac0e8da73b058aa43bd4",
".agents/skills/trellis-meta/references/platform-files/platform-map.md": "9e476e500f10b2a1a05278dd700837deb731a77e3f3993e90f8102528a9bdcca",
".agents/skills/trellis-meta/references/platform-files/skills-and-commands.md": "e39831d860bd27a04e7757f7bd941ab83b3c5b11ad4460cec03975b655f26cc3",
".agents/skills/trellis-meta/SKILL.md": "208e92ace3b8d979d8158b3bd0169185f508f2e1424fcd78fcd03cbcc39516a1",
".agents/skills/trellis-start/SKILL.md": "79a5ba7a2aff3c72e06d7f4cd6942dc4f4f4092dd40f9c8e94f1838024a81e4d",
".codex/agents/trellis-check.toml": "42d774d175dab732872e1056aa70005b6a1693d93fecfde6b9c86ecc50bd7b9e",
".codex/agents/trellis-implement.toml": "c21eb1a671ebb8afe476ad54ae9ae1e33e4e97750bfdc4eabecc772a4d4e777b",
".codex/agents/trellis-research.toml": "200b9fca30a68e027d2e8b2b4c785fafccb9aa75853e70abd92d2d07d7cba5d1",
".codex/hooks/session-start.py": "14de3be1cf6eb9c9feba348d8998b407f3837d6c0756b74210c9200543440677",
".codex/hooks/inject-workflow-state.py": "9ce43910ac39cbb0e4d1783fbde931761eb04536c7f82661a96d95ea72c14bde",
".codex/hooks.json": "85a58ba7cdf1e19e7f75ddcc64e5680180c487ca266a74bd5005f31abeee2e02",
".codex/config.toml": "9f2d20e28f0bc9c886312eca3ad3bba41533ef4615aaaafe25e98152302267bb",
".trellis/config.yaml": "5c9207418cecc390e9d86d589b4183b831f76697d92fb42fefd5221cd8772e51",
".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c",
".trellis/scripts/add_session.py": "6e406a0a9f32d4a50b1b5ca8115cbd06c359011f0e166c41dc5fab34698a4006",
".trellis/scripts/add_session.py": "876dad478edf70db59acccaae9cb4db646a155681f730bd99af48de72ddc9881",
".trellis/scripts/common/__init__.py": "3d5e9347141f0296319a5beb29d69ae714c5a474b9078caeb3edd7c5f6562e22",
".trellis/scripts/common/active_task.py": "6c88ed40ef7289bca0f6d2ecba0f8b8aef46cd58788080fbeeea88de138a431f",
".trellis/scripts/common/cli_adapter.py": "cd844d1e84b1a09b373b3a7609e4d5606ee9d4825154c002cc9bb3f54c8e2fb9",
".trellis/scripts/common/config.py": "25c5a53ad20d6909be5209222e4208a84528805316a4d78350529459a364edb1",
".trellis/scripts/common/active_task.py": "3fcafde57bdc4442940081d1617295e0f4f8e620d0d4fbb97d33394e8679a39c",
".trellis/scripts/common/cli_adapter.py": "5d6bd9d6f5c631e7e792db7dd343351317f9643bc87b73a9a98abd51cefb4307",
".trellis/scripts/common/config.py": "8d2e5f8ccfcd5f622cd2af002aa761f3d3ffcc653182fefb2268afd102e77bca",
".trellis/scripts/common/developer.py": "f5f833123abe68890171b4da825a324216d24913f6b5ad9245afc556424ffd7b",
".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6",
".trellis/scripts/common/git.py": "ea9ad7686bae598b04e41281ca938f83c06d8021716332947ea51339e7cb1aa9",
".trellis/scripts/common/git_context.py": "fa30ced454f1a91ffc9f8b2abeb32225e3447cbdc90bad783797374eba07265d",
".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8",
".trellis/scripts/common/io.py": "75648caae03d5b1107d7aeccaa785d133b25762266e54a520d90ca8c76b43bdb",
".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf",
".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7",
".trellis/scripts/common/paths.py": "05898ef136cc7c4d861b05fbf2b16d53ddd3e6f311a231d4fcfcb81bde7c45ee",
".trellis/scripts/common/safe_commit.py": "8789bff4b30a9065469210f2efab3f59f03dddd77bef4e4b6a5bb641f93539f4",
".trellis/scripts/common/session_context.py": "c91cf213025e5e4ad2f56bb6f570ef43d35430fc8a863be311d3e5f8f32ef652",
".trellis/scripts/common/task_context.py": "1c16a7fa82d363010d0d0ebdc038296ae1552bf6e90214787d707f49567bc159",
".trellis/scripts/common/safe_commit.py": "baa5c82324eb62154374ec63394ecdc8609bb37d93892e3bcb88f452bb7d6446",
".trellis/scripts/common/session_context.py": "19927c62d3fd70fecf70da0c5b25c6c81e63683f8a594a1ae3b5d6c115070368",
".trellis/scripts/common/task_context.py": "51301e180ce702c6a5b1a2dd1b155a6eed1bf2b05cb2ac1e51081212628b11d4",
".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5",
".trellis/scripts/common/task_store.py": "cca4851c948df4a7d8dac38c465dce0d6b15a171001adab8dfef011ad66e02e5",
".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e",
".trellis/scripts/common/task_store.py": "e3c2fbf8b79b591e39fc3c9f4e2f3ee0c840c8201c94a16709ec743fa45037f6",
".trellis/scripts/common/task_utils.py": "90c0a6d50bad502c3f01cb24c1ccfeb0eece5e2c69efaff8d65eb827fba43871",
".trellis/scripts/common/tasks.py": "4436a8b0b53c270a35989e26d9dbd92669408c6562d88c02083a404562da85fe",
".trellis/scripts/common/trellis_config.py": "0839dcf90ebbd77712c276930a89335b3313927051650c91d220fb51ca2a6a3c",
".trellis/scripts/common/trellis_config.py": "e282e897183e3ec2f4e6e56349431946e5f98c1c31d3eca4de7fc44e1383a7bf",
".trellis/scripts/common/types.py": "9962081cc2608fb9d1deb32c6880e336f62cdca6b338e7ae813304701e155ee9",
".trellis/scripts/common/workflow_phase.py": "2b260f4a7770e9c3223129836716bd8e2c0f0568acd682224a57415bc1dc726b",
".trellis/scripts/common/workflow_phase.py": "79ee522de20246acf1e2c222e8ad180ad25aaec7fec98214a93d9e81b350d9a8",
".trellis/scripts/get_context.py": "ca5bf9e90bdb1d75d3de182b95f820f9d108ab28793d29097b24fd71315adcf5",
".trellis/scripts/get_developer.py": "84c27076323c3e0f2c9c8ed16e8aa865e225d902a187c37e20ee1a46e7142d8f",
".trellis/scripts/hooks/linear_sync.py": "e09cc4ce4699aada908808718698f33f705a3edf55c4dcf8f777ad892f80ca79",
".trellis/scripts/init_developer.py": "f9e6c0d882406e81c8cd6b1c5abb204b0befc0069ff89cf650cd536a80f8c60e",
".trellis/scripts/task.py": "40abdd46f5c2b6837610429a38eef50f1fc783fb1852dc4f52a891205e42ab04",
".trellis/workflow.md": "f4b8a6f89017f62071986d6d36cc32c4c7f01fe6f023f0fd9311eddc57d8c94f"
".trellis/scripts/task.py": "e0ffed9f14994069f0c992141e3ec168524be5af32e3681e6ea30ba0a5da4bc4",
".trellis/workflow.md": "f4b8a6f89017f62071986d6d36cc32c4c7f01fe6f023f0fd9311eddc57d8c94f",
".trellis/agents/implement.md": "66e25ad046c94869442834bc3cdfbd5a9a7412d3ff54561d64d2886552c27e87",
".trellis/agents/check.md": "edb4f57361407249a53bf5998ebf91c40d2b969e826a2c5e1b4e813a08bcb175",
".agents/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e",
".agents/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4",
".agents/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd",
".agents/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7",
".agents/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4",
".agents/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3",
".agents/skills/trellis-meta/references/local-architecture/bundled-skills.md": "7a8d1a5dcc8d1140c4c6bd19d02949364cdd01801bd0825a975a308cf85b8f37",
".agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9",
".agents/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779",
".agents/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20",
".agents/skills/trellis-session-insight/SKILL.md": "d20f1d20c6946e26ba6f848b9a016d9a16c803d87501462e5b11a2ced6f56643",
".agents/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e",
".agents/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6",
".agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d",
".agents/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d",
".agents/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a",
".codex/hooks/inject-subagent-context.py": "fe7e315b6ef5ca28dafa9a92072e4a07278492ee9cab74d42f0c0b6addf51662"
}
}
+1 -1
查看文件
@@ -1 +1 @@
0.5.19
0.6.9
+70
查看文件
@@ -0,0 +1,70 @@
---
name: check
description: |
Code quality auditor for the Trellis channel runtime. Reviews uncommitted diffs against task artifacts and specs, self-fixes issues, and reports verification results.
provider: claude
labels: [trellis, check]
---
# Check Agent (channel runtime)
You are the Check Agent spawned by `trellis channel spawn --agent check` inside the Trellis channel runtime. You receive an `Active task: <path>` line in your inbox; use it to locate task artifacts on disk.
## Context
Before reviewing, read in this order:
1. `<task-path>/check.jsonl` if present — spec manifest curated for this turn; read every listed file
2. `<task-path>/prd.md` — requirements
3. `<task-path>/design.md` if present — technical design
4. `<task-path>/implement.md` if present — execution plan
5. `.trellis/spec/` — project-wide guidelines (load only what is relevant to the diff under review)
## Core Responsibilities
1. **Get the diff** — `git diff` / `git diff --staged` for uncommitted changes
2. **Review against task artifacts** — does the diff satisfy `prd.md` (and `design.md` / `implement.md` if present)?
3. **Review against specs** — naming, structure, type safety, error handling, conventions in `.trellis/spec/`
4. **Self-fix** — when an issue is mechanical and small, fix it directly with the editing tools you have
5. **Run verification** — project lint and typecheck on the changed scope
6. **Report** — concrete findings with `file:line` citations and what was fixed vs. what is open
## Forbidden Operations
- `git commit`
- `git push`
- `git merge`
The supervising main session owns commits. Report the post-fix state; do not commit on its behalf.
## Workflow
1. Run `git diff --name-only` and `git diff` to scope the changes
2. Read the task artifacts and relevant spec files
3. For each issue:
- If mechanical (lint nit, missing type, wrong import, dead branch) → fix in-place
- If a design/judgment issue → record and report, do not silently rewrite
4. Run the project's lint and typecheck on the changed scope after self-fixes
5. Report
## Report Format
```
## Self-Check Complete
### Files Checked
- <path>
### Issues Found and Fixed
1. `<file>:<line>` — <what was wrong> → <what you changed>
### Issues Not Fixed
- `<file>:<line>` — <issue> — <why deferred to the main session>
### Verification Results
- TypeCheck: <pass|fail|skipped + reason>
- Lint: <pass|fail|skipped + reason>
### Summary
Checked <N> files, found <X> issues, fixed <Y>, <X-Y> open.
```
+71
查看文件
@@ -0,0 +1,71 @@
---
name: implement
description: |
Code implementation expert for the Trellis channel runtime. Understands specs and task artifacts, then implements features. No git commit allowed.
provider: claude
labels: [trellis, implement]
---
# Implement Agent (channel runtime)
You are the Implement Agent spawned by `trellis channel spawn --agent implement` inside the Trellis channel runtime. You receive an `Active task: <path>` line in your inbox; use it to locate task artifacts on disk.
## Context
Before implementing, read in this order:
1. `<task-path>/implement.jsonl` if present — spec manifest curated for this turn; read every listed file
2. `<task-path>/prd.md` — requirements
3. `<task-path>/design.md` if present — technical design
4. `<task-path>/implement.md` if present — execution plan
5. `.trellis/spec/` — project-wide guidelines (load only what is relevant to the diff you are about to write)
## Core Responsibilities
1. **Understand specs** — read relevant spec files in `.trellis/spec/`
2. **Understand task artifacts** — read the artifacts listed above
3. **Implement features** — write code that follows specs and existing patterns
4. **Self-check** — run lint and typecheck on the changed scope before reporting
## Forbidden Operations
- `git commit`
- `git push`
- `git merge`
The supervising main session owns commits. Report what changed; do not commit on its behalf.
## Workflow
1. Read relevant specs based on task type and the files in `implement.jsonl` if present
2. Read the task's `prd.md`, `design.md` if present, and `implement.md` if present
3. Implement features following specs and existing patterns
4. Run the project's lint and typecheck commands on the changed scope
5. Report files touched, key decisions, and verification results back to the channel
## Code Standards
- Follow existing code patterns
- Don't add unnecessary abstractions
- Only do what the PRD asks for; no speculative scope expansion
- Surface uncertainty back to the channel rather than guessing
## Report Format
```
## Implementation Complete
### Files Modified
- <path> — <one-line description>
### Implementation Summary
1. <step>
2. <step>
### Verification Results
- Lint: <pass|fail|skipped + reason>
- TypeCheck: <pass|fail|skipped + reason>
### Open Questions
- <if any, otherwise omit>
```
+56 -6
查看文件
@@ -74,12 +74,62 @@ session_auto_commit: false
#-------------------------------------------------------------------------------
# Codex (dispatch behavior)
#-------------------------------------------------------------------------------
# Codex-only knob; other platforms ignore it. Default ("inline") makes the
# main Codex agent edit code directly because Codex sub-agents run with
# `fork_turns="none"` isolation and can't inherit the parent session's
# task context. Set to "sub-agent" to opt into the legacy dispatch model
# (main agent spawns trellis-implement / trellis-check / trellis-research
# sub-agents).
# Codex-only knob; other platforms ignore it. Trellis 0.6.9 supports native
# sub-agent context injection, but hyb-oa deliberately keeps the main session
# in charge unless a task explicitly approves sub-agent work. "sub-agent" is
# retained upstream as a backwards-compatible alias for "auto".
#
codex:
dispatch_mode: inline # keep Codex main-session edits until a task explicitly asks for sub-agents
#-------------------------------------------------------------------------------
# Channel worker OOM guard
#-------------------------------------------------------------------------------
# Default safeguards for `trellis channel spawn` workers. The guard runs
# at spawn time (cleans expired idle workers, then enforces the live-worker
# budget) and inside each supervisor (self-terminates a worker that stays
# continuously idle past `idle_timeout`).
#
# Precedence: CLI flag > env var (TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT /
# TRELLIS_CHANNEL_MAX_LIVE_WORKERS) > this config > built-in default.
#
# `idle_timeout: 0` disables idle cleanup (workers can sit idle forever
# unless explicitly killed or given `--timeout`).
# `max_live_workers: 0` disables the spawn-time budget check.
#
# `trusted_context_dirs` extends the `--file`/`--jsonl`/`--agent` containment
# check beyond the worker cwd — useful when `.trellis/tasks` or
# `.trellis/workspace` is a symlink to an external directory. Realpaths under
# any listed dir are accepted in addition to cwd.
# `auto_trust_trellis_symlinks: false` disables the narrow auto-trust of
# `.trellis/tasks` / `.trellis/workspace` when either is itself a top-level
# symlink (auto-trust is on by default).
#
channel:
worker_guard:
idle_timeout: 5m
max_live_workers: 6
# trusted_context_dirs:
# - /work/user/trellis_workspace
# auto_trust_trellis_symlinks: false
#-------------------------------------------------------------------------------
# Sub-agent context injection limits
#-------------------------------------------------------------------------------
# Trellis 0.6.9 caps injected task artifacts and referenced context. Defaults
# are sufficient for hyb-oa; keep them documented but implicit until a real
# task proves they need tuning.
#
# context_injection:
# max_file_bytes: 32768
# max_artifact_bytes: 65536
# max_total_bytes: 131072
#-------------------------------------------------------------------------------
# Per-turn prompt injection
#-------------------------------------------------------------------------------
# Optional one-turn escape hatch. The project does not enable it by default so
# Trellis task state remains deterministic.
#
# prompt_injection:
# skip_keyword: "no-trellis"
+175 -41
查看文件
@@ -12,9 +12,12 @@ Usage:
<session content here>
EOF
# Structured content (repeatable; a section with no bullets is omitted):
python3 add_session.py --title "Title" --change "Did X" --test "Ran Y" --next-step "Do Z"
Branch resolution order:
1. --branch CLI arg (explicit)
2. task.json branch field (from active task)
2. task.json branch field (from active task, if still exists)
3. git branch --show-current (auto-detect)
4. None (omitted gracefully)
"""
@@ -28,6 +31,8 @@ from datetime import datetime
from pathlib import Path
from common.paths import (
DIR_TASKS,
DIR_WORKFLOW,
FILE_JOURNAL_PREFIX,
get_repo_root,
get_current_task,
@@ -36,12 +41,14 @@ from common.paths import (
)
from common.developer import ensure_developer
from common.git import run_git
from common.log import Colors, colored
from common.safe_commit import (
print_gitignore_warning,
safe_git_add,
safe_trellis_paths_to_add,
)
from common.tasks import load_task
from common.types import TaskInfo
from common.config import (
get_packages,
get_session_auto_commit,
@@ -124,6 +131,99 @@ def count_journal_files(dev_dir: Path, active_num: int) -> str:
return "\n".join(result_lines)
def get_current_git_branch(repo_root: Path) -> str | None:
"""Return the current checkout branch, or None for detached/non-git states."""
rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
if rc != 0:
return None
detected = branch_out.strip()
return detected or None
def branch_ref_exists(repo_root: Path, branch: str) -> bool:
"""Return True when branch exists locally or as the local origin ref."""
for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"):
rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root)
if rc == 0:
return True
return False
def resolve_session_branch(
repo_root: Path,
cli_branch: str | None,
task_data: TaskInfo | None,
) -> str | None:
"""Resolve journal branch without trusting stale task.json branch fields."""
if cli_branch:
return cli_branch
current_branch = get_current_git_branch(repo_root)
raw_task_branch = task_data.raw.get("branch") if task_data else None
task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else ""
if not task_branch:
return current_branch
if branch_ref_exists(repo_root, task_branch):
return task_branch
if current_branch:
print(
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; using current branch '{current_branch}'.",
file=sys.stderr,
)
return current_branch
print(
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; omitting branch.",
file=sys.stderr,
)
return None
def is_git_worktree(repo_root: Path) -> bool:
"""Return True when repo_root is a linked worktree (not the main working tree).
Standard test: `git rev-parse --git-dir` (per-worktree) differs from
`git rev-parse --git-common-dir` (shared across all worktrees) once both
are resolved to absolute paths. In the main working tree these are the
same directory.
"""
rc_dir, git_dir, _ = run_git(["rev-parse", "--git-dir"], cwd=repo_root)
rc_common, git_common_dir, _ = run_git(
["rev-parse", "--git-common-dir"], cwd=repo_root
)
if rc_dir != 0 or rc_common != 0:
return False
git_dir_path = (repo_root / git_dir.strip()).resolve()
git_common_dir_path = (repo_root / git_common_dir.strip()).resolve()
return git_dir_path != git_common_dir_path
def warn_if_parallel_worktree(repo_root: Path) -> None:
"""Non-blocking note: index.md conflicts across parallel worktrees/branches
are expected and safe. Only fires when running in a linked git worktree
(not the main tree) with `session_auto_commit` enabled (#415 quick-fix tier).
"""
if not get_session_auto_commit(repo_root):
return
if not is_git_worktree(repo_root):
return
print(
colored(
"[NOTE] Running in a git worktree with session_auto_commit enabled: "
"journal-*.md files auto-merge via .gitattributes, but index.md "
"conflicts across parallel worktrees/branches are expected and safe "
"to resolve by picking either side (task state lives in task.json, "
"not index.md). See .trellis/spec/cli/backend/directory-structure.md "
'("Workspace Journal Merge Behavior").',
Colors.YELLOW,
),
file=sys.stderr,
)
def create_new_journal_file(
dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
) -> Path:
@@ -143,15 +243,39 @@ def create_new_journal_file(
return new_file
def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "- ") -> str:
"""Render a Markdown section as bullets, or "" when there is no content.
A section with zero provided values is omitted entirely from the
rendered entry rather than falling back to a placeholder string.
"""
if not items:
return ""
bullets = "\n".join(f"{bullet_prefix}{item}" for item in items)
return f"\n\n### {header}\n\n{bullets}"
def _render_main_changes(changes: list[str], extra_content: str | None) -> str:
"""Render the Main Changes section from --change bullets or freeform content."""
if changes:
return _render_bullet_section("Main Changes", changes)
if extra_content:
return f"\n\n### Main Changes\n\n{extra_content}"
return ""
def generate_session_content(
session_num: int,
title: str,
commit: str,
summary: str,
extra_content: str,
today: str,
package: str | None = None,
branch: str | None = None,
changes: list[str] | None = None,
extra_content: str | None = None,
tests: list[str] | None = None,
next_steps: list[str] | None = None,
) -> str:
"""Generate session content."""
if commit and commit != "-":
@@ -166,6 +290,10 @@ def generate_session_content(
package_line = f"\n**Package**: {package}" if package else ""
branch_line = f"\n**Branch**: `{branch}`" if branch else ""
main_changes_section = _render_main_changes(changes or [], extra_content)
testing_section = _render_bullet_section("Testing", tests or [], bullet_prefix="- [OK] ")
next_steps_section = _render_bullet_section("Next Steps", next_steps or [])
return f"""
## Session {session_num}: {title}
@@ -175,27 +303,15 @@ def generate_session_content(
### Summary
{summary}
### Main Changes
{extra_content}
{summary}{main_changes_section}
### Git Commits
{commit_table}
### Testing
- [OK] (Add test results)
{commit_table}{testing_section}
### Status
[OK] **Completed**
### Next Steps
- None - task complete
[OK] **Completed**{next_steps_section}
"""
@@ -319,12 +435,14 @@ def update_index(
# =============================================================================
def _auto_commit_workspace(repo_root: Path) -> None:
"""Stage Trellis-owned workspace + task paths and commit.
"""Stage Trellis-owned workspace + current-task paths and commit.
Path scope is restricted to specific products (journal files, index.md,
active task dirs, the archive subtree). We never `git add` the whole
`.trellis/` tree, and if `.gitignore` blocks the specific paths we
warn + skip — never retry with ``-f``.
Path scope is restricted to specific products: the current developer's
journal files + index.md, and ONLY the current task directory (resolved
via ``get_current_task``). We never `git add` the whole `.trellis/` tree
or iterate over all active task dirs (#303: parallel-window dirty task
dirs must not be bundled into the session auto-commit). If `.gitignore`
blocks the specific paths we warn + skip — never retry with ``-f``.
Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to
``false``, this function returns immediately without touching git
@@ -338,7 +456,23 @@ def _auto_commit_workspace(repo_root: Path) -> None:
return
commit_msg = get_session_commit_message(repo_root)
paths = safe_trellis_paths_to_add(repo_root)
# Resolve the current task so staging is scoped to its dir only. The ref
# is ``.trellis/tasks/<name>`` (or under archive/) — pass the bare name.
current = get_current_task(repo_root)
if current:
task_name = Path(current).name
paths = safe_trellis_paths_to_add(repo_root, task_name=task_name)
else:
# Current task unknown (0 or >=2 parallel sessions — exactly the
# parallel-window case #303 is about). Do NOT fall back to the wide
# `tasks_dir.iterdir()` scan; that would re-leak other tasks' dirty
# dirs into the session commit. Stage only the developer's journal/
# index and skip every task dir.
paths = [
p
for p in safe_trellis_paths_to_add(repo_root, task_name=None)
if not p.startswith(f"{DIR_WORKFLOW}/{DIR_TASKS}/")
]
if not paths:
print("[OK] No workspace changes to commit.", file=sys.stderr)
return
@@ -375,14 +509,18 @@ def _auto_commit_workspace(repo_root: Path) -> None:
def add_session(
title: str,
commit: str = "-",
summary: str = "(Add summary)",
extra_content: str = "(Add details)",
summary: str = "Session summary was not supplied.",
changes: list[str] | None = None,
extra_content: str | None = None,
tests: list[str] | None = None,
next_steps: list[str] | None = None,
auto_commit: bool = True,
package: str | None = None,
branch: str | None = None,
) -> int:
"""Add a new session."""
repo_root = get_repo_root()
warn_if_parallel_worktree(repo_root)
ensure_developer(repo_root)
developer = get_developer(repo_root)
@@ -405,8 +543,9 @@ def add_session(
new_session = current_session + 1
session_content = generate_session_content(
new_session, title, commit, summary, extra_content, today, package,
branch,
new_session, title, commit, summary, today, package, branch,
changes=changes, extra_content=extra_content, tests=tests,
next_steps=next_steps,
)
content_lines = len(session_content.splitlines())
@@ -483,10 +622,13 @@ def main() -> int:
)
parser.add_argument("--title", required=True, help="Session title")
parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
parser.add_argument("--summary", default="(Add summary)", help="Brief summary")
parser.add_argument("--summary", default="Session summary was not supplied.", help="Brief summary")
parser.add_argument("--content-file", help="Path to file with detailed content")
parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)")
parser.add_argument("--branch", help="Branch name (auto-detected if omitted)")
parser.add_argument("--change", action="append", help="Main Changes bullet (repeatable)")
parser.add_argument("--test", action="append", help="Testing bullet (repeatable)")
parser.add_argument("--next-step", action="append", help="Next Steps bullet (repeatable)")
parser.add_argument("--no-commit", action="store_true",
help="Skip auto-commit of workspace changes")
parser.add_argument("--stdin", action="store_true",
@@ -494,7 +636,7 @@ def main() -> int:
args = parser.parse_args()
extra_content = "(Add details)"
extra_content: str | None = None
if args.content_file:
content_path = Path(args.content_file)
if content_path.is_file():
@@ -523,20 +665,12 @@ def main() -> int:
task_package = task_data.package if task_data else None
package = resolve_package(task_package, repo_root)
# Resolve branch: CLI → task.json → git auto-detect → None
branch = args.branch
if not branch:
if task_data and task_data.raw.get("branch"):
branch = task_data.raw["branch"]
else:
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
detected = branch_out.strip()
if detected:
branch = detected
branch = resolve_session_branch(repo_root, args.branch, task_data)
return add_session(
args.title, args.commit, args.summary, extra_content,
args.title, args.commit, args.summary,
changes=args.change, extra_content=extra_content, tests=args.test,
next_steps=args.next_step,
auto_commit=not args.no_commit,
package=package,
branch=branch,
+46 -13
查看文件
@@ -43,6 +43,11 @@ _KNOWN_PLATFORMS = {
"kiro",
"copilot",
"pi",
"trae",
"grok",
"kimi",
"zcode",
"snow",
}
_ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
@@ -57,6 +62,15 @@ _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
("kiro", ("KIRO_SESSION_ID",)),
("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")),
("pi", ("PI_SESSION_ID", "PI_SESSIONID")),
("trae", ("TRAE_SESSION_ID",)),
# ZCode reuses CLAUDE_SESSION_ID (it does not document a ZCODE_SESSION_ID).
# Platform-scoped lookup (_iter_env_keys filters by platform name), so this
# only fires when the resolver already detected "zcode" — no collision with
# the claude entry above.
("zcode", ("CLAUDE_SESSION_ID",)),
# Snow CLI exports SNOW_SESSION_ID into hook/terminal/sub-agent children.
# TRELLIS_CONTEXT_ID remains the preferred override when present.
("snow", ("SNOW_SESSION_ID",)),
)
_ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")),
@@ -76,6 +90,12 @@ _ENV_PLATFORM_ALIASES = {
"factory-ai": "droid",
"github-copilot": "copilot",
}
# ZCode intentionally reuses CLAUDE_SESSION_ID. Hooks know the host is ZCode,
# while later shell commands see only the shared env name and resolve it through
# the Claude entry. Canonicalize both paths to one runtime filename.
_CONTEXT_KEY_PLATFORM_ALIASES = {
"zcode": "claude",
}
@dataclass(frozen=True)
@@ -189,6 +209,7 @@ def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None
def _context_key(platform_name: str, kind: str, value: str) -> str:
platform_name = _CONTEXT_KEY_PLATFORM_ALIASES.get(platform_name, platform_name)
if kind == "transcript":
return f"{platform_name}_transcript_{_hash_value(value)}"
safe_value = _sanitize_key(value)
@@ -380,15 +401,18 @@ def _lookup_cursor_shell_ticket_context_key() -> str | None:
def resolve_context_key(
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
*,
allow_environment_context: bool = True,
) -> str | None:
"""Resolve a stable session/window context key, if one is available.
`TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI
scripts and subprocesses. It does not store the task itself.
"""
override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID"))
if override:
return _sanitize_key(override) or _hash_value(override)
if allow_environment_context:
override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID"))
if override:
return _sanitize_key(override) or _hash_value(override)
data = _as_dict(platform_input)
platform_name = _detect_platform(data, platform) if data or platform else None
@@ -406,11 +430,12 @@ def resolve_context_key(
if transcript_path:
return _context_key(platform_name or "session", "transcript", transcript_path)
env_context_key = _lookup_env_context_key(platform_name)
if env_context_key:
return env_context_key
if allow_environment_context:
env_context_key = _lookup_env_context_key(platform_name)
if env_context_key:
return env_context_key
if platform_name in (None, "session", "cursor"):
if allow_environment_context and platform_name in (None, "session", "cursor"):
return _lookup_cursor_shell_ticket_context_key()
return None
@@ -469,17 +494,24 @@ def resolve_active_task(
repo_root: Path,
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
*,
allow_single_session_fallback: bool = True,
allow_environment_context: bool = True,
) -> ActiveTask:
"""Resolve the active task from session runtime state only.
A stale session task is returned as stale. Missing context identity or a
missing/empty session context falls back to single-session inference: if
exactly one session file exists in the runtime, return its task with
source_type="session-fallback" — covers class-2 platform sub-agents (codex,
copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
source_type="session-fallback" — covers pull-based platform sub-agents
(copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
files or 0 files yield ActiveTask(None) — refuses to guess across windows.
"""
context_key = resolve_context_key(platform_input, platform)
context_key = resolve_context_key(
platform_input,
platform,
allow_environment_context=allow_environment_context,
)
if context_key:
context = _read_json(_context_path(repo_root, context_key)) or {}
task_ref = _string_value(context.get("current_task"))
@@ -487,9 +519,10 @@ def resolve_active_task(
if active:
return active
fallback = _resolve_single_session_fallback(repo_root)
if fallback is not None:
return fallback
if allow_single_session_fallback:
fallback = _resolve_single_session_fallback(repo_root)
if fallback is not None:
return fallback
return ActiveTask(None, "none", context_key)
+178 -39
查看文件
@@ -1,7 +1,7 @@
"""
CLI Adapter for Multi-Platform Support.
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces.
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Devin, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces.
Supported platforms:
- claude: Claude Code (default)
@@ -13,12 +13,16 @@ Supported platforms:
- kiro: Kiro Code (skills-based)
- gemini: Gemini CLI
- antigravity: Antigravity (workflow-based)
- windsurf: Windsurf (workflow-based)
- devin: Devin (formerly Windsurf; workflow-based)
- qoder: Qoder
- codebuddy: CodeBuddy
- copilot: GitHub Copilot (VS Code)
- droid: Factory Droid (commands-based)
- pi: Pi Agent (extension-backed)
- trae: Trae IDE (IDE-only, hooks-based)
- omp: Oh My Pi
- grok: Grok Build (pull-based skills/agents; no hook context injection)
- kimi: Kimi Code (pull-based skills; commands delivered as skills; no hook context injection)
Usage:
from common.cli_adapter import CLIAdapter
@@ -47,12 +51,16 @@ Platform = Literal[
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
]
@@ -97,7 +105,7 @@ class CLIAdapter:
"""Get platform-specific config directory name.
Returns:
Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', '.codebuddy', '.github/copilot', '.factory', or '.pi')
Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.devin', '.qoder', '.codebuddy', '.github/copilot', '.factory', '.pi', or '.trae')
"""
if self.platform == "opencode":
return ".opencode"
@@ -115,8 +123,8 @@ class CLIAdapter:
return ".gemini"
elif self.platform == "antigravity":
return ".agent"
elif self.platform == "windsurf":
return ".windsurf"
elif self.platform == "devin":
return ".devin"
elif self.platform == "qoder":
return ".qoder"
elif self.platform == "codebuddy":
@@ -127,6 +135,14 @@ class CLIAdapter:
return ".factory"
elif self.platform == "pi":
return ".pi"
elif self.platform == "trae":
return ".trae"
elif self.platform == "omp":
return ".omp"
elif self.platform == "grok":
return ".grok"
elif self.platform == "kimi":
return ".kimi-code"
else:
return ".claude"
@@ -137,7 +153,7 @@ class CLIAdapter:
project_root: Project root directory
Returns:
Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, .codebuddy, .github/copilot, .factory, or .pi)
Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .devin, .qoder, .codebuddy, .github/copilot, .factory, .pi, or .trae)
"""
return project_root / self.config_dir_name
@@ -169,7 +185,7 @@ class CLIAdapter:
Note:
Cursor uses prefix naming: .cursor/commands/trellis-<name>.md
Antigravity uses workflow directory: .agent/workflows/<name>.md
Windsurf uses workflow directory: .windsurf/workflows/trellis-<name>.md
Devin uses workflow directory: .devin/workflows/trellis-<name>.md
Copilot uses prompt files: .github/prompts/<name>.prompt.md
Pi uses prompt templates: .pi/prompts/trellis-<name>.md
Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
@@ -184,8 +200,31 @@ class CLIAdapter:
filename = filename[:-3]
return prompts_dir / f"trellis-{filename}.md"
return prompts_dir / Path(*parts)
# OMP and Grok: flat slash commands under .{platform}/commands/trellis-<name>.md
if self.platform in ("omp", "grok"):
commands_dir = self.get_config_dir(project_root) / "commands"
if not parts:
return commands_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return commands_dir / f"trellis-{filename}.md"
return commands_dir / Path(*parts)
if self.platform == "windsurf":
# Kimi: commands are skills under .kimi-code/skills/trellis-<name>/SKILL.md
if self.platform == "kimi":
skills_dir = self.get_config_dir(project_root) / "skills"
if not parts:
return skills_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return skills_dir / f"trellis-{filename}" / "SKILL.md"
return skills_dir / Path(*parts)
if self.platform == "devin":
workflow_dir = self.get_config_dir(project_root) / "workflows"
if not parts:
return workflow_dir
@@ -242,7 +281,7 @@ class CLIAdapter:
Kiro: .kiro/skills/trellis-<name>/SKILL.md
Gemini: .gemini/commands/trellis/<name>.toml
Antigravity: .agent/workflows/<name>.md
Windsurf: .windsurf/workflows/trellis-<name>.md
Devin: .devin/workflows/trellis-<name>.md
Pi: .pi/prompts/trellis-<name>.md
Others: .{platform}/commands/trellis/<name>.md
"""
@@ -258,8 +297,8 @@ class CLIAdapter:
return f".gemini/commands/trellis/{name}.toml"
elif self.platform == "antigravity":
return f".agent/workflows/{name}.md"
elif self.platform == "windsurf":
return f".windsurf/workflows/trellis-{name}.md"
elif self.platform == "devin":
return f".devin/workflows/trellis-{name}.md"
elif self.platform == "kilo":
return f".kilocode/workflows/{name}.md"
elif self.platform == "copilot":
@@ -268,6 +307,10 @@ class CLIAdapter:
return f".factory/commands/trellis/{name}.md"
elif self.platform == "pi":
return f".pi/prompts/trellis-{name}.md"
elif self.platform in ("omp", "grok"):
return f"{self.config_dir_name}/commands/trellis-{name}.md"
elif self.platform == "kimi":
return f".kimi-code/skills/trellis-{name}/SKILL.md"
else:
return f"{self.config_dir_name}/commands/trellis/{name}.md"
@@ -293,7 +336,7 @@ class CLIAdapter:
return {} # Gemini CLI doesn't have a non-interactive env var
elif self.platform == "antigravity":
return {}
elif self.platform == "windsurf":
elif self.platform == "devin":
return {}
elif self.platform == "qoder":
return {}
@@ -305,6 +348,14 @@ class CLIAdapter:
return {}
elif self.platform == "pi":
return {}
elif self.platform == "trae":
return {}
elif self.platform == "omp":
return {}
elif self.platform == "grok":
return {}
elif self.platform == "kimi":
return {}
else:
return {"CLAUDE_NON_INTERACTIVE": "1"}
@@ -370,9 +421,9 @@ class CLIAdapter:
raise ValueError(
"Antigravity workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "windsurf":
elif self.platform == "devin":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI agent run is not supported."
"Devin workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "qoder":
cmd = ["qodercli", "-p", prompt]
@@ -390,6 +441,21 @@ class CLIAdapter:
)
elif self.platform == "pi":
cmd = ["pi", "-p", prompt]
elif self.platform == "trae":
raise ValueError(
"Trae is IDE-only; CLI agent run is not supported."
)
elif self.platform == "omp":
raise ValueError(
"OMP uses native task tool for agent runs; CLI agent run is not supported."
)
elif self.platform == "grok":
# Headless single-prompt; sub-agents use in-process spawn_subagent.
cmd = ["grok", "-p", prompt, "--yolo"]
elif self.platform == "kimi":
# Headless single-prompt with auto-approval; sub-agents are the
# built-in coder/explore/plan agents dispatched in-session.
cmd = ["kimi", "-p", prompt, "--yolo"]
else: # claude
cmd = ["claude", "-p"]
@@ -436,9 +502,9 @@ class CLIAdapter:
raise ValueError(
"Antigravity workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "windsurf":
elif self.platform == "devin":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI resume is not supported."
"Devin workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "qoder":
return ["qodercli", "--resume", session_id]
@@ -456,6 +522,18 @@ class CLIAdapter:
)
elif self.platform == "pi":
return ["pi", "-c", session_id]
elif self.platform == "trae":
raise ValueError(
"Trae is IDE-only; CLI resume is not supported."
)
elif self.platform == "omp":
raise ValueError(
"OMP uses native task tool for agent runs; CLI resume is not supported."
)
elif self.platform == "grok":
return ["grok", "-c"]
elif self.platform == "kimi":
return ["kimi", "--session", session_id]
else:
return ["claude", "--resume", session_id]
@@ -518,8 +596,8 @@ class CLIAdapter:
return "gemini"
elif self.platform == "antigravity":
return "agy"
elif self.platform == "windsurf":
return "windsurf"
elif self.platform == "devin":
return "devin"
elif self.platform == "qoder":
return "qodercli"
elif self.platform == "codebuddy":
@@ -530,6 +608,14 @@ class CLIAdapter:
return "droid"
elif self.platform == "pi":
return "pi"
elif self.platform == "trae":
return "trae"
elif self.platform == "omp":
return "omp"
elif self.platform == "grok":
return "grok"
elif self.platform == "kimi":
return "kimi"
else:
return "claude"
@@ -540,7 +626,15 @@ class CLIAdapter:
Claude Code, OpenCode, iFlow, and Codex support CLI agent execution.
Cursor is IDE-only and doesn't support CLI agents.
"""
return self.platform in ("claude", "opencode", "iflow", "codex", "pi")
return self.platform in (
"claude",
"opencode",
"iflow",
"codex",
"pi",
"grok",
"kimi",
)
@property
def requires_agent_definition_file(self) -> bool:
@@ -594,14 +688,21 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
"""Get CLI adapter for the specified platform.
Args:
platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')
platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or 'trae')
Returns:
CLIAdapter instance
Raises:
ValueError: If platform is not supported
Note:
'windsurf' is accepted as a deprecated alias for 'devin' (Windsurf was
renamed to Devin) and normalized before validation.
"""
# Deprecated alias: Windsurf was renamed to Devin.
if platform == "windsurf":
platform = "devin"
if platform not in (
"claude",
"opencode",
@@ -612,15 +713,19 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
):
raise ValueError(
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')"
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', 'omp', 'grok', or 'kimi')"
)
return CLIAdapter(platform=platform) # type: ignore
@@ -636,18 +741,23 @@ _ALL_PLATFORM_CONFIG_DIRS = (
".kiro",
".gemini",
".agent",
".windsurf",
".devin",
".windsurf", # deprecated: pre-rename Devin config dir (still a platform signal)
".qoder",
".codebuddy",
".github/copilot",
".factory",
".pi",
".trae",
".omp",
".grok",
".kimi-code",
)
"""Platform-specific config directory names used by detect_platform exclusion
checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform
layer (written by Codex, also consumed by Amp/Cline/Warp/etc. via the
agentskills.io standard), not a single-platform signal. Its presence must not
block detection of Kiro, Antigravity, Windsurf, or other platforms."""
block detection of Kiro, Antigravity, Devin, or other platforms."""
def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool:
@@ -667,27 +777,33 @@ def detect_platform(project_root: Path) -> Platform:
2. .opencode directory exists → opencode
3. .iflow directory exists → iflow
4. .cursor directory exists (without .claude) → cursor
5. .codex exists and no other platform dirs → codex
6. .kilocode directory exists → kilo
7. .kiro/skills exists and no other platform dirs → kiro
8. .gemini directory exists → gemini
5. .gemini directory exists → gemini
6. .codex exists and no other platform dirs → codex
7. .kilocode directory exists → kilo
8. .kiro/skills exists and no other platform dirs → kiro
9. .agent/workflows exists and no other platform dirs → antigravity
10. .windsurf/workflows exists and no other platform dirs → windsurf
10. .devin/workflows (or legacy .windsurf/workflows) exists and no other platform dirs → devin
11. .codebuddy directory exists → codebuddy
12. .qoder directory exists → qoder
13. .pi directory exists → pi
14. Default → claude
13. .github/copilot directory exists → copilot
14. .factory directory exists → droid
15. .pi directory exists → pi
16. .trae directory exists → trae
17. Default → claude
Args:
project_root: Project root directory
Returns:
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or default 'claude')
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or default 'claude')
"""
import os
# Check environment variable first
env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower()
# Deprecated alias: Windsurf was renamed to Devin.
if env_platform == "windsurf":
env_platform = "devin"
if env_platform in (
"claude",
"opencode",
@@ -698,12 +814,16 @@ def detect_platform(project_root: Path) -> Platform:
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
):
return env_platform # type: ignore
@@ -749,13 +869,16 @@ def detect_platform(project_root: Path) -> Platform:
):
return "antigravity"
# Check for Windsurf workflow directory only when no other platform config exists
# Check for Devin workflow directory only when no other platform config
# exists. `.windsurf/workflows` is the legacy pre-rename path (still detected
# as devin for back-compat until users migrate via `trellis update --migrate`).
if (
project_root / ".windsurf" / "workflows"
).is_dir() and not _has_other_platform_dir(
project_root, {".windsurf"}
(project_root / ".devin" / "workflows").is_dir()
or (project_root / ".windsurf" / "workflows").is_dir()
) and not _has_other_platform_dir(
project_root, {".devin", ".windsurf"}
):
return "windsurf"
return "devin"
# Check for .codebuddy directory (CodeBuddy-specific)
if (project_root / ".codebuddy").is_dir():
@@ -777,6 +900,22 @@ def detect_platform(project_root: Path) -> Platform:
if (project_root / ".pi").is_dir():
return "pi"
# Check for .trae directory (Trae IDE-specific)
if (project_root / ".trae").is_dir():
return "trae"
# Check for .omp directory (OMP-specific)
if (project_root / ".omp").is_dir():
return "omp"
# Check for .grok directory (Grok Build-specific)
if (project_root / ".grok").is_dir():
return "grok"
# Check for .kimi-code directory (Kimi Code-specific)
if (project_root / ".kimi-code").is_dir():
return "kimi"
# Fallback: checkout only has the Codex shared-skills layer
# (.agents/skills/trellis-* dirs) and no explicit platform config dir.
# Happens on fresh clones where .codex/ is gitignored/absent but the
+126 -2
查看文件
@@ -117,11 +117,12 @@ def _parse_yaml_block(
key, _, value = stripped.partition(":")
key = key.strip()
value = _strip_inline_comment(value).strip()
was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'")
value = _unquote(value)
current_list = None
if value:
# key: value
if value or was_quoted:
# key: value (an explicit quoted "" is a value, not "no value")
target[key] = value
i += 1
else:
@@ -167,6 +168,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
DEFAULT_MAX_JOURNAL_LINES = 2000
DEFAULT_SESSION_AUTO_COMMIT = True
DEFAULT_CODEX_DISPATCH_MODE = "auto"
CONFIG_FILE = "config.yaml"
@@ -243,6 +245,128 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
return DEFAULT_SESSION_AUTO_COMMIT
def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
"""Return Codex dispatch mode.
Default is ``auto``, which dispatches Trellis sub-agents and uses native
context injection with a child-side fallback. ``inline`` is an explicit
opt-out. ``sub-agent`` remains a backwards-compatible alias for ``auto``.
Invalid explicit configuration falls back to ``inline`` rather than
unexpectedly dispatching a sub-agent. This CLI-facing parser is the only
place that emits a warning for invalid values; hook readers fail safely
without producing per-turn warning noise.
"""
config = _load_config(repo_root)
codex = config.get("codex")
if codex is None:
return DEFAULT_CODEX_DISPATCH_MODE
if not isinstance(codex, dict):
print(
f"[WARN] invalid codex config: {codex!r}; using inline",
file=sys.stderr,
)
return "inline"
raw = codex.get("dispatch_mode", DEFAULT_CODEX_DISPATCH_MODE)
mode = str(raw).strip().lower()
if mode in ("auto", "inline"):
return mode
if mode == "sub-agent":
return "auto"
print(
f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline",
file=sys.stderr,
)
return "inline"
DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES = 32768
DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES = 65536
DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES = 131072
def get_context_injection_limits(repo_root: Path | None = None) -> dict[str, int]:
"""Return sub-agent context injection byte limits.
Reads the ``context_injection:`` section of ``.trellis/config.yaml``:
context_injection:
max_file_bytes: 32768
max_artifact_bytes: 65536
max_total_bytes: 131072
``0`` disables the corresponding limit. Missing keys use their default;
invalid (non-int or negative) values fall back to the default for that
key with a stderr warning.
"""
defaults = {
"max_file_bytes": DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES,
"max_artifact_bytes": DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES,
"max_total_bytes": DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES,
}
config = _load_config(repo_root)
section = config.get("context_injection")
if not isinstance(section, dict):
return defaults
result = dict(defaults)
for key, default_value in defaults.items():
if key not in section:
continue
raw = section[key]
try:
value = int(raw)
except (TypeError, ValueError):
print(
f"[WARN] invalid context_injection.{key} value: {raw!r}; "
f"using default {default_value}",
file=sys.stderr,
)
continue
if value < 0:
print(
f"[WARN] invalid context_injection.{key} value: {raw!r}; "
f"using default {default_value}",
file=sys.stderr,
)
continue
result[key] = value
return result
DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = "no-trellis"
def get_prompt_injection_config(repo_root: Path | None = None) -> dict[str, str]:
"""Return per-turn prompt injection config.
Reads the ``prompt_injection:`` section of ``.trellis/config.yaml``:
prompt_injection:
skip_keyword: "no-trellis" # "" disables the escape hatch entirely
``skip_keyword`` is the word-boundary, case-insensitive keyword that, when
present in the user's prompt, makes the per-turn workflow-state injection
emit nothing for that turn. Defaults to ``"no-trellis"``. A non-string
value falls back to the default.
"""
defaults = {"skip_keyword": DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD}
config = _load_config(repo_root)
section = config.get("prompt_injection")
if not isinstance(section, dict):
return defaults
result = dict(defaults)
raw = section.get("skip_keyword", DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD)
if isinstance(raw, str):
result["skip_keyword"] = raw
return result
def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
"""Get hook commands for a lifecycle event.
+36
查看文件
@@ -29,3 +29,39 @@ def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
return result.returncode, result.stdout, result.stderr
except Exception as e:
return 1, "", str(e)
def resolve_default_branch(repo_root: Path) -> str | None:
"""Resolve the repository's default branch (origin/HEAD target).
Tries the local `refs/remotes/origin/HEAD` symbolic ref first (no
network access), then falls back to `git remote show origin` (which
may hit the network but also repairs a missing/stale symbolic-ref).
Returns None when neither resolves, so callers can fall back to their
own pre-existing behavior.
"""
rc, out, _ = run_git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=repo_root)
if rc == 0 and out.strip():
return out.strip().rsplit("/", 1)[-1]
rc, out, _ = run_git(["remote", "show", "origin"], cwd=repo_root)
if rc == 0:
for line in out.splitlines():
line = line.strip()
if line.startswith("HEAD branch:"):
branch = line.split(":", 1)[1].strip()
if branch and branch != "(unknown)":
return branch
return None
def branch_exists_locally(branch: str, repo_root: Path) -> bool:
"""Check whether a local branch ref exists in the repository."""
if not branch:
return False
rc, _, _ = run_git(
["rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"],
cwd=repo_root,
)
return rc == 0
+28 -4
查看文件
@@ -8,6 +8,8 @@ for JSON file operations across all Trellis scripts.
from __future__ import annotations
import json
import os
import tempfile
from pathlib import Path
@@ -25,13 +27,35 @@ def read_json(path: Path) -> dict | None:
def write_json(path: Path, data: dict) -> bool:
"""Write dict to JSON file with pretty formatting.
The write is atomic: content goes to a temp file in the same directory
and is then renamed over the target. A crash or Ctrl-C mid-write leaves
the existing file intact rather than truncated, so a corrupted task.json
can never make a task silently vanish from `task.py list`.
Returns True on success, False on error.
"""
payload = json.dumps(data, indent=2, ensure_ascii=False)
try:
path.write_text(
json.dumps(data, indent=2, ensure_ascii=False),
encoding="utf-8",
fd, tmp = tempfile.mkstemp(
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
)
except OSError:
return False
try:
try:
f = os.fdopen(fd, "w", encoding="utf-8")
except OSError:
# fdopen never took ownership of fd; close it ourselves.
os.close(fd)
raise
with f:
f.write(payload)
os.replace(tmp, path)
return True
except (OSError, IOError):
except OSError:
try:
os.unlink(tmp)
except OSError:
pass
return False
+45 -15
查看文件
@@ -58,7 +58,10 @@ TRELLIS_IGNORED_SUBPATHS = (
)
def safe_trellis_paths_to_add(repo_root: Path) -> list[str]:
def safe_trellis_paths_to_add(
repo_root: Path,
task_name: str | None = None,
) -> list[str]:
"""Return the list of repo-relative paths the auto-commit should stage.
Only includes paths that exist on disk so callers don't pass non-existent
@@ -68,12 +71,23 @@ def safe_trellis_paths_to_add(repo_root: Path) -> list[str]:
Included:
- .trellis/workspace/<developer>/journal-*.md
- .trellis/workspace/<developer>/index.md
- .trellis/tasks/<task-dir>/ (every active task directory)
- .trellis/tasks/archive/ (whole archive subtree, if present)
- .trellis/tasks/<task_name>/ (ONLY the current task dir when
``task_name`` is passed; plus its archive location if the task
already lives under archive/)
Excluded (intentionally — these must not be staged):
- .trellis/.backup-*, .trellis/worktrees/,
.trellis/.template-hashes.json, .trellis/.runtime/, .trellis/.cache/
Scope contract (see #303 / break-loop analysis): when ``task_name`` is
passed, the task segment stages ONLY that task directory — it never walks
``tasks_dir.iterdir()`` over all active tasks. This mirrors
:func:`safe_archive_paths_to_add` and prevents dirty changes in OTHER
parallel-window task dirs from being bundled into the session auto-commit.
Backwards-compat: with no ``task_name``, the function walks every active
task directory (+ the archive subtree) the old wide way. New callers
should always pass ``task_name``.
"""
paths: list[str] = []
@@ -93,20 +107,36 @@ def safe_trellis_paths_to_add(repo_root: Path) -> list[str]:
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/index.md"
)
# Active tasks: each direct child of tasks/ that is a directory and not
# the archive root. The archive subtree is added as a single path below.
tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS
if tasks_dir.is_dir():
for child in sorted(tasks_dir.iterdir()):
if not child.is_dir():
continue
if child.name == DIR_ARCHIVE:
continue
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}")
if not tasks_dir.is_dir():
return paths
archive_dir = tasks_dir / DIR_ARCHIVE
if archive_dir.is_dir():
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}")
if task_name is not None:
# Narrow scope — ONLY the current task directory (active or archived).
# Never iterdir() all tasks: parallel-window dirty task dirs must not
# leak into the session auto-commit.
active_task = tasks_dir / task_name
if active_task.is_dir():
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}")
archived_task = tasks_dir / DIR_ARCHIVE / task_name
if archived_task.is_dir():
paths.append(
f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{task_name}"
)
return paths
# Legacy wide scope (no task_name): each direct child of tasks/ that is a
# directory and not the archive root, plus the whole archive subtree.
for child in sorted(tasks_dir.iterdir()):
if not child.is_dir():
continue
if child.name == DIR_ARCHIVE:
continue
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}")
archive_dir = tasks_dir / DIR_ARCHIVE
if archive_dir.is_dir():
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}")
return paths
+1 -1
查看文件
@@ -412,7 +412,7 @@ def _get_update_hint(repo_root: Path) -> str | None:
return (
f"Trellis update available: {current_version} -> {latest_version}, "
f"run npm install -g {_PACKAGE_NAME}@latest"
"run trellis update"
)
+106 -10
查看文件
@@ -10,9 +10,9 @@ Provides:
Note:
``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files
are now seeded at ``task.py create`` time with a self-describing
``_example`` line; the AI agent curates real entries during Phase 1.3 of
the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current
instructions.
``_example`` line; the AI agent curates real entries during planning when
the task needs sub-agent/spec context. See ``.trellis/workflow.md`` for the
current planning artifact contract.
"""
from __future__ import annotations
@@ -21,10 +21,36 @@ import argparse
import json
from pathlib import Path
from .config import get_context_injection_limits
from .git import branch_exists_locally
from .io import read_json
from .log import Colors, colored
from .paths import get_repo_root
from .paths import FILE_TASK_JSON, get_repo_root
from .task_utils import resolve_task_dir
# Extensions that look like code rather than spec/research docs. Entries with
# one of these extensions outside .trellis/spec/, docs/docs-site, or the
# task's own directory get a hygiene warning in `task.py validate` — the
# reader is a sub-agent, not a human, so code paths belong in the diff the
# agent reads itself, not in implement.jsonl / check.jsonl.
_CODE_FILE_EXTENSIONS = {
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".py",
".go",
".rs",
".java",
".rb",
".c",
".cc",
".cpp",
".h",
}
# =============================================================================
# Command: add-context
@@ -97,10 +123,26 @@ def cmd_validate(args: argparse.Namespace) -> int:
print(f"Target dir: {target_dir}")
print()
# Warn (don't fail validation) when the recorded branch is stale — it
# was likely already merged and deleted (#399 item 2).
task_json_path = target_dir / FILE_TASK_JSON
if task_json_path.is_file():
task_data = read_json(task_json_path)
stored_branch = task_data.get("branch") if task_data else None
if stored_branch and not branch_exists_locally(stored_branch, repo_root):
print(
colored(
f"Warning: recorded branch '{stored_branch}' no longer exists locally "
"(likely merged and deleted).",
Colors.YELLOW,
)
)
print()
total_errors = 0
for jsonl_name in ["implement.jsonl", "check.jsonl"]:
jsonl_file = target_dir / jsonl_name
errors = _validate_jsonl(jsonl_file, repo_root)
errors = _validate_jsonl(jsonl_file, repo_root, target_dir)
total_errors += errors
print()
@@ -112,11 +154,33 @@ def cmd_validate(args: argparse.Namespace) -> int:
return 1
def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
def _is_exempt_from_code_file_warning(file_path: str, task_rel: str) -> bool:
"""Whether a jsonl entry path is exempt from the code-file hygiene warning.
Exempt: spec docs (``.trellis/spec/``), documentation (``docs``,
``docs-site``), and the task's own directory (execution plans, generated
artifacts, etc. legitimately live there).
"""
posix_path = file_path.replace("\\", "/").lstrip("/")
exempt_prefixes = (".trellis/spec/", "docs/", "docs-site/")
if posix_path.startswith(exempt_prefixes):
return True
if task_rel and (posix_path == task_rel or posix_path.startswith(f"{task_rel}/")):
return True
return False
def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = None) -> int:
"""Validate a single JSONL file.
Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
skipped silently; they are self-describing comments, not real entries.
Beyond hard errors (missing file/dir, invalid JSON), this also prints
non-blocking hygiene warnings (never counted in ``errors``, never change
the exit code): entries that look like code files rather than
spec/research docs, and entries whose file size exceeds the configured
sub-agent context injection cap (``context_injection.max_file_bytes``).
"""
file_name = jsonl_file.name
errors = 0
@@ -125,6 +189,15 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
return 0
task_rel = ""
if task_dir is not None:
try:
task_rel = task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
except ValueError:
task_rel = ""
max_file_bytes = get_context_injection_limits(repo_root).get("max_file_bytes", 0)
line_num = 0
real_entries = 0
for line in jsonl_file.read_text(encoding="utf-8").splitlines():
@@ -152,10 +225,33 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
if not full_path.is_dir():
print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
errors += 1
else:
if not full_path.is_file():
print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
errors += 1
continue
if not full_path.is_file():
print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
errors += 1
continue
extension = Path(file_path).suffix.lower()
if extension in _CODE_FILE_EXTENSIONS and not _is_exempt_from_code_file_warning(
file_path, task_rel
):
warning = (
f"{file_name}:{line_num}: Warning: {file_path} looks like a code file — "
"implement/check.jsonl should reference spec/research docs; "
"agents read code themselves"
)
print(f" {colored(warning, Colors.YELLOW)}")
if max_file_bytes:
size = full_path.stat().st_size
if size > max_file_bytes:
warning = (
f"{file_name}:{line_num}: Warning: {file_path} is {size} bytes, "
f"exceeds context_injection.max_file_bytes ({max_file_bytes}); "
"injection will truncate it"
)
print(f" {colored(warning, Colors.YELLOW)}")
if errors == 0:
print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
+284 -36
查看文件
@@ -9,6 +9,7 @@ Provides:
cmd_set_branch - Set git branch for task
cmd_set_base_branch - Set PR target branch
cmd_set_scope - Set scope for PR title
cmd_set_meta - Set/overwrite a task metadata key
cmd_add_subtask - Link child task to parent
cmd_remove_subtask - Unlink child task from parent
"""
@@ -23,13 +24,14 @@ from datetime import datetime
from pathlib import Path
from .config import (
get_codex_dispatch_mode,
get_packages,
get_session_auto_commit,
is_monorepo,
resolve_package,
validate_package,
)
from .git import run_git
from .git import branch_exists_locally, resolve_default_branch, run_git
from .io import read_json, write_json
from .log import Colors, colored
from .paths import (
@@ -50,6 +52,7 @@ from .safe_commit import (
from .task_utils import (
archive_task_complete,
find_task_by_name,
is_within_tasks_dir,
resolve_task_dir,
run_task_hooks,
)
@@ -113,13 +116,13 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str:
# Config directories of platforms that consume implement.jsonl / check.jsonl.
# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
# platforms listed in workflow.md's "agent-capable" Skill Routing block
# (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity /
# Windsurf are NOT in this list: they do not consume JSONL.
# platforms listed in workflow.md's "agent-capable" Skill Routing block.
# Codex is checked separately because explicit inline mode does not consume
# JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
# specs through skills instead of JSONL.
_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
".claude",
".cursor",
".codex",
".kiro",
".gemini",
".opencode",
@@ -128,7 +131,13 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
".factory", # Factory Droid
".github/copilot",
".pi", # Pi Agent
".trae", # Trae IDE
".omp", # Oh My Pi
".zcode", # ZCode
".grok", # Grok Build
".kimi-code", # Kimi Code
)
_CODEX_CONFIG_DIR = ".codex"
_SEED_EXAMPLE = (
"Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
@@ -141,13 +150,16 @@ _SEED_EXAMPLE = (
def _has_subagent_platform(repo_root: Path) -> bool:
"""Return True if any sub-agent-capable platform is configured.
Detected by probing well-known config directories at the repo root. Used
only to decide whether ``task.py create`` should seed empty
``implement.jsonl`` / ``check.jsonl`` files.
Detected by probing well-known config directories at the repo root. Codex
counts by default through ``codex.dispatch_mode: auto`` (including the
legacy ``sub-agent`` alias); explicit inline mode loads context through
skills, not JSONL.
"""
for config_dir in _SUBAGENT_CONFIG_DIRS:
if (repo_root / config_dir).is_dir():
return True
if (repo_root / _CODEX_CONFIG_DIR).is_dir():
return get_codex_dispatch_mode(repo_root) == "auto"
return False
@@ -162,6 +174,52 @@ def _write_seed_jsonl(path: Path) -> None:
path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8")
def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
"""Parse repeatable ``--meta key=value`` pairs into a dict.
Returns ``None`` (after printing an error naming the bad value) on the
first malformed pair: missing ``=`` or an empty key. Values are stored
as-is (strings, no nesting, no type coercion).
"""
meta: dict[str, str] = {}
for pair in pairs or []:
key, sep, value = pair.partition("=")
if not sep or not key:
print(
colored(f"Error: malformed --meta value '{pair}' (expected key=value)", Colors.RED),
file=sys.stderr,
)
return None
meta[key] = value
return meta
def _default_prd_content(title: str, description: str | None = None) -> str:
"""Return the default PRD skeleton created with every task."""
goal = (description or "").strip() or "TBD."
heading = title.strip() or "Untitled task"
return f"""# {heading}
## Goal
{goal}
## Requirements
- TBD
## Acceptance Criteria
- [ ] TBD
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
"""
# =============================================================================
# Command: create
# =============================================================================
@@ -174,6 +232,11 @@ def cmd_create(args: argparse.Namespace) -> int:
print(colored("Error: title is required", Colors.RED), file=sys.stderr)
return 1
# Validate --meta (CLI source: fail-fast, before any directory is created)
meta = _parse_meta_pairs(getattr(args, "meta", None))
if meta is None:
return 1
# Validate --package (CLI source: fail-fast)
package: str | None = getattr(args, "package", None)
if not is_monorepo(repo_root):
@@ -213,6 +276,34 @@ def cmd_create(args: argparse.Namespace) -> int:
# Create task directory with MM-DD-slug format
tasks_dir = get_tasks_dir(repo_root)
date_prefix = generate_task_date_prefix()
# Guard against date-prefixed --slug (e.g. a full task dir name pasted in),
# which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an
# explicit --slug is guarded; title-derived slugs are left untouched.
if args.slug:
m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug)
if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
slug_prefix = f"{m.group(1)}-{m.group(2)}"
if slug_prefix == date_prefix:
slug = m.group(3)
print(
colored(
f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"',
Colors.YELLOW,
),
file=sys.stderr,
)
else:
print(
colored(
f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).",
Colors.RED,
),
file=sys.stderr,
)
print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr)
return 1
dir_name = f"{date_prefix}-{slug}"
task_dir = tasks_dir / dir_name
task_json_path = task_dir / FILE_TASK_JSON
@@ -231,15 +322,48 @@ def cmd_create(args: argparse.Namespace) -> int:
today = datetime.now().strftime("%Y-%m-%d")
# Record current branch as base_branch (PR target)
# Record the PR target branch. Prefer the repo's actual default branch
# (origin/HEAD) so creating a task from a feature branch doesn't
# mis-stamp that feature branch as the PR target (#399 item 1). Falls
# back to the checked-out branch when the default can't be resolved
# (no remote configured, offline, etc.) — the pre-existing behavior.
# --base-branch lets the caller override both when neither is correct.
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
current_branch = branch_out.strip() or "main"
explicit_base_branch: str | None = getattr(args, "base_branch", None)
if explicit_base_branch:
base_branch = explicit_base_branch
else:
resolved_base_branch = resolve_default_branch(repo_root)
if resolved_base_branch:
base_branch = resolved_base_branch
else:
base_branch = current_branch
print(
colored(
f"warning: could not resolve the repository's default branch "
f"(no remote configured, offline, etc.); stamping base_branch as "
f"the checked-out branch '{base_branch}'. Pass --base-branch to override.",
Colors.YELLOW,
),
file=sys.stderr,
)
description = (args.description or "").strip()
if not description.strip():
print(
colored(
"warning: task description is empty; pass --description to improve search and later audits.",
Colors.YELLOW,
),
file=sys.stderr,
)
task_data = {
"id": slug,
"name": slug,
"title": args.title,
"description": args.description or "",
"description": description,
"status": "planning",
"dev_type": None,
"scope": None,
@@ -250,7 +374,7 @@ def cmd_create(args: argparse.Namespace) -> int:
"createdAt": today,
"completedAt": None,
"branch": None,
"base_branch": current_branch,
"base_branch": base_branch,
"worktree_path": None,
"commit": None,
"pr_url": None,
@@ -259,14 +383,21 @@ def cmd_create(args: argparse.Namespace) -> int:
"parent": None,
"relatedFiles": [],
"notes": "",
"meta": {},
"meta": meta,
}
write_json(task_json_path, task_data)
prd_path = task_dir / "prd.md"
if not prd_path.exists():
prd_path.write_text(
_default_prd_content(args.title, description),
encoding="utf-8",
)
# Seed implement.jsonl / check.jsonl for sub-agent-capable platforms.
# Agent curates real entries in Phase 1.3 (see .trellis/workflow.md).
# Agent-less platforms (Kilo / Antigravity / Windsurf) skip this — they
# Agent curates real entries during planning when the task needs them.
# Agent-less platforms (Kilo / Antigravity / Devin) skip this — they
# load specs via the trellis-before-dev skill instead of JSONL.
seeded_jsonl = False
if _has_subagent_platform(repo_root):
@@ -303,30 +434,70 @@ def cmd_create(args: argparse.Namespace) -> int:
# outside an AI session) — the task is still created, the user can run
# task.py start later. Pointer is session-scoped so this never affects
# other AI sessions.
try:
from .active_task import resolve_context_key, set_active_task
if resolve_context_key():
if getattr(args, "no_start", False):
print(
colored(
"Skipped session activation (--no-start); run task.py start when ready.",
Colors.YELLOW,
),
file=sys.stderr,
)
else:
try:
from .active_task import resolve_context_key, set_active_task
except Exception as exc:
print(
colored(f"Warning: session activation unavailable (import failed: {exc})", Colors.YELLOW),
file=sys.stderr,
)
else:
try:
rel_dir = task_dir.relative_to(repo_root).as_posix()
except ValueError:
rel_dir = str(task_dir)
set_active_task(rel_dir, repo_root)
except Exception:
pass
context_key = resolve_context_key()
except Exception as exc:
print(
colored(f"Warning: session activation failed (context resolution: {exc})", Colors.YELLOW),
file=sys.stderr,
)
else:
# No session identity is the normal CLI-outside-an-AI-session
# case (see comment above) — stay silent, not a failure.
if context_key:
try:
rel_dir = task_dir.relative_to(repo_root).as_posix()
except ValueError:
rel_dir = str(task_dir)
try:
active = set_active_task(rel_dir, repo_root)
except Exception as exc:
print(
colored(f"Warning: session activation failed (pointer persistence: {exc})", Colors.YELLOW),
file=sys.stderr,
)
else:
if active:
print(
colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
file=sys.stderr,
)
print(f"Source: {active.source}", file=sys.stderr)
else:
print(
colored("Warning: session activation failed (no pointer returned)", Colors.YELLOW),
file=sys.stderr,
)
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
print("", file=sys.stderr)
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
print(" 1. Create prd.md with requirements", file=sys.stderr)
print(" - Fill prd.md with requirements and acceptance criteria", file=sys.stderr)
print(" - Lightweight task: PRD-only is valid", file=sys.stderr)
print(" - Complex task: add design.md and implement.md before task.py start", file=sys.stderr)
if seeded_jsonl:
print(
" 2. Curate implement.jsonl / check.jsonl (spec + research files only — "
"see .trellis/workflow.md Phase 1.3)",
" - Curate implement.jsonl / check.jsonl as spec/research manifests when sub-agents need context",
file=sys.stderr,
)
print(" 3. Run: python3 task.py start <dir>", file=sys.stderr)
else:
print(" 2. Run: python3 task.py start <dir>", file=sys.stderr)
print(" - Use /trellis:continue or phase context to decide the next step", file=sys.stderr)
print("", file=sys.stderr)
# Output relative path for script chaining
@@ -363,6 +534,17 @@ def cmd_archive(args: argparse.Namespace) -> int:
print(f" - {t.dir_name}/", file=sys.stderr)
return 1
# Refuse to archive anything that isn't a real task directly under
# .trellis/tasks/. A mistyped name (e.g. "src") resolves to repo_root/src,
# which is a dir but not a task — without this guard archive would move the
# user's source directory out of the repo.
if not is_within_tasks_dir(task_dir, repo_root):
print(colored(
f"Error: refusing to archive '{task_name}': "
f"{task_dir} is not a task under {tasks_dir}",
Colors.RED), file=sys.stderr)
return 1
dir_name = task_dir.name
task_json_path = task_dir / FILE_TASK_JSON
@@ -374,6 +556,19 @@ def cmd_archive(args: argparse.Namespace) -> int:
if task_json_path.is_file():
data = read_json(task_json_path)
if data:
# Warn (don't block) when the recorded branch is stale — it was
# likely already merged and deleted (#399 item 2).
stored_branch = data.get("branch")
if stored_branch and not branch_exists_locally(stored_branch, repo_root):
print(
colored(
f"Warning: recorded branch '{stored_branch}' no longer exists locally "
"(likely merged and deleted).",
Colors.YELLOW,
),
file=sys.stderr,
)
data["status"] = "completed"
data["completedAt"] = today
write_json(task_json_path, data)
@@ -410,7 +605,16 @@ def cmd_archive(args: argparse.Namespace) -> int:
# Auto-commit unless --no-commit
if not getattr(args, "no_commit", False):
_auto_commit_archive(dir_name, repo_root, modified_children)
if not _auto_commit_archive(dir_name, repo_root, modified_children):
print(
colored(
"Archive moved on disk, but git auto-commit did not complete. "
"Resolve `git status` before continuing.",
Colors.RED,
),
file=sys.stderr,
)
return 1
# Return the archive path
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
@@ -427,7 +631,7 @@ def _auto_commit_archive(
task_name: str,
repo_root: Path,
modified_children: list[str] | None = None,
) -> None:
) -> bool:
"""Stage Trellis-owned task paths and commit after archive.
Scoped narrowly to the archived task's source + destination paths
@@ -449,14 +653,21 @@ def _auto_commit_archive(
"[OK] session_auto_commit: false — skipping git stage/commit.",
file=sys.stderr,
)
return
return True
source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
rc, tracked_out, _ = run_git(
["ls-files", "--", source_rel],
cwd=repo_root,
)
source_was_tracked = rc == 0 and bool(tracked_out.strip())
paths = safe_archive_paths_to_add(
repo_root, task_name=task_name, modified_children=modified_children
)
if not paths:
print("[OK] No task changes to commit.", file=sys.stderr)
return
return True
success, _, err = safe_git_add(paths, repo_root)
if not success:
@@ -467,7 +678,7 @@ def _auto_commit_archive(
f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
file=sys.stderr,
)
return
return not source_was_tracked
# Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses
# `git add` (no -A) which only stages additions/modifications. The
@@ -478,7 +689,6 @@ def _auto_commit_archive(
#
# `--ignore-unmatch` makes this a no-op when the task was never tracked
# (e.g. archiving a task that lived only in working tree).
source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
run_git(
["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel],
cwd=repo_root,
@@ -490,14 +700,16 @@ def _auto_commit_archive(
)
if rc == 0:
print("[OK] No task changes to commit.", file=sys.stderr)
return
return True
commit_msg = f"chore(task): archive {task_name}"
rc, _, err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
if rc == 0:
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
return True
else:
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
return not source_was_tracked
# =============================================================================
@@ -695,3 +907,39 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
return 0
# =============================================================================
# Command: set-meta
# =============================================================================
def cmd_set_meta(args: argparse.Namespace) -> int:
"""Set/overwrite one metadata key on an existing task."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
key = args.key
value = args.value
if not key:
print(colored("Error: Missing arguments", Colors.RED))
print("Usage: python3 task.py set-meta <task-dir> <key> <value>")
return 1
task_json = target_dir / FILE_TASK_JSON
if not task_json.is_file():
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
return 1
data = read_json(task_json)
if not data:
return 1
meta = data.get("meta")
if not isinstance(meta, dict):
meta = {}
meta[key] = value
data["meta"] = meta
write_json(task_json, data)
print(colored(f"✓ Meta set: {key} = {value}", Colors.GREEN))
return 0
+24
查看文件
@@ -69,6 +69,30 @@ def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
return True
def is_within_tasks_dir(task_dir_abs: Path, repo_root: Path | None = None) -> bool:
"""Check that a resolved task directory really is a task under the tasks dir.
A real task lives directly at ``.trellis/tasks/<name>``. This returns True
only when ``task_dir_abs`` is an immediate child of the tasks directory.
Guards archive: ``resolve_task_dir`` falls back to ``repo_root/<name>`` for
an unknown name, so a mistyped ``task.py archive src`` resolves to the real
``src/`` source directory. Without this check archive would ``shutil.move``
it out of the repo. Also rejects the tasks dir itself and anything nested
under ``archive/`` (already-archived tasks).
"""
if repo_root is None:
repo_root = get_repo_root()
try:
resolved = task_dir_abs.resolve()
tasks_resolved = get_tasks_dir(repo_root).resolve()
except (OSError, RuntimeError):
return False
if resolved.parent != tasks_resolved:
return False
return resolved.name != "archive"
# =============================================================================
# Task Lookup
# =============================================================================
+2 -1
查看文件
@@ -78,10 +78,11 @@ def _parse_yaml_block(
key, _, value = stripped.partition(":")
key = key.strip()
value = _strip_inline_comment(value).strip()
was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'")
value = _unquote(value)
current_list = None
if value:
if value or was_quoted:
target[key] = value
i += 1
else:
+26 -22
查看文件
@@ -60,15 +60,12 @@ def _parse_marker(line: str) -> tuple[bool, list[str]] | None:
def get_phase_index() -> str:
"""Return Phase Index + Phase 1/2/3 step bodies from workflow.md.
"""Return the compact Phase Index summary from workflow.md.
Matches what the SessionStart hook injects into the `<workflow>` block:
starts at `## Phase Index`, continues through `## Phase 1: Plan`,
`## Phase 2: Execute`, `## Phase 3: Finish`, stops at
`## Customizing Trellis (for forks)` (the docs-for-forks footer).
`[workflow-state:STATUS]` tag blocks (now embedded in Phase Index since
v0.5.0-rc.0) are consumed by the UserPromptSubmit hook so they're
stripped from this output.
SessionStart and no-step phase context use this small summary as their
orientation payload. Detailed Phase 1/2/3 instructions are loaded with
``get_step`` on demand. ``[workflow-state:STATUS]`` tag blocks are
consumed by the per-turn hook, so they're stripped from this output.
"""
text = _read_workflow()
lines = text.splitlines()
@@ -80,7 +77,7 @@ def get_phase_index() -> str:
if start is None and stripped == _PHASE_INDEX_HEADING:
start = i
continue
if start is not None and stripped == "## Customizing Trellis (for forks)":
if start is not None and stripped == "## Phase 1: Plan":
end = i
break
@@ -147,27 +144,34 @@ def _platform_matches(platform: str, block_names: list[str]) -> bool:
def resolve_effective_platform(platform: str, config: dict) -> str:
"""Map ``codex`` to a dispatch-mode-namespaced virtual platform name.
When ``--platform codex`` is passed, return ``"codex-inline"`` (default)
or ``"codex-sub-agent"`` based on ``.trellis/config.yaml`` ``codex.dispatch_mode``.
When ``--platform codex`` is passed, return ``"codex-sub-agent"`` by
default or ``"codex-inline"`` when explicitly configured in
``.trellis/config.yaml``. ``sub-agent`` remains an alias for ``auto``.
``filter_platform`` then surfaces blocks whose marker lists include the
namespaced name (e.g. ``[codex-sub-agent, ...]`` or ``[codex-inline, Kilo,
Antigravity, Windsurf]``).
Antigravity, Devin]``).
Default is ``inline`` because Codex sub-agents run with ``fork_turns="none"``
isolation and can't inherit the parent session's task context — inline
keeps the main agent in charge so context isn't lost. Invalid / missing
values also fall back to inline.
Native Codex context injection supports the ``auto`` default. Invalid
explicit values fall back to ``inline`` safely; this renderer deliberately
does not warn because it can run in normal CLI output flows.
Other platforms are returned unchanged.
"""
if platform == "codex":
mode = "inline"
mode = "auto"
codex_cfg = config.get("codex") if isinstance(config, dict) else None
if isinstance(codex_cfg, dict):
cfg_mode = codex_cfg.get("dispatch_mode")
if cfg_mode in ("inline", "sub-agent"):
mode = cfg_mode
return f"codex-{mode}"
if codex_cfg is not None:
if not isinstance(codex_cfg, dict):
mode = "inline"
else:
cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower()
if cfg_mode == "inline":
mode = "inline"
elif cfg_mode in ("auto", "sub-agent"):
mode = "auto"
else:
mode = "inline"
return "codex-sub-agent" if mode == "auto" else "codex-inline"
return platform
+117 -15
查看文件
@@ -4,16 +4,17 @@
Task Management Script.
Usage:
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] [--no-start]
python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
python3 task.py validate <dir> # Validate jsonl files
python3 task.py list-context <dir> # List jsonl entries
python3 task.py start <dir> # Set active task
python3 task.py current [--source] # Show active task
python3 task.py current [--source] [--json] # Show active task
python3 task.py finish # Clear active task
python3 task.py set-branch <dir> <branch> # Set git branch
python3 task.py set-base-branch <dir> <branch> # Set PR target branch
python3 task.py set-scope <dir> <scope> # Set scope for PR title
python3 task.py set-meta <dir> <key> <value> # Set a task metadata key
python3 task.py archive <task-dir> # Archive completed task
python3 task.py list # List active tasks
python3 task.py list-archive [month] # List archived tasks
@@ -24,6 +25,7 @@ Usage:
from __future__ import annotations
import argparse
import json
import sys
from common.log import Colors, colored
@@ -53,6 +55,7 @@ from common.task_store import (
cmd_set_branch,
cmd_set_base_branch,
cmd_set_scope,
cmd_set_meta,
cmd_add_subtask,
cmd_remove_subtask,
)
@@ -166,6 +169,27 @@ def cmd_current(args: argparse.Namespace) -> int:
repo_root = get_repo_root()
active = resolve_active_task(repo_root)
if getattr(args, "json", False):
task_obj = None
if active.task_path:
data = read_json(repo_root / active.task_path / FILE_TASK_JSON) or {}
task_obj = {
"dir": active.task_path,
"id": data.get("id") or data.get("name"),
"title": data.get("title"),
"status": data.get("status"),
"parent": data.get("parent"),
"children": data.get("children", []),
"branch": data.get("branch"),
"base_branch": data.get("base_branch"),
}
print(json.dumps({
"current_task": task_obj,
"source": active.source,
"stale": active.stale,
}, ensure_ascii=False))
return 0 if active.task_path else 1
if args.source:
print(f"Current task: {active.task_path or '(none)'}")
print(f"Source: {active.source}")
@@ -184,6 +208,24 @@ def cmd_current(args: argparse.Namespace) -> int:
# Command: list
# =============================================================================
def _display_status(t, all_statuses: dict) -> str:
"""Return the status label to show for a task in `list` output.
A parent task's stored status stays "planning" until someone runs
`task.py start` on the parent directly, even while its children are
actively being worked — a misleading label for anyone scanning the
list (#399 item 3). Show "active" instead when at least one child is
past planning; the stored status.json value is left untouched.
"""
if t.status == "planning" and t.children:
child_in_flight = any(
all_statuses.get(c) not in (None, "planning") for c in t.children
)
if child_in_flight:
return "active"
return t.status
def cmd_list(args: argparse.Namespace) -> int:
"""List active tasks."""
repo_root = get_repo_root()
@@ -192,6 +234,38 @@ def cmd_list(args: argparse.Namespace) -> int:
developer = get_developer(repo_root)
filter_mine = args.mine
filter_status = args.status
as_json = getattr(args, "json", False)
# Single pass: collect all tasks via shared iterator
all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
all_statuses = {name: t.status for name, t in all_tasks.items()}
if as_json:
if filter_mine and not developer:
print(json.dumps({"error": "No developer set"}), file=sys.stderr)
return 1
items = []
for dir_name in sorted(all_tasks.keys()):
t = all_tasks[dir_name]
if filter_mine and (t.assignee or "-") != developer:
continue
if filter_status and t.status != filter_status:
continue
items.append({
"dir": f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}",
"id": t.raw.get("id") or dir_name,
"title": t.title,
"status": t.status,
"display_status": _display_status(t, all_statuses),
"priority": t.priority,
"assignee": t.assignee or None,
"parent": t.parent,
"children": list(t.children),
"package": t.package,
})
print(json.dumps({"tasks": items}, ensure_ascii=False))
return 0
if filter_mine:
if not developer:
@@ -202,10 +276,6 @@ def cmd_list(args: argparse.Namespace) -> int:
print(colored("All active tasks:", Colors.BLUE))
print()
# Single pass: collect all tasks via shared iterator
all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
all_statuses = {name: t.status for name, t in all_tasks.items()}
# Display tasks hierarchically
count = 0
@@ -228,6 +298,7 @@ def cmd_list(args: argparse.Namespace) -> int:
# Children progress
progress = children_progress(t.children, all_statuses)
status_label = _display_status(t, all_statuses)
# Package tag
pkg_tag = f" @{t.package}" if t.package else ""
@@ -235,9 +306,9 @@ def cmd_list(args: argparse.Namespace) -> int:
prefix = " " * indent + " - "
if filter_mine:
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}")
print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress}{marker}")
else:
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
count += 1
# Print children indented
@@ -245,9 +316,12 @@ def cmd_list(args: argparse.Namespace) -> int:
if child_name in all_tasks:
_print_task(child_name, indent + 1)
# Display only top-level tasks (those without a parent)
# Display only top-level tasks: those without a parent, plus orphans
# whose recorded parent is not (or no longer) in the active set — a
# dangling parent ref must still render flat instead of disappearing.
for dir_name in sorted(all_tasks.keys()):
if not all_tasks[dir_name].parent:
parent = all_tasks[dir_name].parent
if not parent or parent not in all_tasks:
_print_task(dir_name)
if count == 0:
@@ -307,6 +381,7 @@ Usage:
python3 task.py create <title> Create new task directory
python3 task.py create <title> --package <pkg> Create task for a specific package
python3 task.py create <title> --parent <dir> Create task as child of parent
python3 task.py create <title> --no-start Create without making it active in this session
python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
python3 task.py validate <dir> Validate jsonl files
python3 task.py list-context <dir> List jsonl entries
@@ -316,10 +391,11 @@ Usage:
python3 task.py set-branch <dir> <branch> Set git branch
python3 task.py set-base-branch <dir> <branch> Set PR target branch
python3 task.py set-scope <dir> <scope> Set scope for PR title
python3 task.py set-meta <dir> <key> <value> Set/overwrite a task metadata key
python3 task.py archive <task-dir> Archive completed task
python3 task.py add-subtask <parent> <child> Link child task to parent
python3 task.py remove-subtask <parent> <child> Unlink child from parent
python3 task.py list [--mine] [--status <status>] List tasks
python3 task.py list [--mine] [--status <status>] [--json] List tasks
python3 task.py list-archive [YYYY-MM] List archived tasks
Monorepo options:
@@ -328,10 +404,12 @@ Monorepo options:
List options:
--mine, -m Show only tasks assigned to current developer
--status, -s <s> Filter by status (planning, in_progress, review, completed)
--json Output machine-readable JSON (also available on `current`)
Examples:
python3 task.py create "Add login feature" --slug add-login
python3 task.py create "Add login feature" --slug add-login --package cli
python3 task.py create "Add login feature" --meta linear=ENG-123 --meta epic=auth
python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
python3 task.py set-branch <dir> task/add-login
@@ -369,12 +447,12 @@ def main() -> int:
file=sys.stderr,
)
print(
"sub-agent-capable platforms and curated by the AI during Phase 1.3.",
"sub-agent-capable platforms and curated by the AI during planning when needed.",
file=sys.stderr,
)
print("See .trellis/workflow.md Phase 1.3 or run:", file=sys.stderr)
print("See .trellis/workflow.md planning artifact guidance or run:", file=sys.stderr)
print(
" python3 ./.trellis/scripts/get_context.py --mode phase --step 1.3",
" python3 ./.trellis/scripts/get_context.py --mode phase --step 1",
file=sys.stderr,
)
print(
@@ -392,12 +470,26 @@ def main() -> int:
# create
p_create = subparsers.add_parser("create", help="Create new task")
p_create.add_argument("title", help="Task title")
p_create.add_argument("--slug", "-s", help="Task slug")
p_create.add_argument("--slug", "-s", help="Task slug without the MM-DD date prefix")
p_create.add_argument("--assignee", "-a", help="Assignee developer")
p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
p_create.add_argument("--description", "-d", help="Task description")
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
p_create.add_argument("--package", help="Package name for monorepo projects")
p_create.add_argument(
"--base-branch",
help="PR target branch (overrides origin/HEAD detection and the checked-out-branch fallback)",
)
p_create.add_argument(
"--meta",
action="append",
help="Task metadata key=value (repeatable)",
)
p_create.add_argument(
"--no-start",
action="store_true",
help="Create the task without making it active in this session",
)
# add-context
p_add = subparsers.add_parser("add-context", help="Add context entry")
@@ -422,6 +514,8 @@ def main() -> int:
p_current = subparsers.add_parser("current", help="Show active task")
p_current.add_argument("--source", action="store_true",
help="Show active task source")
p_current.add_argument("--json", action="store_true",
help="Output machine-readable JSON")
# finish
subparsers.add_parser("finish", help="Clear active task")
@@ -441,6 +535,12 @@ def main() -> int:
p_scope.add_argument("dir", help="Task directory")
p_scope.add_argument("scope", help="Scope name")
# set-meta
p_setmeta = subparsers.add_parser("set-meta", help="Set/overwrite a task metadata key")
p_setmeta.add_argument("dir", help="Task directory")
p_setmeta.add_argument("key", help="Metadata key")
p_setmeta.add_argument("value", help="Metadata value")
# archive
p_archive = subparsers.add_parser("archive", help="Archive task")
p_archive.add_argument("name", help="Task directory or name")
@@ -450,6 +550,7 @@ def main() -> int:
p_list = subparsers.add_parser("list", help="List tasks")
p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
p_list.add_argument("--status", "-s", help="Filter by status")
p_list.add_argument("--json", action="store_true", help="Output machine-readable JSON")
# add-subtask
p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
@@ -482,6 +583,7 @@ def main() -> int:
"set-branch": cmd_set_branch,
"set-base-branch": cmd_set_base_branch,
"set-scope": cmd_set_scope,
"set-meta": cmd_set_meta,
"archive": cmd_archive,
"add-subtask": cmd_add_subtask,
"remove-subtask": cmd_remove_subtask,
@@ -51,11 +51,92 @@ rollback and future self-maintenance.
## Current Local Commands
- `python3 -m compileall -q .trellis/scripts .codex/hooks`
- `python3 ./.trellis/scripts/task.py validate <task>`
- `node scripts/qa/check-trellis-governance.mjs`
- `git diff --check`
- `bash scripts/check-minimal.sh`
## Scenario: Trellis Project Upgrade Compatibility
### 1. Scope / Trigger
- Trigger: upgrading the project Trellis version, applying migrations, or
refreshing managed platform assets.
- Purpose: prevent a template update from silently breaking the project's
actual Python runtime or enabling an AI platform that is not in scope.
### 2. Signatures
- Version check: `trellis --version` and `.trellis/.version`.
- Runtime check:
`python3 -m compileall -q .trellis/scripts .codex/hooks`.
- Project preview: `trellis update --dry-run` using the target CLI version.
- Task smoke tests: `task.py validate`, `task.py current --source --json` and
`task.py list --json`.
### 3. Contracts
- Run compile checks with the same `python3` executable used by project
Trellis commands; compatibility with a newer developer interpreter is not
sufficient.
- Project-enabled platforms are authoritative. A local-only settings directory
such as `.claude/` must not cause an out-of-scope managed platform bundle to
enter the upgrade diff.
- Temporarily isolating a local-only platform directory is allowed only when
its exact contents are restored after update and the final diff confirms no
managed assets for that platform were added.
- Preserve intentional local overlays through explicit merge; do not use
`--force` to erase `workflow.md`, `config.yaml`, `.gitignore` or `AGENTS.md`.
- Treat dry-run output and manifest cleanliness as evidence, but do not assume
a dry-run is write-free; inspect `git status` before and after it.
### 4. Validation & Error Matrix
- Runtime compile failure -> stop the upgrade and fix or revert the incompatible
managed script before running task commands.
- Unexpected platform assets in dry-run or final diff -> stop, correct platform
discovery, and rerun the preview.
- `.new` conflict files remain -> upgrade is incomplete; merge or discard each
one deliberately.
- Post-upgrade dry-run changes manifest again -> investigate template hash drift
before commit.
- Only documented project overlays remain modified -> acceptable after task and
governance gates pass.
### 5. Good/Base/Bad Cases
- Good: isolate a local-only `.claude/`, update Codex assets, restore the local
setting, compile with project Python, then verify a stable dry-run.
- Base: update a project with no local platform ambiguity and run the same
compile, task and governance checks.
- Bad: accept generated Claude assets because `.claude/settings.local.json`
happened to exist, or validate Python code only with a newer interpreter.
### 6. Tests Required
- Assert CLI and project versions match the intended target.
- Compile every Python file under `.trellis/scripts` and `.codex/hooks`.
- Exercise task validate/current/list and run Trellis governance.
- Confirm no `.new`, backup, bytecode or out-of-scope platform assets are
tracked; finish with `git diff --check`.
### 7. Wrong vs Correct
#### Wrong
```text
trellis update reports success -> commit immediately
```
#### Correct
```text
preview platform scope -> conservative update/migrate -> merge overlays
-> compile with project Python -> smoke-test task runtime -> stable dry-run
-> governance and project gates -> commit
```
## Legacy Issue CSV
- Do not delete, bulk rewrite or move historical `issues/*.csv` files.
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,36 @@
# Trellis 升级与项目状态多维审查设计
## 决策门
本任务先完成只读盘点和升级方案。用户批准前,不执行 CLI 安装、`trellis update`、模板覆盖、文件删除或业务代码修改。
## 升级顺序
1. 将全局 Trellis CLI 从当前版本对齐到 npm `latest`,再次 dry-run,避免用旧 CLI 写入旧模板。
2. 为当前干净基线保留可回滚提交点;注意 0.6.9 的 `trellis update --dry-run` 会实际清理 `.template-hashes.json` 中旧拼写 skill 的 5 个 manifest 条目,不能把 dry-run 当作零写入操作。
3. 临时安全保存项目内仅含本地设置的 `.claude/` 目录后再升级,避免 Trellis 仅凭目录存在误判 Claude 已启用并生成整套 Claude 资产;升级完成后原样恢复本地设置。
4. 项目升级优先使用“为冲突文件生成 `.new`、自动更新未改模板”的保守策略并应用 migration;不使用 `--force`。
5. 对 `AGENTS.md`、`.trellis/workflow.md`、`.trellis/config.yaml`、`.trellis/.gitignore` 逐文件三方合并,保留项目的中文、Trellis-first、Codex inline、禁止自动提交和授权边界。
6. 应用明确需要的 migration,验证 task、hook、context、governance 和完整本地质量门禁。
## 审查维度
- 产品/交付状态:继承并复核 07-16 审查与 07-18 本地候选证据,严格区分本地、部署、外部验证。
- 工程质量:代码规模、测试入口、全量门禁、风险覆盖和依赖/构建观察项。
- Trellis 治理:版本漂移、模板定制、task/spec/archive、Codex 集成和 session 状态。
- 文档治理:事实源、未注册文档、历史 Issue 与重复状态描述。
- 仓库卫生:ignored 生成物、tracked 历史证据、体积热点和清理验证。
- 运维边界:部署、生产配置、真实数据、企微和付款仍需单独授权。
## 建议分级
- P0:会导致升级覆盖、本地候选状态误判或高风险开放项失管的问题。
- P1:近期应完成、收益明确且可独立验收的改进。
- P2:持续治理或触达即修,不打包成巨型重构。
- 精简项:删除或合并前必须证明非事实源、非审计合同且可从 git 恢复。
## 回滚
- 全局 CLI 可按升级前版本号重新安装。
- 项目文件只在干净基线后修改;升级提交与后续优化提交分开,可独立回退。
- 不手改 `.trellis/.template-hashes.json`;发现 manifest 异常时停止升级并从基线恢复升级提交。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,41 @@
# Trellis 升级实施清单
## 执行步骤
- [x] 将全局 Trellis CLI 升级到 0.6.9。
- [x] 临时隔离并恢复 `.claude/settings.local.json`,避免 Claude 平台误检测。
- [x] 以 `--create-new --migrate` 升级项目 Trellis 模板和 runtime。
- [x] 保留 `session_auto_commit: false` 与 `codex.dispatch_mode: inline`。
- [x] 合并上游 workflow,并恢复 hyb-oa Trellis-first overlay。
- [x] 保留项目 `.trellis/.gitignore` 的本地 workspace 排除规则。
- [x] 保留项目 `AGENTS.md`,不追加重复的通用英文托管块。
- [x] 删除冲突 `.new` 副本和迁移遗留的旧拼写 skill。
- [x] 运行升级后 task/context/hooks/governance 验证。
- [x] 运行完整本地质量门禁并记录结果。
- [x] 评估并更新稳定 spec。
- [x] 形成中文提交。
- [ ] 完成 Trellis task 归档。
## 验证结果
- Trellis CLI、项目版本与 npm latest 均为 0.6.9;升级后 dry-run 稳定。
- Python 3.9.2 compileall、task validate/current/list 与 Trellis runtime 基本命令通过。
- `bash scripts/check-minimal.sh` 退出码为 0:Prisma validate、服务端 985/985、前端 typecheck/build、功能矩阵均通过。
- 非阻断提示仅有既有 `punycode` deprecated 与 Browserslist 数据陈旧提示。
## 验证命令
- `trellis --version`
- `python3 ./.trellis/scripts/get_context.py`
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-26-trellis-upgrade-project-audit`
- `node scripts/qa/check-trellis-governance.mjs`
- `node scripts/qa/audit-docs.mjs --check-registry`
- `node scripts/qa/check-process-contract.mjs --baseline --json`
- `bash scripts/check-minimal.sh`
- `git diff --check`
## 回滚点
- CLI:重新安装升级前的 0.6.8。
- 项目:回退本任务升级提交;`.trellis/.backup-*` 只用于本地临时核对,不纳入提交。
- 若 hooks 或 task runtime 验证失败,停止后续业务工作,不以旧 workflow 混用新 runtime。
@@ -0,0 +1,88 @@
# Trellis 升级与项目状态多维审查
## Goal
确认本项目 Trellis 与上游版本差异,制定可回滚的升级方案;同时从任务治理、代码与测试、文档事实源、仓库卫生、交付与运维等角度梳理当前项目状态,输出分级改进、优化和精简建议。
## What I already know
- 当前分支为 `main`,审查开始时工作区干净。
- 当前没有正式 active task;旧的 `05-31-hyinda-light-ops-greenfield` 仅来自 stale session fallback。
- 项目只启用 Codex 适配,Trellis 不自动提交。
- 非微小改动必须先给出方案并等待批准。
- 近期已经完成过多维系统审查和五人团队正式内用最小收口,需要优先复用结论,避免重复造文档。
## Assumptions (temporary)
- “升级 Trellis”指升级当前项目内由 Trellis 管理的模板、脚本和 Codex 适配文件,不修改全局 npm 包源码。
- 本阶段只做只读审查、版本比对和方案设计;升级与冲突处理在用户批准后实施。
- 审查建议应区分立即处理、近期优化和可暂缓项,并明确收益、风险与验证方式。
## Open Questions
- 待版本与模板漂移盘点后,只保留会实质影响升级范围的偏好问题。
## Requirements (evolving)
- 识别已安装 Trellis 版本、项目记录版本和可用最新版。
- 识别 Trellis 管理文件的本地修改、模板漂移、潜在覆盖冲突和升级回滚点。
- 复核现有 task、spec、docs、代码结构、测试与质量门禁、仓库产物和交付流程。
- 复用既有事实源与历史审查,不轻易新建常驻文档。
- 输出按优先级排序、可执行且避免过度治理的改进建议。
## Scope
- Trellis:全局 CLI 版本、项目模板版本、迁移、Codex hooks/agents/skills、task/spec/workflow/config 兼容性。
- 工程:服务端、前端、测试与发布门禁的当前可执行状态;本阶段不主动重构业务代码。
- 治理:task 生命周期、事实源状态、文档注册、历史 Issue 与 Trellis archive 的保留边界。
- 仓库:已跟踪与本地忽略产物、体积热点、可安全清理项和防复发规则。
- 交付:本地候选、部署、公网/真机/真实数据等状态边界是否仍准确。
## Acceptance Criteria (evolving)
- [x] 给出 Trellis 当前版本、目标版本、升级命令、冲突预案与验证清单。
- [x] 给出项目现状摘要,并以证据路径支撑关键结论。
- [x] 建议覆盖改进、优化、精简三个方向,并区分优先级。
- [x] 在任何非微小修改前获得用户批准。
- [x] 获批实施后通过 Trellis 治理、文档注册、核心前后端及 `git diff --check` 等适用验证。
## Acceptance Evidence
- Trellis CLI 与项目模板均已升级到 0.6.9;升级后 dry-run 不再产生 manifest 变化,仅识别五个预期本地定制文件。
- 项目实际 Python 3.9.2 已通过 `.trellis/scripts` 与 `.codex/hooks` 全量编译检查;并修复 0.6.9 `task_context.py` 的嵌套多行 f-string 兼容问题。
- Trellis task 的 validate/current/list、governance、process contract、敏感扫描与功能矩阵通过。
- Prisma validate 通过;服务端 161 个测试文件、985/985 测试通过;前端 typecheck 与 production build 通过。
- 文档 registry 仅保留既有 22 项假阳性 warning,原因和后续独立治理建议已记录在 research,不作为本次升级失败项。
- 交付状态继续限定为 `LOCAL_CANDIDATE_PASS`;本任务未部署、未执行公网检查或真实数据操作。
## Definition of Done (team quality bar)
- 适用的 lint、typecheck、测试与流程检查通过。
- 升级差异经过人工审阅,本地定制不被静默覆盖。
- 事实源或任务记录按需更新,不制造重复文档。
- 升级与审查改动形成中文提交并引用任务名。
## Test Plan
- 升级前后分别运行 `trellis update --dry-run`,确认版本、迁移、新增文件、本地修改和用户数据保护行为。
- 运行 task validate、Trellis governance、文档 registry/process contract、敏感内容扫描和 `git diff --check`。
- 运行 `bash scripts/check-minimal.sh`,覆盖 Prisma validate、服务端全量 Vitest、前端 typecheck/build 和功能矩阵。
- 仅在修改业务代码或页面行为时追加聚焦服务端/Playwright;本轮默认不部署、不做公网或真实数据检查。
- 升级后复核 Codex hooks、active task 解析、`get_context.py` 和 task 生命周期基本命令。
## Out of Scope (explicit)
- 公网部署、生产配置、真实数据写入、数据库重置、企业微信写入和真实付款。
- 未经批准的大规模业务功能重构。
- 启用 Codex 之外的 AI 平台适配。
## Technical Notes
- Trellis task:`.trellis/tasks/07-26-trellis-upgrade-project-audit/`
- 工作流:`.trellis/workflow.md`
- Trellis 项目规约:`.trellis/spec/hyb-oa/`
- 关键业务事实源:`docs/文档索引.md`、`docs/文档事实源状态矩阵.md`、`docs/Agent开发交付实践.md`
## Research References
- `research/current-state-audit.md`:Trellis 0.6.9 升级预演、当前质量门禁、业务开放项、依赖、文档与仓库卫生审查结论。
@@ -0,0 +1,95 @@
# 当前项目状态多维审查
## 结论摘要
- 当前业务代码仍处于 `LOCAL_CANDIDATE_PASS`,不是已部署或可公网测试;07-18 收口后的业务代码没有新增提交。
- 本次重新执行完整本地门禁通过:Prisma schema 有效,服务端 161 个测试文件 / 985 项测试全部通过,前端 typecheck 与 production build 通过,流程合同、Trellis governance、敏感扫描和功能矩阵通过。
- Trellis 项目版本为 0.5.19,本机 CLI 为 0.6.8,npm 最新为 0.6.9。升级应直接以 0.6.9 为目标。
- 当前最需要优先处理的新增工程风险是前端 React Router 两项 moderate advisory;最重要的既有业务开放项仍是统一导出安全与客户端离线数据生命周期。
## Trellis 升级审查
### 实施结果(2026-07-26)
- 全局 CLI 与项目模板已从 0.6.8 / 0.5.19 对齐到 0.6.9,并完成官方 migration。
- 升级时临时隔离仅含本地设置的 `.claude/`,恢复后未生成 Claude 托管资产;项目继续保持 Codex-only。
- 四个冲突文件按设计完成合并,保留 Trellis-first overlay、Codex inline、禁止自动提交和 `workspace/` 忽略规则;未保留 `.new` 冲突副本。
- 0.6.9 runtime 的 `task_context.py` 包含 Python 3.9.2 不接受的嵌套多行 f-string;已改为先构造 warning 再着色,并通过全量 compileall 与 task runtime 验证。
- 升级后 dry-run 不再改变 manifest,仅报告 `task_context.py`、`config.yaml`、`.gitignore`、`workflow.md`、`AGENTS.md` 五个有意保留的项目定制。
### 升级前版本与差异
- 项目:0.5.19。
- 本机 CLI:0.6.8。
- npm latest:0.6.9。
- 0.6.9 将更新 Trellis runtime、共享 skills、Codex agents/hooks/config,并新增 `.trellis/agents/`、`trellis-channel`、`trellis-session-insight` 和拼写修正后的 `trellis-spec-bootstrap`。
- `AGENTS.md`、`.trellis/workflow.md`、`.trellis/config.yaml`、`.trellis/.gitignore` 被识别为本地修改,必须三方合并。
### 已确认风险
1. 仅因为项目存在 `.claude/settings.local.json`,CLI 就把 Claude 判定为已配置平台,计划新增整套 `.claude/` commands、skills、agents、hooks 与 settings;这与 Codex-only 边界冲突。
2. 0.6.9 的 `update --dry-run` 会实际清理 `.template-hashes.json` 中旧拼写 `trellis-spec-bootstarp` 的 5 个条目,尽管结尾显示 `No changes made`。预演后的非预期修改已恢复。
3. 上游建议 `--migrate`,涉及旧拼写目录迁移;不能手工提前删除旧 skill 或强制覆盖本地 workflow。
### 推荐升级路径
1. 升级全局 CLI 到 0.6.9。
2. 临时安全保存 `.claude/` 本地目录,使 Trellis 只检测到 Codex。
3. 在干净提交点上执行保守更新和 migration;冲突文件生成 `.new`,不使用 force。
4. 逐文件合并四个本地定制文件,保留中文沟通、方案批准、Codex inline、no auto commit、生产授权边界。
5. 恢复 `.claude/settings.local.json`,确认没有新增 Claude 托管资产。
6. 验证 context、task lifecycle、hooks、governance、完整本地门禁并单独提交升级。
## 代码、测试与依赖
- 规模:服务端约 200 个跟踪源文件,前端约 297 个跟踪源文件;服务端 161 个单元测试文件,前端测试目录 81 个文件;Prisma migration 61 项。
- 全量门禁:985/985 服务端测试通过;前端 typecheck/build 通过。
- 覆盖率配置能生成报告,但没有风险阈值;既有 `QA-COVERAGE-001` 仍开放。建议针对认证、付款、worker、迁移/恢复入口建立风险型阈值,不追求全仓统一百分比。
- 前端生产依赖审计报告 2 项 moderate,均来自 React Router 6.30.4;公开修复指向 7.18.1,属于需要专项回归的 major migration。
- 服务端 `npm audit` 因 npm quick-audit endpoint 返回 invalid package tree 而不可用;`npm ls` 无 problem、`npm ci --dry-run` 可完成,说明更像审计接口/锁树兼容问题,但仍应在依赖维护任务中闭合。
- 可安全优先升级一批服务端 patch/minor;Vitest、React、Vite、Tailwind、pg-boss 等 major 不应混成一次升级。
- 构建观察:PWA precache 约 2.1 MiB,扫码 vendor chunk 约 411 KiB,Material Symbols 字体约 479 KiB;目前无行为故障证据,先测真实首屏/弱网再优化。
- 非阻断工具提示:Node `punycode` 弃用;Browserslist 数据约 6 个月未更新。
## 业务与交付状态
- 07-18 已本地验证:认证代理信任、附件主动内容、员工敏感字段、员工合同附件、付款状态机/审计、核心长表单、移动考勤、响应式表格、权限显隐与本地发布门禁等。
- 上述本地修复仍未部署和公网复核;两个原 P0 的 Gate 0 外部验证仍需单独授权。
- 当前 P1 开放项:统一 CSV/表格导出中和;退出/换账号后的离线数据隔离与清除。
- P2 持续治理:下载凭证日志面、未执行 ACL 元数据、CSP、防误触、通知/搜索定位、路由元数据、错误恢复、冻结差旅入口、错误文案、移动权限与可访问性。
- 这些 P2 不应打包成巨型重构,按触达页面或独立小任务收敛。
## 文档与治理
- 事实源矩阵最近审计为 07-16,已正确声明当前可信基线与 Trellis-first 优先级。
- `audit-docs --check-registry` 报 22 个未注册文件,但这 22 个文件都在同一文档后续“当前事实源/手册/证据/技术参考”章节列出;脚本只解析首个主表,因此 warning 噪声高。
- 建议让 registry checker 识别后续分类表,或把 22 项合并进主台账;优先改检查器,避免复制 22 行冗长元数据。
- 历史 Issue CSV 共 349 个、约 3 MiB;Trellis archive 78 个 task、850 个文件、约 5.7 MiB。体积不大且有审计价值,不建议本轮物理删除。
- 后续应停止向历史 Issue CSV 增长;Trellis archive 只保留 PRD/设计/关键结论和最小证据,不再重复提交大批 raw log。
## 仓库卫生
- 整体工作目录约 1.2 GiB,但 Git 仓库仅约 27 MiB;主要是本地依赖与忽略产物,不是 Git 历史膨胀。
- `server/node_modules` 约 644 MiB,`client/node_modules` 约 253 MiB,属于可重建依赖缓存。
- ignored 证据/输出中,`output` 约 154 MiB、`artifacts` 约 17 MiB,加上 build/report/coverage 可安全回收约 174 MiB;当前 `.gitignore` 已覆盖这些路径。
- 建议新增一个显式的本地清理命令,只删除已忽略且可重建的固定路径;默认不删 node_modules,不清理已跟踪 Trellis archive。
## 建议优先级
### P0 / 本轮升级
- 安全升级 Trellis 至 0.6.9,隔离 Claude 误检测、dry-run 写 manifest 和本地四文件合并风险。
- 保持 `LOCAL_CANDIDATE_PASS` 状态上限;未经授权不部署、不做公网/真实数据验证。
### P1 / 下一批独立任务
- React Router 安全升级与导航/重定向回归。
- 统一导出安全 + 客户端离线数据生命周期,沿用既有 WP-5。
- 消除 document registry 的 22 项假阳性 warning,使门禁重新具备信噪比。
### P2 / 维护与精简
- 风险型覆盖阈值、服务端 audit 可用性、patch/minor 依赖维护。
- 本地 ignored artifacts 清理命令和证据保留期限。
- 按触达修复可访问性、导航元数据、错误恢复等开放项。
- 真实性能数据证明需要后,再优化扫码 chunk、字体和 PWA precache。
@@ -0,0 +1,35 @@
{
"id": "trellis-upgrade-project-audit",
"name": "trellis-upgrade-project-audit",
"title": "Trellis 升级与项目状态多维审查",
"description": "安全升级项目 Trellis,并基于现有事实源多维复核项目状态,形成分级优化与精简建议。",
"status": "in_progress",
"dev_type": null,
"scope": "trellis-governance-and-project-audit",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-07-26",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
".trellis/",
".agents/skills/",
".codex/",
"AGENTS.md",
"docs/",
"scripts/qa/",
"server/",
"client/"
],
"notes": "",
"meta": {}
}
+164 -150
查看文件
@@ -11,7 +11,7 @@ mandatory prerequisite for new work.
Required project sequence for non-trivial work:
1. Create or update a `.trellis/tasks/<task>/` task with `prd.md`, acceptance notes and test plan.
2. Add `design.md` or `implement.md` for complex process, API, migration, permission, deployment or workflow-governance changes.
2. Add `design.md` and `implement.md` for complex process, API, migration, permission, deployment or workflow-governance changes.
3. Read the relevant `.trellis/spec/hyb-oa/` files and project docs referenced there.
4. Implement only after the user has approved the task plan.
5. Run the task's test plan plus the Trellis governance gate.
@@ -21,15 +21,10 @@ Required project sequence for non-trivial work:
Hard boundaries:
- Do not let Trellis replace docs fact sources or production deployment gates.
- Do not create new Issue CSV files unless the user explicitly needs a tabular
external audit packet or legacy compatibility export.
- Do not auto-commit Trellis workspace/session files; `session_auto_commit` is
disabled in `.trellis/config.yaml`.
- Do not write passwords, token, Secret, real invoice originals, contract bodies,
bank materials, full accounts, complete local paths, or raw payloads into
`.trellis/`, docs, reports, or chat.
- In Codex, this project defaults to inline dispatch. Sub-agents are optional and
must receive an explicit active task path if used.
- Do not create new Issue CSV files unless the user explicitly needs a tabular external audit packet or legacy compatibility export.
- Do not auto-commit Trellis workspace/session files; `session_auto_commit` is disabled in `.trellis/config.yaml`.
- Do not write passwords, token, Secret, real invoice originals, contract bodies, bank materials, full accounts, complete local paths, or raw payloads into `.trellis/`, docs, reports, or chat.
- In Codex, this project defaults to inline dispatch. Sub-agents are optional and must receive an explicit active task path if used.
---
@@ -70,7 +65,7 @@ python3 ./.trellis/scripts/get_context.py --mode packages # list packages / la
### Task System
Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `prd.md`, `implement.jsonl`, `check.jsonl`, `task.json`, optional `research/`, `info.md`.
Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `task.json`, `prd.md`, optional `design.md`, optional `implement.md`, optional `research/`, and context manifests (`implement.jsonl`, `check.jsonl`) for sub-agent-capable platforms.
```bash
# Task lifecycle
@@ -84,7 +79,7 @@ python3 ./.trellis/scripts/task.py list-archive
# Code-spec context (injected into implement/check agents via JSONL).
# `implement.jsonl` / `check.jsonl` are seeded on `task create` for sub-agent-capable
# platforms; the AI curates real spec + research entries during Phase 1.3.
# platforms; the AI curates real spec + research entries during planning when needed.
python3 ./.trellis/scripts/task.py add-context <name> <action> <file> <reason>
python3 ./.trellis/scripts/task.py list-context <name> [action]
python3 ./.trellis/scripts/task.py validate <name>
@@ -130,7 +125,7 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed
<!--
WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below)
The 4 [workflow-state:STATUS] blocks embedded in the ## Phase Index section
The [workflow-state:STATUS] blocks embedded in the ## Phase Index section
below are the SINGLE source of truth for the per-turn `<workflow-state>`
breadcrumb that every supported AI platform's UserPromptSubmit hook
reads. inject-workflow-state.py (Python platforms) and
@@ -145,15 +140,17 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed
Every workflow-walkthrough step marked `[required · once]` must have a
matching enforcement line in its phase's [workflow-state:*] block. The
breadcrumb is the only per-turn channel; if a mandatory step isn't
mentioned there, the AI silently skips it (Phase 1.3 jsonl curation
mentioned there, the AI silently skips it (Phase 1 planning gate
skip and Phase 3.4 commit skip both manifested via this gap).
TAG ↔ PHASE scoping:
[workflow-state:no_task] → no active task; before Phase 1
[workflow-state:planning] → all of Phase 1 (status='planning')
[workflow-state:in_progress] → Phase 2 + Phase 3.1-3.4
[workflow-state:planning-inline] → Codex inline variant of Phase 1
[workflow-state:in_progress] → Phase 2 + Phase 3.2-3.4
(status stays 'in_progress' from
task.py start until task.py archive)
[workflow-state:in_progress-inline] → Codex inline variant of Phase 2/3
[workflow-state:completed] → currently DEAD: cmd_archive flips
status and moves the dir in the same
call, so the resolver loses the
@@ -173,45 +170,69 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed
## Phase Index
```
Phase 1: Plan → figure out what to do (brainstorm + research → prd.md)
Phase 2: Execute → write code and pass quality checks
Phase 3: Finish → distill lessons + wrap-up
Phase 1: Plan → classify, get task-creation consent, then write planning artifacts
Phase 2: Execute → implement only after task status is in_progress
Phase 3: Finish → verify, update spec, commit, and wrap up
```
### Request Triage
- Simple conversation or small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session.
- Complex task: ask whether you may create a Trellis task and enter planning. If the user says no, do not do broad inline implementation; explain, clarify scope, or suggest a smaller split.
- User approval to create a task is not approval to start implementation. Planning still happens first.
### Planning Artifacts
- `prd.md` — requirements, constraints, and acceptance criteria. Do not put technical design or execution checklists here.
- `design.md` — technical design for complex tasks: boundaries, contracts, data flow, tradeoffs, compatibility, rollout / rollback shape.
- `implement.md` — execution plan for complex tasks: ordered checklist, validation commands, review gates, and rollback points.
- `implement.jsonl` / `check.jsonl` — spec and research manifests for sub-agent context. They do not replace `implement.md`.
- Lightweight tasks may be PRD-only. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`.
### Parent / Child Task Trees
Use a parent task when one user request contains several independently verifiable deliverables. The parent task owns the source requirement set, the task map, cross-child acceptance criteria, and final integration review; it normally should not be the implementation target unless it also has direct work.
Use child tasks for deliverables that can be planned, implemented, checked, and archived independently. Parent/child structure is not a dependency system: if one child must wait for another, write that ordering in the child `prd.md` / `implement.md` and keep each child's acceptance criteria testable.
Create new children with `task.py create "<title>" --slug <name> --parent <parent-dir>`. Link existing tasks with `task.py add-subtask <parent> <child>`, and unlink mistakes with `task.py remove-subtask <parent> <child>`.
<!-- Per-turn breadcrumb: shown when there is no active task (before Phase 1) -->
[workflow-state:no_task]
No active task. **A Direct answer** — pure Q&A / explanation / lookup / chat; no file writes + one-line answer + repo reads ≤ 2 files → AI judges, no override needed.
**B Create a task** — any implementation / code change / build / refactor work. Entry sequence: (1) `python3 ./.trellis/scripts/task.py create "<title>"` to create the task (status=planning, breadcrumb switches to [workflow-state:planning] for brainstorm + jsonl phase guidance) → (2) load `trellis-brainstorm` skill to discuss requirements with the user and iterate on prd.md → (3) once prd is done and jsonl is curated, run `task.py start <task-dir>` to enter [workflow-state:in_progress] for the implementation skeleton. **"It looks small" is NOT grounds for downgrading B to A or C**.
**C Inline change** (per-turn only, escape hatch for B) — the user's CURRENT message MUST contain one of: "skip trellis" / "no task" / "just do it" / "don't create a task" / "跳过 trellis" / "别走流程" / "小修一下" / "直接改" / "先别建任务" → briefly acknowledge ("ok, skipping trellis flow this turn"), then inline. **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said.
No active task. First classify the current turn and ask for task-creation consent before creating any Trellis task.
Simple conversation / small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session.
Complex task: ask the user if you can create a Trellis task and enter the planning phase. If the user says no, explain, clarify scope, or suggest a smaller split.
[/workflow-state:no_task]
### Phase 1: Plan
- 1.0 Create task `[required · once]` (just `task.py create`; status enters planning)
- 1.1 Requirement exploration `[required · repeatable]`
- 1.0 Create task `[required · once]` (only after task-creation consent)
- 1.1 Requirement exploration `[required · repeatable]` (`prd.md`; complex tasks also need `design.md` + `implement.md`)
- 1.2 Research `[optional · repeatable]`
- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi
- 1.4 Activate task `[required · once]` (run `task.py start`; status → in_progress)
- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Grok, Kimi Code (sub-agent-dispatch platforms only; inline platforms skip)
- 1.4 Activate task `[required · once]` (review gate, then `task.py start`; status → in_progress)
- 1.5 Completion criteria
<!-- Per-turn breadcrumb: shown throughout Phase 1 (status='planning') -->
[workflow-state:planning]
Load the `trellis-brainstorm` skill and iterate on prd.md with the user.
Phase 1.3 (required, once): before `task.py start`, you MUST curate `implement.jsonl` and `check.jsonl` — list the spec / research files sub-agents need so they get the right context injected. You may skip only if the jsonl already has agent-curated entries (the seed `_example` row alone doesn't count).
Then run `task.py start <task-dir>` to flip status to in_progress.
Load `trellis-brainstorm`; stay in planning.
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`.
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
Sub-agent mode: curate `implement.jsonl` and `check.jsonl` as spec/research manifests before start.
[/workflow-state:planning]
<!-- Per-turn breadcrumb: shown throughout Phase 1 when codex.dispatch_mode=inline.
Codex-only opt-in alternate to [workflow-state:planning]. The main agent
edits code directly in Phase 2, so Phase 1.3 jsonl curation is skipped —
edits code directly in Phase 2, so jsonl curation is skipped —
the inline workflow loads `trellis-before-dev` instead of injecting JSONL
into a sub-agent. -->
[workflow-state:planning-inline]
Load the `trellis-brainstorm` skill and iterate on prd.md with the user.
Phase 1.3 jsonl curation is **skipped** in inline dispatch mode — the main session loads `trellis-before-dev` directly in Phase 2 and reads spec context itself, so there is no sub-agent to inject jsonl into.
Then run `task.py start <task-dir>` to flip status to in_progress.
Load `trellis-brainstorm`; stay in planning.
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`.
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
Inline mode: skip jsonl curation; Phase 2 reads artifacts/specs via `trellis-before-dev`.
[/workflow-state:planning-inline]
### Phase 2: Execute
@@ -220,18 +241,18 @@ Then run `task.py start <task-dir>` to flip status to in_progress.
- 2.3 Rollback `[on demand]`
<!-- Per-turn breadcrumb: shown while status='in_progress'.
Scope: all of Phase 2 + Phase 3.1-3.4 (status stays 'in_progress' from
Scope: all of Phase 2 + Phase 3.2-3.4 (status stays 'in_progress' from
task.py start until task.py archive; only archive flips it). The body
therefore must cover every required step from implementation through
commit, including Phase 3.3 spec update and Phase 3.4 commit. -->
Sub-agent dispatch protocol applies to all platforms and all sub-agents, including native Codex `SubagentStart` context injection with child-side pull fallback, class-2 Gemini/Qoder/Copilot/Reasonix/Trae/Grok/Kimi Code, hook-backed ZCode/Snow, and `trellis-research`: every dispatch prompt starts with `Active task: <task path from task.py current>` before role-specific instructions. On Grok Build, use `spawn_subagent` with `subagent_type` set to the Trellis agent name (e.g. `trellis-implement`). On Kimi Code, dispatch the built-in `coder` / `explore` sub-agent with the matching `.kimi-code/skills/trellis-<role>/SKILL.md` instructions.
[workflow-state:in_progress]
**Tools**: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill — there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes.
**Flow**: trellis-implement → trellis-check → trellis-update-spec → commit (Phase 3.4) → `/trellis:finish-work`.
**Main-session default (no override)**: dispatch the `trellis-implement` / `trellis-check` sub-agents — the main agent does NOT edit code by default. Phase 3.4 commit (required, once): after trellis-update-spec, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`).
**Sub-agent self-exemption**: if you are already running as `trellis-implement`, implement directly from the loaded task context and do NOT spawn another `trellis-implement`; if you are already running as `trellis-check`, review/fix directly and do NOT spawn another `trellis-check`. The default dispatch rule applies to the main session only.
**Sub-agent dispatch protocol (all platforms, all sub-agents)**: When you spawn `trellis-implement` / `trellis-check` / `trellis-research`, your dispatch prompt **MUST** start with one line: `Active task: <task path from \`task.py current\`>`. No exceptions. On class-2 platforms (codex / copilot / gemini / qoder) the sub-agent depends on this line because there is no hook to inject task context. On class-1 platforms (claude / cursor / opencode / kiro / codebuddy / droid) the line is normally redundant — the hook injects context directly — but it serves as a critical fallback when the hook fails (Windows + Claude Code PreToolUse silent skip, `--continue` resume, fork distribution, hooks disabled, etc.). For `trellis-research`, the line tells the sub-agent which `{task_dir}/research/` to write into.
**Inline override** (per-turn only, escape hatch for sub-agent dispatch): the user's CURRENT message MUST explicitly contain one of: "do it inline" / "no sub-agent" / "你直接改" / "别派 sub-agent" / "main session 写就行" / "不用 sub-agent". **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said.
Tools: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill; there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes.
Flow: `trellis-implement` -> `trellis-check` -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`.
Main-session default: dispatch implement/check sub-agents. Sub-agent self-exemption: if already running as `trellis-implement`, do NOT spawn another `trellis-implement` or `trellis-check`; if already running as `trellis-check`, do NOT spawn another `trellis-check` or `trellis-implement`. Dispatch is main session only.
Dispatch prompt starts with `Active task: <task path from task.py current>`. Read context: jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`.
[/workflow-state:in_progress]
<!-- Per-turn breadcrumb: shown while status='in_progress' when
@@ -240,18 +261,19 @@ Then run `task.py start <task-dir>` to flip status to in_progress.
instead of dispatching sub-agents. -->
[workflow-state:in_progress-inline]
**Flow** (inline mode): main session loads `trellis-before-dev` → main session edits code → main session loads `trellis-check` → run lint / type-check / tests → fix → `trellis-update-spec` → commit (Phase 3.4) → `/trellis:finish-work`.
**Main-session default (inline dispatch_mode)**: the main agent edits code directly. Do NOT dispatch `trellis-implement` / `trellis-check` sub-agents. Load the `trellis-before-dev` skill before writing code; load the `trellis-check` skill before reporting completion.
Phase 3.4 commit (required, once): after `trellis-update-spec`, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`).
Flow: `trellis-before-dev` -> edit -> `trellis-check` -> validation -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`.
Do not dispatch implement/check sub-agents in inline mode.
Read context: `prd.md` -> `design.md if present` -> `implement.md if present`, plus relevant spec/research loaded by skills.
[/workflow-state:in_progress-inline]
### Phase 3: Finish
- 3.1 Quality verification `[required · repeatable]`
- 3.2 Debug retrospective `[on demand]`
- 3.3 Spec update `[required · once]`
- 3.4 Commit changes `[required · once]`
- 3.5 Wrap-up reminder
> Note: step 3.1 was folded into 2.2 (last-iteration full-scope check) and 3.4 (commit preamble). Numbering kept stable to avoid breaking external references.
<!-- Per-turn breadcrumb: shown while status='completed'.
Currently DEAD in normal flow: cmd_archive writes status='completed' in
the same call that moves the task dir to archive/, so the active-task
@@ -261,9 +283,7 @@ Phase 3.4 commit (required, once): after `trellis-update-spec`, or whenever impl
channel as the live blocks. -->
[workflow-state:completed]
Code committed via Phase 3.4; run `/trellis:finish-work` to wrap up (archive the task + record session).
If you reach this state with uncommitted code, return to Phase 3.4 first — `/finish-work` refuses to run on a dirty working tree.
`task.py archive` deletes any runtime session files that still point at the archived task.
Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first.
[/workflow-state:completed]
### Rules
@@ -272,60 +292,33 @@ If you reach this state with uncommitted code, return to Phase 3.4 first — `/f
2. Run steps in order inside each Phase; `[required]` steps can't be skipped
3. Phases can roll back (e.g., Execute reveals a prd defect → return to Plan to fix, then re-enter Execute)
4. Steps tagged `[once]` are skipped if the output already exists; don't re-run
5. Artifact presence informs the next step; missing `design.md` / `implement.md` is valid for lightweight tasks and incomplete planning for complex tasks.
### Skill Routing
### Active Task Routing
When a user request matches one of these intents, load the corresponding skill (or dispatch the corresponding sub-agent) first — do not skip skills.
When a user request matches one of these intents inside an active task, route first, then load the detailed phase step if needed.
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
| User intent | Route |
|---|---|
| Wants a new feature / requirement unclear | `trellis-brainstorm` |
| About to write code / start implementing | Dispatch the `trellis-implement` sub-agent per Phase 2.1 |
| Finished writing / want to verify | Dispatch the `trellis-check` sub-agent per Phase 2.2 |
| Stuck / fixed same bug several times | `trellis-break-loop` |
| Spec needs update | `trellis-update-spec` |
- Planning or unclear requirements -> `trellis-brainstorm`.
- `in_progress` implementation/check -> dispatch `trellis-implement` / `trellis-check`.
- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`.
**Why `trellis-before-dev` is NOT in this table:** you are not the one writing code — the `trellis-implement` sub-agent is. Sub-agent platforms get spec context via `implement.jsonl` injection / prelude, not via the main thread loading `trellis-before-dev`.
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[codex-inline, Kilo, Antigravity, Devin]
[codex-inline, Kilo, Antigravity, Windsurf]
- Planning or unclear requirements -> `trellis-brainstorm`.
- Before editing -> `trellis-before-dev`; after editing -> `trellis-check`.
- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`.
| User intent | Skill |
|---|---|
| Wants a new feature / requirement unclear | `trellis-brainstorm` |
| About to write code / start implementing | `trellis-before-dev` (then implement directly in the main session) |
| Finished writing / want to verify | `trellis-check` |
| Stuck / fixed same bug several times | `trellis-break-loop` |
| Spec needs update | `trellis-update-spec` |
[/codex-inline, Kilo, Antigravity, Devin]
[/codex-inline, Kilo, Antigravity, Windsurf]
### Guardrails
### DO NOT skip skills
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
| What you're thinking | Why it's wrong |
|---|---|
| "This is simple, I'll just code it in the main thread" | Dispatching `trellis-implement` is the cheap path; skipping it tempts you to write code in the main thread and lose spec context — sub-agents get `implement.jsonl` injected, you don't |
| "I already thought it through in plan mode" | Plan-mode output lives in memory — sub-agents can't see it; must be persisted to prd.md |
| "I already know the spec" | The spec may have been updated since you last read it; the sub-agent gets the fresh copy, you may not |
| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper |
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[codex-inline, Kilo, Antigravity, Windsurf]
| What you're thinking | Why it's wrong |
|---|---|
| "This is simple, just code it" | Simple tasks often grow complex; `trellis-before-dev` takes under a minute and loads the spec context you'll need |
| "I already thought it through in plan mode" | Plan-mode output lives in memory — must be persisted to prd.md before code |
| "I already know the spec" | The spec may have been updated since you last read it; read again |
| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper |
[/codex-inline, Kilo, Antigravity, Windsurf]
- Task creation approval is not implementation approval; implementation waits for `task.py start` after artifact review.
- PRD-only is valid for lightweight tasks; complex tasks need `design.md` + `implement.md`.
- Planning must be persisted to task artifacts; checks must run before reporting completion.
### Loading Step Detail
@@ -340,11 +333,11 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <step>
## Phase 1: Plan
Goal: figure out what to build, produce a clear requirements doc and the context needed to implement it.
Goal: classify the request, get task-creation consent when a task is needed, and produce the planning artifacts required before implementation.
#### 1.0 Create task `[required · once]`
Create the task directory (status enters `planning`, the session active-task pointer auto-targets the new task when session identity is available):
Create the task directory only after task-creation consent. The command sets status to `planning`, writes `task.json`, creates a default `prd.md`, and auto-targets the new task when session identity is available:
```bash
python3 ./.trellis/scripts/task.py create "<task title>" --slug <name>
@@ -352,9 +345,11 @@ python3 ./.trellis/scripts/task.py create "<task title>" --slug <name>
`--slug` is the human-readable name only. Do **not** include the `MM-DD-` date prefix; `task.py create` adds that prefix automatically.
After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to enter the brainstorm + jsonl curation phase.
For task trees, create the parent task first and then create each child with `--parent <parent-dir>`. Do not start the parent just because children exist; start the child that owns the next independently verifiable deliverable.
⚠️ **Run only `create` here — do not also run `start`**. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before brainstorm + jsonl are done — the AI will silently skip them. Save `start` for step 1.4, after jsonl curation is complete.
After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to stay in planning.
Run only `create` here — do not also run `start`. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before planning artifacts are reviewed. Save `start` for step 1.4.
Skip when `python3 ./.trellis/scripts/task.py current --source` already points to a task.
@@ -367,14 +362,24 @@ The brainstorm skill will guide you to:
- Prefer researching over asking the user
- Prefer offering options over open-ended questions
- Update `prd.md` immediately after each user answer
- Split large scopes into a parent task plus child tasks when the deliverables can be verified independently
- Keep `prd.md` focused on requirements and acceptance criteria
- For complex tasks, produce `design.md` and `implement.md` before implementation starts
Return to this step whenever requirements change and revise `prd.md`.
When considering a parent/child split:
- Use a parent task when one request contains several independently verifiable deliverables.
- Parent tasks own source requirements, child-task mapping, cross-child acceptance criteria, and final integration review.
- Child tasks own actual deliverables that can be planned, implemented, checked, and archived independently.
- Parent/child structure is not a dependency system. If child B depends on child A, write that ordering in child B's `prd.md` / `implement.md`.
- Start the child task that owns the next deliverable. Do not start the parent unless the parent itself has direct implementation work.
Return to this step whenever requirements change and revise the relevant artifact.
#### 1.2 Research `[optional · repeatable]`
Research can happen at any time during requirement exploration. It isn't limited to local code — you can use any available tool (MCP servers, skills, web search, etc.) to look up external information, including third-party library docs, industry practices, API references, etc.
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
Spawn the research sub-agent:
@@ -382,13 +387,13 @@ Spawn the research sub-agent:
- **Task description**: Research <specific question>
- **Key requirement**: Research output MUST be persisted to `{TASK_DIR}/research/`
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
[codex-inline, Kilo, Antigravity, Windsurf]
[codex-inline, Kilo, Antigravity, Devin]
Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. (For `codex-inline` this avoids the `fork_turns="none"` isolation that prevents `trellis-research` sub-agents from resolving the active task path.)
Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. `codex-inline` is the explicit mode that keeps work in the main session.
[/codex-inline, Kilo, Antigravity, Windsurf]
[/codex-inline, Kilo, Antigravity, Devin]
**Research artifact conventions**:
- One file per research topic (e.g. `research/auth-library-comparison.md`)
@@ -401,9 +406,9 @@ Brainstorm and research can interleave freely — pause to research a technical
#### 1.3 Configure context `[required · once]`
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries.
Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec/research context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries.
**Location**: `{TASK_DIR}/implement.jsonl` and `{TASK_DIR}/check.jsonl` (already exist).
@@ -421,6 +426,8 @@ Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the rig
- `implement.jsonl` → specs + research the implement sub-agent needs to write code correctly
- `check.jsonl` → specs for the check sub-agent (quality guidelines, check conventions, same research if needed)
These manifests do not replace `implement.md`. `implement.md` is the human-readable execution plan for a complex task; jsonl files only list context files to inject or load.
**How to discover relevant specs**:
```bash
@@ -440,24 +447,28 @@ python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reas
Delete the seed `_example` line once real entries exist (optional — it's skipped automatically by consumers).
Skip when: `implement.jsonl` has agent-curated entries (the seed row alone doesn't count).
Ready gate: both `implement.jsonl` and `check.jsonl` must contain at least one real `{"file": "...", "reason": "..."}` entry before `task.py start`. The seed `_example` row alone is not ready.
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
Skip this step only when both files already have real curated entries.
[codex-inline, Kilo, Antigravity, Windsurf]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
[codex-inline, Kilo, Antigravity, Devin]
Skip this step. Context is loaded directly by the `trellis-before-dev` skill in Phase 2.
[/codex-inline, Kilo, Antigravity, Windsurf]
[/codex-inline, Kilo, Antigravity, Devin]
#### 1.4 Activate task `[required · once]`
Once prd.md is complete and 1.3 jsonl curation is done, flip the task status to `in_progress`:
After artifact review, flip the task status to `in_progress`:
```bash
python3 ./.trellis/scripts/task.py start <task-dir>
```
For lightweight tasks, `prd.md` can be enough. For complex tasks, `prd.md`, `design.md`, and `implement.md` must exist and be reviewed before start. On sub-agent-dispatch platforms, `implement.jsonl` and `check.jsonl` must both have real curated entries before start. Runtime consumers tolerate missing or seed-only manifests for compatibility, but that tolerance is not a planning-ready state.
After this command succeeds, the breadcrumb auto-switches to `[workflow-state:in_progress]`, and the rest of Phase 2 / 3 follows.
If `task.py start` errors with a session-identity message (no context key from hook input, `TRELLIS_CONTEXT_ID`, or platform-native session env), follow the hint in the error to set up session identity, then retry.
@@ -467,95 +478,98 @@ If `task.py start` errors with a session-identity message (no context key from h
| Condition | Required |
|------|:---:|
| `prd.md` exists | ✅ |
| User confirms requirements | ✅ |
| User confirms task should enter implementation | ✅ |
| `task.py start` has been run (status = in_progress) | ✅ |
| `research/` has artifacts (complex tasks) | recommended |
| `info.md` technical design (complex tasks) | optional |
| `design.md` exists (complex tasks) | ✅ |
| `implement.md` exists (complex tasks) | ✅ |
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
| `implement.jsonl` has agent-curated entries (not just the seed row) | ✅ |
| `implement.jsonl` and `check.jsonl` each contain at least one real curated entry (seed row does not count) | ✅ |
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
---
## Phase 2: Execute
Goal: turn the prd into code that passes quality checks.
Goal: turn reviewed planning artifacts into code that passes quality checks.
#### 2.1 Implement `[required · repeatable]`
[Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, CodeBuddy, Droid, Pi, ZCode, Snow, Oh My Pi]
Spawn the implement sub-agent:
- **Agent type**: `trellis-implement`
- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`.
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`.
The platform hook/plugin auto-handles:
- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt
- Injects prd.md content
- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt
- Injects `prd.md`, `design.md` if present, and `implement.md` if present
- For Codex, `SubagentStart` supplies native context injection; the agent profile keeps child-side loading as the fallback
[/Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, CodeBuddy, Droid, Pi, ZCode, Snow, Oh My Pi]
[codex-sub-agent]
[Gemini, Qoder, Copilot, Reasonix, Trae, Grok, Kimi Code]
Spawn the implement sub-agent:
- **Agent type**: `trellis-implement`
- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then explicitly say the spawned agent is already `trellis-implement` and must implement directly without spawning another `trellis-implement` / `trellis-check`.
The Codex sub-agent definition auto-handles the context load requirement:
- Resolves the active task with `task.py current --source`, then reads `prd.md` and `info.md` if present
- Reads `implement.jsonl` and requires the agent to load each referenced spec file before coding
The pull-based sub-agent definition auto-handles the context load requirement:
- Resolves the active task with `task.py current --source`, then reads `prd.md`, `design.md` if present, and `implement.md` if present
- Reads `implement.jsonl` and requires the agent to load each referenced spec/research file before coding
[/codex-sub-agent]
[/Gemini, Qoder, Copilot, Reasonix, Trae, Grok, Kimi Code]
[Kiro]
Spawn the implement sub-agent:
- **Agent type**: `trellis-implement`
- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`.
The platform prelude auto-handles the context load requirement:
- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt
- Injects prd.md content
- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt
- Injects `prd.md`, `design.md` if present, and `implement.md` if present
[/Kiro]
[codex-inline, Kilo, Antigravity, Windsurf]
[codex-inline, Kilo, Antigravity, Devin]
1. Load the `trellis-before-dev` skill to read project guidelines
2. Read `{TASK_DIR}/prd.md` for requirements
2. Read `{TASK_DIR}/prd.md`, then `design.md` if present, then `implement.md` if present
3. Consult materials under `{TASK_DIR}/research/`
4. Implement the code per requirements
4. Implement the code per reviewed artifacts
5. Run project lint and type-check
[/codex-inline, Kilo, Antigravity, Windsurf]
[/codex-inline, Kilo, Antigravity, Devin]
#### 2.2 Quality check `[required · repeatable]`
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
Spawn the check sub-agent:
- **Agent type**: `trellis-check`
- **Task description**: Review all code changes against spec and prd; fix any findings directly; ensure lint and type-check pass
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`.
- **Task description**: Review all code changes against specs and task artifacts; fix any findings directly; ensure lint and type-check pass
- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`.
The check agent's job:
- Review code changes against specs
- Review code changes against `prd.md`, `design.md` if present, and `implement.md` if present
- Auto-fix issues it finds
- Run lint and typecheck to verify
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi]
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code]
[codex-inline, Kilo, Antigravity, Windsurf]
[codex-inline, Kilo, Antigravity, Devin]
Load the `trellis-check` skill and verify the code per its guidance:
- Spec compliance
@@ -564,7 +578,9 @@ Load the `trellis-check` skill and verify the code per its guidance:
If issues are found → fix → re-check, until green.
[/codex-inline, Kilo, Antigravity, Windsurf]
[/codex-inline, Kilo, Antigravity, Devin]
**Final pass (before Phase 3.4 commit)**: the last 2.2 of a task must run full-scope, not just on the latest implement chunk. List all affected packages with `python3 ./.trellis/scripts/get_context.py --mode packages`, then load each package's spec index Quality Check section. This catches cross-layer / multi-package issues a mid-iteration local 2.2 cannot.
#### 2.3 Rollback `[on demand]`
@@ -578,15 +594,6 @@ If issues are found → fix → re-check, until green.
Goal: ensure code quality, capture lessons, record the work.
#### 3.1 Quality verification `[required · repeatable]`
Load the `trellis-check` skill and do a final verification:
- Spec compliance
- lint / type-check / tests
- Cross-layer consistency (when changes span layers)
If issues are found → fix → re-check, until green.
#### 3.2 Debug retrospective `[on demand]`
If this task involved repeated debugging (the same issue was fixed multiple times), load the `trellis-break-loop` skill to:
@@ -607,6 +614,8 @@ Update the docs under `.trellis/spec/` accordingly. Even if the conclusion is "n
#### 3.4 Commit changes `[required · once]`
**Spec-sync preamble**: before drafting commits, ask: did this task fix a bug or surface non-obvious knowledge that should land in `.trellis/spec/` so future-you (or future-AI) doesn't repeat the mistake? If yes, return to Phase 3.3 first — spec writes belong in the same task's commit batch, not as a forgotten follow-up.
The AI drives a batched commit of this task's code changes so `/finish-work` can run cleanly afterwards. Goal: produce work commits FIRST, then bookkeeping (archive + journal) commits land after — never interleaved.
**Step-by-step**:
@@ -667,15 +676,20 @@ This section is for developers who want to modify the Trellis workflow itself. A
### Changing what a step means
Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. **Critical constraint**: if you change a step's `[required · once]` marker or add a new `[required · once]` step, you MUST also add a matching enforcement line to that phase's `[workflow-state:STATUS]` tag block — otherwise the per-turn breadcrumb omits the reinforcement, and the AI silently skips the step. The regression tests assert this.
Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. Critical invariants:
- No active task must triage first and ask for task-creation consent before creating a Trellis task.
- Planning must distinguish lightweight PRD-only tasks from complex tasks that require `prd.md`, `design.md`, and `implement.md` before start.
- Every required execution path must keep the Phase 3.4 commit reminder reachable before `/trellis:finish-work`.
All 4 tag blocks live in the `## Phase Index` section above, immediately after each phase summary:
All tag blocks live in the `## Phase Index` section above, immediately after each phase summary:
| Scope | Corresponding tag |
|---|---|
| No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) |
| All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) |
| Phase 2 + Phase 3.1–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) |
| Codex inline Phase 1 | `[workflow-state:planning-inline]` |
| Phase 2 + Phase 3.2–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) |
| Codex inline Phase 2 + Phase 3.2–3.4 | `[workflow-state:in_progress-inline]` |
| After Phase 3.5 (archived) | `[workflow-state:completed]` (after Phase 3 summary; **currently DEAD**) |
### Changing the per-turn prompt text