@yunhozz/arc 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MANIFEST.sha256 CHANGED
@@ -1,22 +1,23 @@
1
- cbd254348f60c8610d0e5c4b8216c9772c0bd5dbfef324796da516b87054b06e README.md
1
+ 85cea3cfddbf92091b1eec35feaea339e70fc2a1faa65099c99cc6318aec1cc2 README.md
2
+ d41edeb8c6631dee30c67c705d4edad977aa1631627ec9de59c013f8990f4018 bin/arc-upgrade.mjs
2
3
  484d9b2002ce542c87589bb719d644d3762706aad28a6ee010024875f8cc00e2 bin/arc.mjs
3
4
  d853d31ba0d1a04661adf8cae1df11058455cb5d366c52312888c9dd2e9a1fb8 claude/agents/arc-coder.md
4
5
  91a2c62f4ea3ff00145407411fbbe62aa02fb7608af8f33bbd3d1719fc9d2f13 claude/agents/arc-planner.md
5
6
  d864e57a1b2b6d69435a17a5c56a0e2c68d26592dd5e510be8d0fa341762cad1 claude/agents/arc-qa.md
6
7
  bd51442a5e76d4b61cbeb7f42d43cbcc7bab890db0d39ef16002ae13f5317c34 claude/agents/arc-reviewer.md
7
8
  c20162438bb8ac6905164268f867a8f1cfdbbfd51ddfd7e795cc211cc7f8c858 claude/agents/arc-scouter.md
8
- 525c92fb52cc45c2f779176629e11c3a21f4e4434648cd5d6e4f6d7657f536f6 claude/skill/SKILL.md
9
+ c5002996b3feb406c52dcbb305631e98d5206176c4617933763c6f2e17da6bb3 claude/skill/SKILL.md
9
10
  0bf77247b82371b3fe12a8dbe8fd2415c98d06e676d2c234b086b1f1e661b9db claude/skill/hooks/git-policy.json
10
11
  b188c24d1339367acfe16b81be5743298e498856915e5e91b45e1a7220dad602 claude/skill/hooks/role-git-guard.mjs
11
12
  5126d44470486cb2581790b7c8d1605c4d0ee2448b725cf536ba1141eb40fa81 claude/skill/hooks/runtime-gate.mjs
12
13
  3926dbf9c7c244ebbb0baee17b1163387979793cb81c28e90977473d1e0a85e9 claude/skill/references/commit.md
13
- 33ccd670b9d365abcc03edbfa26761ad00970df8e46633799beee4e72e6148fe claude/skill/references/coordination.md
14
+ 61dc37bd08a9004e67cc7b76102cf3cbcb170a08d5d3576f27dcc39dec47a692 claude/skill/references/coordination.md
14
15
  90198f8bc6d9e99cbdb32604413c708753ff8503954b6b120aa7c7f00719de96 claude/skill/references/git-recovery.md
15
16
  d4b3100626831b23f0b1cf0106ef9f3bc7e6f3d14864e9dd93ae0f8063b5bdda claude/skill/references/handoff-prep.md
16
17
  42a13f77f2b28d660236237dff88cab166f052a178295cef253a774fd6fc8e40 claude/skill/references/handoff.md
17
18
  4172c1c8ba397ceb561e437cc6ef02d4c2d7e7ed3df1400a4efbf4b46ddca4fa claude/skill/references/limits.md
18
19
  93a7001c6ad4215170fa331261c2d58835fcdad823724f024d5c81a450133340 claude/skill/references/planning.md
19
- 78704f725dd82bf49f7916ef5522e8c52040ccb05e109b568796d8a9f6e52e6e claude/skill/references/verification.md
20
+ d25178b548e5bccd7ee5c9efea1b48e8a2f3a6275b6572ab648566eada309c2b claude/skill/references/verification.md
20
21
  cae43b5105a1300e87526bcc286516519863b2f8c5f70cd6fe1e0950b196a762 claude/skill/references/visual-companion.md
21
22
  30d8cdcb9d7ab6396cab09cd9f5cf6b8003f5490fb5c0ffe9f5acffbb0534978 claude/skill/runtime/adapters/claude.mjs
22
23
  d30d118d3b42b805195ed05a3dea0ed3cf939eac811cf0228ca0b0cafeda7969 claude/skill/runtime/adapters/codex.mjs
@@ -27,7 +28,7 @@ b210df233983ffef326d65ae41a096fdef1b53d1e300ada3b217de3524686701 claude/skill/r
27
28
  494470cf70121ddac8d716586b0c05b1f0977f3eada8c0323ed34dc157b0db8e claude/skill/runtime/git-query.mjs
28
29
  cde1d91eb252e212e1985cee4bac0753964ba1c507202c711adab88f020e474d claude/skill/runtime/identity.mjs
29
30
  23339b1d2ac83c673ded090f993dee7985c785442cababdb6a3a224163451eef claude/skill/runtime/operations.mjs
30
- c93f08cf6cafb5d96007df33d00dadda676c54b074fb998c2e523ecfc5e23d30 claude/skill/runtime/role-policy.json
31
+ 46b4e793c538f858b3a3e46087cd140a5a34154e6ecf5381c22b71250f289904 claude/skill/runtime/role-policy.json
31
32
  71d5696e0523f2f915a2de02cf3ded453b27783b9e09d2b98ec4aef1958a26a4 claude/skill/runtime/schema.mjs
32
33
  aa1361419c0137f74793847805a3abc076a8d782c6f590e2d28b8eee5c35065d claude/skill/runtime/store.mjs
33
34
  e4445f3582c55861907e11dfe97ea9e87c69bcc3ff773d3d583914c198f1d024 claude/skill/scripts/apply-models.py
@@ -37,17 +38,17 @@ d9c718e4a1cce5fef343afbc623ebb82fd29b85f40270b84a0f1770f57a61a50 codex/agents/a
37
38
  793d132db240208729c2d54068634e872ead72b573c0cd4648a6e2f50e5cc56c codex/agents/arc-qa.toml
38
39
  022f1abd7fd325ab543f6c7c5263a1d0c973be417a39ee8f3bb3295b44749fa3 codex/agents/arc-reviewer.toml
39
40
  0dc1905c3bb16e3b7117d9b38cab0c1e6e93f496afdb82f782ef830f1f3a6417 codex/agents/arc-scouter.toml
40
- 79a113eebcaff301656fdc52f3713f0dad909ce7b55d6a0357504ca20eb48549 codex/skill/SKILL.md
41
+ e80d96e74664741e9c6659b73c0ed5327ab0148e7b0f81a7eb7683ff43b8f094 codex/skill/SKILL.md
41
42
  019f190ee7a2ccfea059a289ca971265574b043353f89bc96e219436c9827e9e codex/skill/hooks/runtime-gate.mjs
42
43
  ce8ab4dd1a69176fc4d76b2e9e3c81b9165586c3a29833fe855f2a085f22195e codex/skill/references/commit.md
43
- 263b4d59d67bc365bcf9e658ebfba1d3896662c5fcdb0f45a87a30fd6b0fd43a codex/skill/references/coordination.md
44
+ 1aa3442643111a79985cb29ea90ca3fc357b989aaf37a1540115864d3e9c3acd codex/skill/references/coordination.md
44
45
  be0a2e4050a0cad20d272775698be82dcc244081c4c1092de56e8a118de718b1 codex/skill/references/git-recovery.md
45
46
  39ce5bb81d1fa303394c8c2c6b0d306d9fc341a866654fdabb02a33e6f5c2dee codex/skill/references/handoff-prep.md
46
47
  272b60b91ac05139985611d1555c417eab3ca1783655a9196511a0eda02caaba codex/skill/references/handoff-worktree.md
47
48
  3338d9080415c3cdd4c7a5337f5c1f1d74e9f6cfb084181fe9cce78d906a1162 codex/skill/references/handoff.md
48
49
  89ae51633112f8c7bd71ab15940085e01e57d6582fb0d977c9efdf9d77ce9fc3 codex/skill/references/limits.md
49
50
  ed40f33754e4b6df683fe567b91974a381cf99ebd5ec772b493a71f17f01d5a7 codex/skill/references/planning.md
50
- 3b89adb5bfbf91ac4937b41cdd3a5ebb2ffb652826fff9d834489bd377245e2c codex/skill/references/verification.md
51
+ 092ecc14f10c45a2048499270340074b52c1cb28c3807c55f50abe63053db4eb codex/skill/references/verification.md
51
52
  23d928dc115b922e5418db9c89057dab2f19acb7c946420a55923d3bb5148417 codex/skill/references/visual-companion.md
52
53
  30d8cdcb9d7ab6396cab09cd9f5cf6b8003f5490fb5c0ffe9f5acffbb0534978 codex/skill/runtime/adapters/claude.mjs
53
54
  d30d118d3b42b805195ed05a3dea0ed3cf939eac811cf0228ca0b0cafeda7969 codex/skill/runtime/adapters/codex.mjs
@@ -58,19 +59,22 @@ b210df233983ffef326d65ae41a096fdef1b53d1e300ada3b217de3524686701 codex/skill/ru
58
59
  494470cf70121ddac8d716586b0c05b1f0977f3eada8c0323ed34dc157b0db8e codex/skill/runtime/git-query.mjs
59
60
  cde1d91eb252e212e1985cee4bac0753964ba1c507202c711adab88f020e474d codex/skill/runtime/identity.mjs
60
61
  23339b1d2ac83c673ded090f993dee7985c785442cababdb6a3a224163451eef codex/skill/runtime/operations.mjs
61
- c93f08cf6cafb5d96007df33d00dadda676c54b074fb998c2e523ecfc5e23d30 codex/skill/runtime/role-policy.json
62
+ 46b4e793c538f858b3a3e46087cd140a5a34154e6ecf5381c22b71250f289904 codex/skill/runtime/role-policy.json
62
63
  71d5696e0523f2f915a2de02cf3ded453b27783b9e09d2b98ec4aef1958a26a4 codex/skill/runtime/schema.mjs
63
64
  aa1361419c0137f74793847805a3abc076a8d782c6f590e2d28b8eee5c35065d codex/skill/runtime/store.mjs
64
65
  57e93213dc97465757294cb9d0ae2f4eb2f1384b955c4ca2a39f2fa38f7a9bd7 codex/skill/scripts/apply-models.py
65
66
  bce13feeea37c78e5b0ddaf6bdd5ffd2fd0162eabc8c468667133edbabebe944 codex/skill/scripts/model_config.py
66
67
  800964b417c573aa7b2a07a76759a4750652222de63a985bc133d50194739358 install.command
67
- 92e329fcfb26899f524c31ce6cdb08997c274560b271d80d230ae303d6987f17 lib/cli/choices.mjs
68
- 7833879c00f296248bb37a367564ad0c5db07e2c441873a0b4e6a3c3733ca137 lib/cli/completion.mjs
69
- 6655cf91a3af427378755299433feb3f0086d43b047c4f6cd5a2c429f2812dba lib/cli/index.mjs
68
+ d8ed829763544877436f0e6bf349c5460db0c44cce73b75d38c5661c902536c4 lib/cli/choices.mjs
69
+ 640d1bfdda880c2f17149209ce35933ee9f4318b35166df2e86ff49e271d8fc5 lib/cli/completion.mjs
70
+ 0e0d5135fce87d00bda6a92c03ca39ad542841f80a785674d3f7ac446dbce014 lib/cli/index.mjs
71
+ fe34003f994ab962f60f6ec6e04e6c5f812ceb00936cef3d25365bc4c672707b lib/cli/postinstall.mjs
70
72
  515132cf88973720bb6197a849d896face56c23f3c7fc4681ae9123ffc576318 lib/cli/preset.mjs
71
73
  a575a94adc9461a7c5c051983b47d1a40a88fb8ee90fa469041cd99165cbb80f lib/cli/progress.mjs
72
- ad8ad2f47da6e1d3160d786628d768e8d95108f1a1c113a6a5305e7c85c438db lib/cli/root.mjs
74
+ b5497f312d0ad84efc5328def88197adffa239a18f4746ef12ea20059e36d6a8 lib/cli/root.mjs
75
+ 034a93e4127e303880dc347488e46214702b343da06f9ba7a7a6c757473622a5 lib/cli/terminal.mjs
73
76
  85f25429f208a4b45a36e66f89933d2bbfb470d6f52f032f70971817063e45f0 lib/cli/uninstall.mjs
77
+ 4fadbd52e4ecd0d95c7240037c84aa74714e1f9f764440e31d3ce3402c23b5b6 lib/cli/upgrade.mjs
74
78
  7719a16f6327ea3ba35206ee41c829ab967f865df9ebbd161129ca28babceb3d lib/installer/acquisition.mjs
75
79
  66bc9a18523f70ddbb1d9a0844173d311aabd2d00d9ed5c06ca6679179ba8b06 lib/installer/global-config.mjs
76
80
  061ff82e0c655f47b872610b6ad7bde5d1941eaf7c563491a44291c86b16a349 lib/installer/install.mjs
@@ -78,7 +82,7 @@ f9a0958fc424742f49ce6e2e0a49c7cf2bbe288e0cf3551d69a8a912c8f2709f lib/installer/
78
82
  64713575291e51971abc542df716baa99e9a530a489e92b2f8345b782e0ce4cd lib/installer/limits.mjs
79
83
  cec99b4e9c6e6b6c634c84fba978b93484011b6357b652b99b31612ac522a22d lib/installer/manifest.mjs
80
84
  1e6667f21dfa6eb427d7447df98038bbe534345ebb57d6b401f51efe1077f1ed lib/installer/package-references.mjs
81
- 92065a84b0f252cc8e90a76b24f110c615c11035e45e073fb00bb7cb5de83bdf lib/installer/plan.mjs
85
+ 383390bb5a73d3db85d4cfe7f8ae0502925552eacc66c849c0061cd82e487ebd lib/installer/plan.mjs
82
86
  5a7860583cadb57295bdac8296a6c8caa4cedf0d1823e7d08c57846ed8342ba5 lib/installer/preset.mjs
83
87
  4892d61cfe498a85f47f4a8044a831c853660365c98543eb14f4f4449b727c2f lib/installer/runtime-config.mjs
84
88
  e37be4e8b6f366201f42aeab5d7973baa8303b1f96461ee2ced6ce2e73da3bd2 lib/installer/shell-completion.mjs
@@ -87,14 +91,20 @@ c2ab7eda987cf62bba4dce1b7eb302f350a6c67679b0aaf063b3eea11eb010c5 lib/installer/
87
91
  07805edb471c54b87dcc827cf24264571a6427961989d508773b9751a39dc1f0 lib/installer/targets.mjs
88
92
  65ba12b4786b8b8c2cd65731a96475a64d1888644e8c68825bc369d74c266ab8 lib/installer/transaction.mjs
89
93
  f0606cd2cd4871d11e544b5ad8221e4d9050761d1d80ab94c9219eb7e494b375 lib/installer/uninstall.mjs
94
+ b83205b8244edd9868c91ffbf19864f84a51556b16e87c219b39916b8c31ed51 lib/installer/upgrade-state.mjs
95
+ 6274809b326a65bfc6cb5062226b276ef95cc386a5c90c0e70537930c7fd1822 lib/installer/upgrade.mjs
90
96
  2946f055d61fbe2ffd6af1c182096e01d8c1a4bfb0aa274547ce5e147dccfd19 lib/limits.mjs
91
97
  94c3315811e9694db4423c851437975e658949edb7706cf8124d6e1a4b60d0e5 lib/models/config.mjs
92
98
  a7afd79d4620abcf85f4954aab4599870d8e47b897490d1c5c774f256c5e8800 lib/models/index.mjs
93
99
  cc205194c9aa90887bb74c11cb0f5adb63f9da8afe660755a73c62d090309385 lib/models/profiles.mjs
100
+ acf28d52dac6a097e577da79ac8b9d2b30872a8f0ea66fc855db4b3f4863be5d lib/npm/context.mjs
101
+ ee81dee01b1623d95dda9b1d195ce2008811dfb54af3da8324a9e538238ac84e lib/npm/process.mjs
102
+ 8945c12db082a4a93bc71943d698cf60326e0ba45ae2a1caf6488a1dbc9d7cc7 lib/npm/upgrade.mjs
94
103
  7c36313221f36a9d08964017c79e8c7b3c848655f6a338f71a24ce508ac6e3ed lib/policy/claude-hook.mjs
95
104
  f32fcb03201c6a73d658a6b3107376f13d03f21503c568a7b4e71ff9b71dd426 lib/policy/placement.mjs
96
105
  09b5851cdbcb88b9d4d027e2d96aadb275660354ea64eef0aaa7583ec8090c3e lib/policy/render.mjs
97
- 5fd21f28b7983a89de8d52033ea61003462586ae679dbf87843b8291a2a29237 lib/policy/role-policy.json
106
+ 46b4e793c538f858b3a3e46087cd140a5a34154e6ecf5381c22b71250f289904 lib/policy/role-policy.json
107
+ 80e270d526e9a9c5cac23fe6ae293c2fd25a0176e4a8060987fd19dafe2a4594 lib/release/artifact.mjs
98
108
  30d8cdcb9d7ab6396cab09cd9f5cf6b8003f5490fb5c0ffe9f5acffbb0534978 lib/runtime/adapters/claude.mjs
99
109
  d30d118d3b42b805195ed05a3dea0ed3cf939eac811cf0228ca0b0cafeda7969 lib/runtime/adapters/codex.mjs
100
110
  c5a56ac147f13e9f946f673ac57a5dadbb4560d1f9deaa7c3b60e703f39b2d83 lib/runtime/capabilities.mjs
@@ -110,7 +120,7 @@ aa1361419c0137f74793847805a3abc076a8d782c6f590e2d28b8eee5c35065d lib/runtime/st
110
120
  4174dfcfcd4d69a5486b91108ec03279cfd50644109c7457aa87f9ac877fb1ac lib/safe-fs/native.mjs
111
121
  ff9c33c8b3c2bd466d54ffbe4c7139ccd87c79c7de34a6c4cde92e9f6e9b42d2 native/binding.c
112
122
  205bedc1dee1a9ffa58124c6e21256a1f0cbe24f06f7c8d7cb0b99524cfab9d0 native/binding.gyp
113
- b4aa1c8a1b3226485c90ca31841b57559150ab2a83db5764bf11ad84083236dd package.json
123
+ 4e557701705fd2e87f0513f95c79b02b88106e1e6f58a7e283ce5b7102dd88ea package.json
114
124
  208c65cfde244d39ce45c3cf1b0acb128f04d21199b6b3fb5466e62db5b64af9 prebuilds/darwin-arm64/arc_safe_fs.node
115
125
  572887ac794d10a8b921d8168ea2328e31f05b45987611ff02c83114dba901af prebuilds/darwin-x64/arc_safe_fs.node
116
126
  2bb9443021f9d873712b26321de733eddd6794c45f0bc79e2d5dc98669196635 prebuilds/index.json
@@ -118,12 +128,15 @@ b98bb4ac757ebd6e8f8fc1eb7a6a4389e3a8199984baf6bb1c327a6f952c2c0d prebuilds/linu
118
128
  9f8c2c1e328ee287355bc36afc3173c00ed062611fbd1262e4d30fca83e979dc prebuilds/linux-x64-gnu/arc_safe_fs.node
119
129
  0750a576f8042248a35b55559ccc52ef787a7706de7901169ec929622f256643 scripts/build-native.mjs
120
130
  aa394307b1bba68ea7146f76bec479a21f2cbeff9ea18d0786516c677a2d9fbb scripts/check-installer-parity.mjs
131
+ de88500b49726973d1de71c0bbdf9c993e28720a5b3f28751992c023b617e04c scripts/pack-release.mjs
132
+ 21cb8f3c35ee69ee31e9243f5b8844ca48f1351e77368071f8d4de0116dd1dd7 scripts/postinstall.mjs
121
133
  52aa4334c02e1877822304e66959b12f98a1649a547d51b9271623221ef3573a scripts/probe-runtime-hooks.mjs
122
134
  9c6bce1c1ab29ba75b70742e4d67c1eb0d0a0cab31e06b8abd28b057253f32d0 scripts/update-manifest.mjs
123
135
  e99ca8fd7dbde12840097e3d68bef06037f04284474fc5006976da40758cf6bb scripts/update-role-policy.mjs
124
136
  38163bb8828e4b4ab6bedfad719dba744ef0d99a09c9b23a85b1076d8375f181 scripts/update-runtime-assets.mjs
125
137
  f7c5d8288905ed7a6f0e99e3668a34ed43a3c1a7a3157bc883f22ec94fe22e98 scripts/update-superpowers.mjs
126
138
  40f41cc52354e02d9743c7b8d270c187856aac02cd9d7996d19b1f1089355d88 scripts/vendor-toml.mjs
139
+ a8884682a56e563b64931a2e0b025ec50a18bc758b0de7f5a92b7890ac36c4a5 scripts/verify-release.mjs
127
140
  fa5659948374d4f555594f47f6da073b40dc503e921aeeece30df4362b3051a5 vendor/toml/LICENSE
128
141
  ef924a2a42c7f16761e7098c3ed8e04195ecbcb02d5c369db0aee76f251a409c vendor/toml/dist/date.js
129
142
  76f8e47f5fc57273748242d59570656d7c921a80bceb2291ca5a4d7a723e9f29 vendor/toml/dist/error.js
package/README.md CHANGED
@@ -53,6 +53,17 @@ npx --yes @yunhozz/arc preset smart --target codex
53
53
  npx --yes @yunhozz/arc uninstall --target codex
54
54
  ```
55
55
 
56
+ ## 🧰 Optional: use the global CLI
57
+
58
+ If you run ARC management commands regularly, you can install the CLI globally and keep using the `arc` command:
59
+
60
+ ```sh
61
+ npm i -g @yunhozz/arc
62
+ arc
63
+ ```
64
+
65
+ The first command installs the CLI. Run `arc` to install the ARC harness. Afterward, use `arc preset` or `arc uninstall` to manage it. `arc uninstall` removes the harness; `npm uninstall -g @yunhozz/arc` removes the CLI and leaves the harness installed.
66
+
56
67
  ## 🎛️ Models and language
57
68
 
58
69
  The Orchestrator uses the current conversation's model. The five specialist roles use the selected preset from their platform model file: [Codex](codex/skill/models.toml) · [Claude Code](claude/skill/models.toml). The configured language applies across ARC; an explicit user language request takes precedence.
@@ -0,0 +1,3 @@
1
+ import {runChild} from '../lib/cli/upgrade.mjs';
2
+ try{let bytes=Buffer.alloc(0);for await(const chunk of process.stdin){bytes=Buffer.concat([bytes,chunk]);if(bytes.length>4096)throw Error('Invalid private upgrade request');}process.exitCode=await runChild(JSON.parse(bytes.toString('utf8')));}
3
+ catch{process.stderr.write('Error: Invalid private upgrade request\n');process.exitCode=2;}
@@ -13,7 +13,8 @@ If either auto-triggers in Orchestrator context, Orchestrator stops and routes t
13
13
  path in the brief. A user request to skip questions never waives design or approval gates; Planner returns questions to
14
14
  Orchestrator. Each top-level task (a plan task, or the whole request without a plan) gets a FRESH Coder,
15
15
  and each task that needs QA under Risk-scaled execution gets a FRESH QA, never one reused from another task;
16
- same-task rechecks may reuse.
16
+ same-task corrections and rechecks default to the current Coder and QA while the assignment remains valid;
17
+ a QA finding within the current task does not itself create a new task (see coordination and verification for restart exceptions).
17
18
 
18
19
  Follow user instructions and project guidance. Orchestrator uses the current chat model and answers a simple,
19
20
  self-contained question (answerable from context or one quick read, no edits) directly.
@@ -84,6 +85,6 @@ Read [handoff](references/handoff.md#common-handoff-gate) before every Coder fir
84
85
  Generated from lib/policy/role-policy.json; do not edit this managed section manually.
85
86
  - [scope.non-escalation] Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.
86
87
  - [external.explicit-user-authority] Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority grants no external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.
87
- - [role.orchestrator-boundary] Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it.
88
+ - [role.orchestrator-boundary] Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Coder per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Coder and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Coder even within the task. Before reusing Coder, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.
88
89
  - [platform.claude] Existing per-role tool allowlists reduce available tools; Bash and role contracts are not a filesystem sandbox. A role-scoped Git guard is a separate integration and is not supplied by these policy sections alone.
89
90
  [ARC role policy: contract end]
@@ -51,11 +51,14 @@ Planner, Reviewer, and Scouter return findings, relevant questions, next actions
51
51
  Orchestrator. Planner preserves complete requested drafts. Orchestrator integrates results (save, route,
52
52
  obtain approval; never author), routes decisions and required approvals,
53
53
  and owns development acceptance and completion. Orchestrator coordinates dependent implementation tasks sequentially and
54
- starts a fresh Coder for each top-level task; reuse the current Coder only for corrections, rework, or rechecks of the same
55
- task while the approved scope, workspace, ownership, and relevant context remain valid, after Orchestrator rechecks workspace,
56
- branch/HEAD and change state against the brief. A fresh Coder for the next task is a restart under handoff (choice reuse
57
- and restart brief rules are in handoff). After an approved plan or acceptance-criteria amendment, start a fresh Coder for
58
- the remaining work even within the same task. Do not add per-task QA or
54
+ starts a fresh Coder for each top-level task (Role gate); reuse the current Coder only for corrections, rework,
55
+ or rechecks of the same task while the approved scope, workspace, ownership, and relevant assignment context remain valid;
56
+ default to the current Coder for same-task QA corrections. A QA finding within the current task does not itself create
57
+ a new task. Before sending corrections or rework to a reused Coder, Orchestrator rechecks workspace, branch/HEAD and change state against the brief.
58
+ Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes, subject to the
59
+ amendment exception below. A fresh Coder for the next task is a restart under handoff (choice reuse and restart brief rules are in handoff).
60
+ After an approved plan or acceptance-criteria amendment, start a fresh Coder for
61
+ the remaining work even within the same task; it is also a restart under handoff. Do not add per-task QA or
59
62
  review gates solely because a task boundary exists; apply risk requirements and specific approved obligations. If the
60
63
  next task depends on a still-unverified risky contract, complete applicable QA at that dependency boundary and
61
64
  resolve or explicitly disposition findings before dependent implementation. This does not create a gate for every task.
@@ -22,13 +22,19 @@ repairs.
22
22
  ## Reviewer timing and task acceptance
23
23
 
24
24
  Task acceptance and its focused commit need only the task's risk-scaled evidence: a FRESH QA instance per task that
25
- needs QA (same-task rechecks may reuse), or Orchestrator focused checks for small, low-risk tasks; Reviewer is never a
25
+ needs QA (default to the current QA for post-fix rechecks of the same task while the approved scope, workspace,
26
+ ownership, and relevant assignment context remain valid), or Orchestrator focused checks for small, low-risk tasks; Reviewer is never a
26
27
  plan-task acceptance prerequisite. Do not delay an independently acceptable task for final-only checks,
27
28
  and do not defer currently required task checks to final verification. For significant work with a plan,
28
29
  Reviewer reviews once on the final full change after all plan tasks are accepted with their risk-scaled checks and any
29
30
  QA recheck at the final relevant state; without a plan, Reviewer follows QA before acceptance.
30
31
  A task commit does not mark unperformed final verification complete.
31
32
 
33
+ Fresh-per-task rules apply to Coder and QA, not Reviewer. Orchestrator may reuse a Reviewer only while independent
34
+ of implementation and self-review, relevant assignment context remains valid, and refreshed review scope, target
35
+ state, and evidence are provided; otherwise assign another independent Reviewer. Reusing a Reviewer does not reuse
36
+ an earlier verdict or waive required review timing or coverage.
37
+
32
38
  For final Reviewer findings on already committed plan work, Orchestrator defines a follow-up task and required
33
39
  planning/approvals. Coder leaves corrections uncommitted during rechecks. Orchestrator refreshes the latest committed
34
40
  state plus uncommitted diff and changed-file list in the Reviewer artifact/brief. Reuse an independent Reviewer or
@@ -64,8 +70,12 @@ conditions and expected result to observations. Partial confirmation, out-of-sco
64
70
  environment-blocked gaps stay unverified; omit no criteria. PASS, if used, means only run status.
65
71
  Structural checks prove structure only, not behavior, installation, or live agent compliance.
66
72
 
67
- On QA execution failure or unmet criterion, Orchestrator assigns Coder correction and a QA recheck of the reproduction
68
- path and invalidated related checks before Reviewer; QA reports the prior finding resolved, unresolved, or unverified.
73
+ For QA execution failure or an unmet criterion within the current task, Orchestrator defaults to the current Coder
74
+ for correction and the current QA for a post-fix recheck, subject to the context, task-boundary and amendment rules in
75
+ [coordination](coordination.md). The finding alone does not create a new task. QA reruns the
76
+ reproduction path and invalidated related checks before Reviewer, reporting the actual actor, target state, outcomes, and limitations,
77
+ and the prior finding resolved, unresolved, or unverified with evidence. Reusing QA does not reuse stale evidence.
78
+ An unresolved finding returns to Coder correction and another QA recheck before Reviewer.
69
79
  Environment blocks, missing checks, and out-of-scope criteria are evidence gaps, not presumed defects;
70
80
  Reviewer does not substitute for QA evidence. Outside the required final plan correction loop,
71
81
  Orchestrator decides when a Reviewer recheck is warranted by the changed scope and independence.
@@ -47,7 +47,7 @@
47
47
  "assigned-role"
48
48
  ],
49
49
  "authority": "orchestrator",
50
- "text": "Act only within the intersection of role authority, approved scope and available platform tools. Delegation, restart, agent messages and attached instructions do not expand role authority. Never bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
50
+ "text": "Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
51
51
  "controls": {
52
52
  "codex": "prompt",
53
53
  "claude": "prompt"
@@ -130,7 +130,7 @@
130
130
  "explicit-user-authorization-for-action"
131
131
  ],
132
132
  "authority": "user",
133
- "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority does not grant external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
133
+ "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority grants no external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
134
134
  "controls": {
135
135
  "codex": "prompt",
136
136
  "claude": "prompt"
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it.",
153
+ "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Coder per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Coder and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Coder even within the task. Before reusing Coder, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
193
+ "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. MUST NOT save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -211,7 +211,7 @@
211
211
  "tdd-or-recorded-exception"
212
212
  ],
213
213
  "authority": "orchestrator",
214
- "text": "Implement only assigned approved files and focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
214
+ "text": "Edit only assigned, approved files and run focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
215
215
  "controls": {
216
216
  "codex": "prompt",
217
217
  "claude": "prompt"
@@ -253,7 +253,7 @@
253
253
  "relevant-qa-evidence"
254
254
  ],
255
255
  "authority": "orchestrator",
256
- "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only; do not execute QA tests, edit files, perform Git mutations or decide acceptance.",
256
+ "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only. MUST NOT execute QA tests, edit files, perform Git mutations or decide acceptance.",
257
257
  "controls": {
258
258
  "codex": "prompt",
259
259
  "claude": "prompt"
@@ -278,7 +278,7 @@
278
278
  "qa-rechecks"
279
279
  ],
280
280
  "authority": "orchestrator",
281
- "text": "For significant/high-risk work without a plan: Coder \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. Do not omit Reviewer merely because no plan exists. This creates no new plan obligation.",
281
+ "text": "For significant/high-risk work without a plan: Coder \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
282
282
  "controls": {
283
283
  "codex": "prompt",
284
284
  "claude": "prompt"
@@ -327,7 +327,7 @@
327
327
  "reviewer-findings"
328
328
  ],
329
329
  "authority": "orchestrator",
330
- "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only to that final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
330
+ "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
331
331
  "controls": {
332
332
  "codex": "prompt",
333
333
  "claude": "prompt"
@@ -13,7 +13,8 @@ If either auto-triggers in Orchestrator context, Orchestrator stops and routes t
13
13
  path in the brief. A user request to skip questions never waives design or approval gates; Planner returns questions to
14
14
  Orchestrator. Each top-level task (a plan task, or the whole request without a plan) gets a FRESH Coder,
15
15
  and each task that needs QA under Risk-scaled execution gets a FRESH QA, never one reused from another task;
16
- same-task rechecks may reuse.
16
+ same-task corrections and rechecks default to the current Coder and QA while the assignment remains valid;
17
+ a QA finding within the current task does not itself create a new task (see coordination and verification for restart exceptions).
17
18
 
18
19
  Follow user instructions and project guidance. Orchestrator uses the current chat model and answers simple,
19
20
  self-contained questions directly. For substantive multistep work, Orchestrator coordinates implementation, configuration, tests,
@@ -85,6 +86,6 @@ Read [handoff](references/handoff.md#common-handoff-gate) before every Coder fir
85
86
  Generated from lib/policy/role-policy.json; do not edit this managed section manually.
86
87
  - [scope.non-escalation] Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.
87
88
  - [external.explicit-user-authority] Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority grants no external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.
88
- - [role.orchestrator-boundary] Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it.
89
+ - [role.orchestrator-boundary] Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Coder per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Coder and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Coder even within the task. Before reusing Coder, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.
89
90
  - [platform.codex] Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Coder and QA need shell for approved checks.
90
91
  [ARC role policy: contract end]
@@ -58,9 +58,11 @@ ARC approval, ownership, TDD exceptions and executor adapters still apply.
58
58
  Orchestrator integrates role results, routes decisions and required approvals, and owns development acceptance
59
59
  and completion. Orchestrator coordinates dependent implementation tasks sequentially and
60
60
  starts a fresh Coder for each top-level task (Role gate); reuse the current Coder only for corrections, rework,
61
- or rechecks of the same task while the approved scope, workspace, ownership, and relevant context remain valid; before
62
- sending corrections or rework to a reused Coder, Orchestrator rechecks workspace, branch/HEAD and change state against the brief.
63
- A fresh Coder for the next task is a restart under handoff (choice reuse and restart brief rules are in handoff).
61
+ or rechecks of the same task while the approved scope, workspace, ownership, and relevant assignment context remain valid;
62
+ default to the current Coder for same-task QA corrections. A QA finding within the current task does not itself create
63
+ a new task. Before sending corrections or rework to a reused Coder, Orchestrator rechecks workspace, branch/HEAD and change state against the brief.
64
+ Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes, subject to the
65
+ amendment exception below. A fresh Coder for the next task is a restart under handoff (choice reuse and restart brief rules are in handoff).
64
66
  After an approved plan or acceptance-criteria amendment, start a fresh Coder for
65
67
  the remaining work even within the same task; it is also a restart under handoff. Task-boundary gates
66
68
  follow [Risk-scaled execution](verification.md#risk-scaled-execution).
@@ -40,6 +40,11 @@ explicitly dispositioned.
40
40
 
41
41
  Criterion verdicts follow the Evidence verdict.
42
42
 
43
+ Fresh-per-task rules apply to Coder and QA, not Reviewer. Orchestrator may reuse a Reviewer only while independent
44
+ of implementation and self-review, relevant assignment context remains valid, and refreshed review scope, target
45
+ state, and evidence are provided; otherwise assign another independent Reviewer. Reusing a Reviewer does not reuse
46
+ an earlier verdict or waive required review timing or coverage.
47
+
43
48
  ### Final plan Reviewer correction loop
44
49
 
45
50
  For a plan's final overall Reviewer findings, including behavior findings, Orchestrator assigns
@@ -95,7 +100,8 @@ acceptance criteria to actual evidence, currently required checks and findings b
95
100
  completion, a Coder report or passing commands alone do not establish acceptance.
96
101
 
97
102
  Task acceptance and its focused commit require only the task's risk-scaled evidence: a FRESH QA instance per task that
98
- needs QA (same-task rechecks may reuse), or Orchestrator focused checks for small, low-risk tasks;
103
+ needs QA (default to the current QA for post-fix rechecks of the same task while the approved scope, workspace,
104
+ ownership, and relevant assignment context remain valid), or Orchestrator focused checks for small, low-risk tasks;
99
105
  Reviewer timing and task-boundary gates follow the Risk-scaled execution section above. For an independently acceptable task,
100
106
  do not delay its acceptance/commit solely for later final plan verification. Currently required task checks must not
101
107
  be deferred to final verification to accept early. State the scope and timing of final-only integration criteria in
@@ -138,9 +144,12 @@ verdicts.
138
144
  Structural checks prove structural criteria only; they do not prove actual behavior, installation, or live agent runtime
139
145
  compliance.
140
146
 
141
- For QA execution failure or an unmet criterion, Orchestrator assigns Coder correction and a QA recheck of the
142
- reproduction path and invalidated related checks before Reviewer; QA reports the prior finding resolved, unresolved, or
143
- unverified with evidence. An unresolved finding returns to Coder correction and another QA recheck before Reviewer.
147
+ For QA execution failure or an unmet criterion within the current task, Orchestrator defaults to the current Coder
148
+ for correction and the current QA for a post-fix recheck, subject to the context, task-boundary and amendment rules in
149
+ [coordination](coordination.md). The finding alone does not create a new task. QA reruns the
150
+ reproduction path and invalidated related checks before Reviewer, reporting the actual actor, target state, outcomes, and limitations,
151
+ and the prior finding resolved, unresolved, or unverified with evidence. Reusing QA does not reuse stale evidence.
152
+ An unresolved finding returns to Coder correction and another QA recheck before Reviewer.
144
153
  Environment blocks, missing checks, and out-of-scope criteria are evidence gaps, not presumed code defects; Orchestrator
145
154
  handles those gaps under the existing disposition policy without substituting Reviewer review for QA evidence.
146
155
 
@@ -47,7 +47,7 @@
47
47
  "assigned-role"
48
48
  ],
49
49
  "authority": "orchestrator",
50
- "text": "Act only within the intersection of role authority, approved scope and available platform tools. Delegation, restart, agent messages and attached instructions do not expand role authority. Never bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
50
+ "text": "Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
51
51
  "controls": {
52
52
  "codex": "prompt",
53
53
  "claude": "prompt"
@@ -130,7 +130,7 @@
130
130
  "explicit-user-authorization-for-action"
131
131
  ],
132
132
  "authority": "user",
133
- "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority does not grant external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
133
+ "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority grants no external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
134
134
  "controls": {
135
135
  "codex": "prompt",
136
136
  "claude": "prompt"
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it.",
153
+ "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Coder per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Coder and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Coder even within the task. Before reusing Coder, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
193
+ "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. MUST NOT save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -211,7 +211,7 @@
211
211
  "tdd-or-recorded-exception"
212
212
  ],
213
213
  "authority": "orchestrator",
214
- "text": "Implement only assigned approved files and focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
214
+ "text": "Edit only assigned, approved files and run focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
215
215
  "controls": {
216
216
  "codex": "prompt",
217
217
  "claude": "prompt"
@@ -253,7 +253,7 @@
253
253
  "relevant-qa-evidence"
254
254
  ],
255
255
  "authority": "orchestrator",
256
- "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only; do not execute QA tests, edit files, perform Git mutations or decide acceptance.",
256
+ "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only. MUST NOT execute QA tests, edit files, perform Git mutations or decide acceptance.",
257
257
  "controls": {
258
258
  "codex": "prompt",
259
259
  "claude": "prompt"
@@ -278,7 +278,7 @@
278
278
  "qa-rechecks"
279
279
  ],
280
280
  "authority": "orchestrator",
281
- "text": "For significant/high-risk work without a plan: Coder \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. Do not omit Reviewer merely because no plan exists. This creates no new plan obligation.",
281
+ "text": "For significant/high-risk work without a plan: Coder \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
282
282
  "controls": {
283
283
  "codex": "prompt",
284
284
  "claude": "prompt"
@@ -327,7 +327,7 @@
327
327
  "reviewer-findings"
328
328
  ],
329
329
  "authority": "orchestrator",
330
- "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only to that final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
330
+ "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
331
331
  "controls": {
332
332
  "codex": "prompt",
333
333
  "claude": "prompt"
@@ -13,10 +13,10 @@ export async function selectChoices(options,{input=process.stdin,output=process.
13
13
  try{return await new Promise((resolve,reject)=>{
14
14
  abort=()=>reject(cancelled());onEnd=abort;onError=reject;
15
15
  onKey=(str,key={})=>{try{if(key.name==='c'&&key.ctrl||key.name==='d'&&key.ctrl)return abort();if(key.name==='up'||key.name==='down'){index=(index+(key.name==='up'?-1:1)+menus[menu][1].length)%menus[menu][1].length;draw(true);}else if(key.name==='return'||key.name==='enter'){const [name,,values]=menus[menu];options[name]=values[index];output.write('\n');if(++menu===menus.length){keys.removeListener('keypress',onKey);resolve(options);}else{index=menus[menu][3];draw();}}}catch(error){reject(error);}};
16
- keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);
16
+ keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);
17
17
  if(input.setRawMode)input.setRawMode(true);input.resume();draw();
18
18
  });}finally{
19
- keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
19
+ keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
20
20
  for(const [event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
21
21
  if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
22
22
  }
@@ -36,9 +36,9 @@ export async function selectLimits(options,selections,{input=process.stdin,outpu
36
36
  if(name==='maxConcurrentSubagents')options.concurrentSelected=true;output.write('\n');value='';if(++index===fields.length){input.removeListener('keypress',onKey);resolve(options);}else draw();
37
37
  }else if(key.name==='backspace'){value=value.slice(0,-1);output.write('\b \b');}else if(str&&!key.ctrl&&!key.meta){value+=str;output.write(str);}};
38
38
  onData=chunk=>{for(const character of chunk.toString()){if(index===fields.length)break;const name=character==='\r'||character==='\n'?'return':character==='\x03'?'c':character==='\x04'?'d':character==='\x7f'||character==='\b'?'backspace':undefined;onKey(character,{name,ctrl:character==='\x03'||character==='\x04'});}};
39
- input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
39
+ input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
40
40
  });}finally{
41
- input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
41
+ input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
42
42
  for(const[event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
43
43
  if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
44
44
  }
@@ -2,17 +2,18 @@ export function renderCompletion(shell){
2
2
  if(shell==='bash')return `_arc() {
3
3
  local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" command="\${COMP_WORDS[1]}" choices=""
4
4
  case "$prev" in
5
- --target) choices="claude codex all" ;;
6
- --preset) choices="economy balanced smart" ;;
7
- --language) choices="en ko" ;;
5
+ --target) [[ $command == -* || $command == preset || $command == uninstall ]] && choices="claude codex all" ;;
6
+ --preset) [[ $command == -* ]] && choices="economy balanced smart" ;;
7
+ --language) [[ $command == -* ]] && choices="en ko" ;;
8
8
  *)
9
- if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall"
9
+ if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall completion upgrade"
10
10
  else
11
11
  case "$command" in
12
12
  preset)
13
13
  if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then choices="economy balanced smart"
14
14
  else choices="--target --check --dry-run --help"; fi ;;
15
15
  uninstall) choices="--target --dry-run --skip-global --help --version" ;;
16
+ upgrade) choices="--help" ;;
16
17
  completion) (( COMP_CWORD == 2 )) && choices="zsh bash" ;;
17
18
  -*) choices="--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version" ;;
18
19
  esac
@@ -25,17 +26,18 @@ complete -F _arc arc
25
26
  if(shell==='zsh')return `_arc() {
26
27
  local -a candidates
27
28
  if (( CURRENT == 2 )) && [[ \${words[CURRENT]} != -* ]]; then
28
- candidates=(preset uninstall)
29
+ candidates=(preset uninstall completion upgrade)
29
30
  else
30
31
  case "\${words[CURRENT-1]}" in
31
- --target) candidates=(claude codex all) ;;
32
- --preset) candidates=(economy balanced smart) ;;
33
- --language) candidates=(en ko) ;;
32
+ --target) [[ \${words[2]} == -* || \${words[2]} == preset || \${words[2]} == uninstall ]] && candidates=(claude codex all) ;;
33
+ --preset) [[ \${words[2]} == -* ]] && candidates=(economy balanced smart) ;;
34
+ --language) [[ \${words[2]} == -* ]] && candidates=(en ko) ;;
34
35
  *) case "\${words[2]}" in
35
36
  preset)
36
37
  if (( CURRENT == 3 )) && [[ \${words[CURRENT]} != -* ]]; then candidates=(economy balanced smart)
37
38
  else candidates=(--target --check --dry-run --help); fi ;;
38
39
  uninstall) candidates=(--target --dry-run --skip-global --help --version) ;;
40
+ upgrade) candidates=(--help) ;;
39
41
  completion) (( CURRENT == 3 )) && candidates=(zsh bash) ;;
40
42
  -*) candidates=(--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version) ;;
41
43
  esac ;;
package/lib/cli/index.mjs CHANGED
@@ -13,7 +13,7 @@ export function parseArgs(args){
13
13
  if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
14
14
  }
15
15
  export async function selectChoices(options,io){return(await import('./choices.mjs')).selectChoices(options,io);}
16
- export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
16
+ export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr,cwd=process.cwd(),env=process.env}={}){
17
17
  let controller,onInterrupt,progress;
18
18
  try{
19
19
  const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}if(options.version){output.write(JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version+'\n');return 0;}
@@ -22,12 +22,12 @@ export async function runCli(args=process.argv.slice(2),{input=process.stdin,out
22
22
  const {startProgress}=await import('./progress.mjs');
23
23
  const interactive=input.isTTY&&output.isTTY,label=options.check?'Checking ARC…':options.dryRun?'Preparing ARC dry run…':'Installing ARC…';
24
24
  if(!interactive)progress=startProgress({output,label});
25
- controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);
25
+ controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
26
26
  const {loadNative}=await import('../safe-fs/native.mjs');const native=loadNative();
27
27
  const [{openRoot},{verifyPayload},{resolveTarget},{buildInstallPlan,validatePlanSet}]=await Promise.all([import('../safe-fs/index.mjs'),import('../installer/manifest.mjs'),import('../installer/targets.mjs'),import('../installer/plan.mjs')]);
28
28
  const safeFs={openRoot:p=>openRoot(p,{native})},payload=verifyPayload({packageRoot,safeFs});
29
29
  const platforms=options.target==='all'?['codex','claude']:[options.target];
30
- const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env:process.env});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
30
+ const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env,cwd});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
31
31
  if(interactive){const{selectLimits,selectLanguage}=await import('./choices.mjs');await selectLimits(options,selections,{input,output});await selectLanguage(options,defaultLanguage(selections.map(item=>item.languagePreview.existing)),{input,output});}
32
32
  progress??=startProgress({output,label});
33
33
  const plans=selections.map(({target,preview,languagePreview})=>buildInstallPlan({payload,target,language:options.language,languagePreview,preset:options.preset,skipGlobal:options.skipGlobal,safeFs,limitsPreview:preview,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)}));const {attachShellCompletion}=await import('../installer/shell-completion.mjs');attachShellCompletion(plans,{safeFs});validatePlanSet(plans);
@@ -48,8 +48,8 @@ export async function runCli(args=process.argv.slice(2),{input=process.stdin,out
48
48
  progress.stop();
49
49
  const reportResults=()=>{for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const recovery of item.residualPaths)output.write(`Residual path: ${recovery}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}};
50
50
  if(result.interrupted){reportResults();return 130;}
51
- if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
51
+ if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC version ${JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version} installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
52
52
  reportResults();return 1;
53
53
  }catch(cause){progress?.stop();error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}if(cause.code==='ARC_CANCELLED')return 130;return ['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;
54
- }finally{progress?.stop();if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
54
+ }finally{progress?.stop();if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
55
55
  }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import {inspectNpmContext,resolveInvocationCwd} from '../npm/context.mjs';
3
+ import {openInstallerTerminal} from './terminal.mjs';
4
+ import {runCli} from './index.mjs';
5
+ export async function runPostinstall({packageRoot,env=process.env,cwd=process.cwd(),input=process.stdin,output=process.stdout,error=process.stderr,deps={}}){
6
+ let terminal;
7
+ try{
8
+ if(env.npm_lifecycle_event!=='postinstall'){output.write('Run arc to install the ARC harness.\n');return 0;}
9
+ const context=await(deps.inspectNpmContext??inspectNpmContext)({packageRoot,entryPath:path.join(packageRoot,'scripts/postinstall.mjs'),env,cwd});
10
+ if(context.kind!=='global'){output.write('Run arc to install the ARC harness.\n');return 0;}
11
+ terminal=(deps.openInstallerTerminal??openInstallerTerminal)({env,input,output,error});
12
+ if(terminal.kind==='skip'){output.write('Run arc to install the ARC harness.\n');return 0;}
13
+ const invocationCwd=resolveInvocationCwd({env,cwd,lifecycle:true});
14
+ return await(deps.runCli??runCli)([],{...terminal.io,cwd:invocationCwd,env});
15
+ }catch(cause){(terminal?.io.error??error).write(`Error: ${cause.message}\n`);return 1;}
16
+ finally{terminal?.dispose?.();}
17
+ }
package/lib/cli/root.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import {renderCompletion} from './completion.mjs';
3
- const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
3
+ const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc upgrade [--help]\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
4
4
  const completionUsage='Usage: arc completion <zsh|bash>';
5
5
  export async function runRoot(args=process.argv.slice(2),io={}){
6
6
  const output=io.output??process.stdout,error=io.error??process.stderr;
@@ -8,6 +8,7 @@ export async function runRoot(args=process.argv.slice(2),io={}){
8
8
  if(command==='--help'&&!rest.length){output.write(usage+'\n\n');return(await import('./index.mjs')).runCli(args,io);}
9
9
  if(command==='--version'&&!rest.length){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
10
10
  if(command==='preset')return(await import('./preset.mjs')).runCli(rest,io);
11
+ if(command==='upgrade')return(await import('./upgrade.mjs')).runCli(rest,io);
11
12
  if(command==='install'){error.write('Error: The install subcommand was removed. Run npx --yes @yunhozz/arc [installation options].\n');return 2;}
12
13
  if(command===undefined||command.startsWith('--'))return(await import('./index.mjs')).runCli(args,io);
13
14
  if(command==='uninstall')return(await import('./uninstall.mjs')).runCli(rest,io);
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs';
2
+ import tty from 'node:tty';
3
+ function openControllingTerminal(){
4
+ let inputFd,outputFd,input,output;
5
+ try{
6
+ inputFd=fs.openSync('/dev/tty','r+');outputFd=fs.openSync('/dev/tty','r+');
7
+ input=new tty.ReadStream(inputFd);inputFd=undefined;
8
+ output=new tty.WriteStream(outputFd);outputFd=undefined;
9
+ return{io:{input,output,error:output},dispose(){input.destroy();output.destroy();}};
10
+ }catch(cause){input?.destroy();output?.destroy();if(inputFd!==undefined)fs.closeSync(inputFd);if(outputFd!==undefined)fs.closeSync(outputFd);throw cause;}
11
+ }
12
+ export function openInstallerTerminal({platform=process.platform,env=process.env,input=process.stdin,output=process.stdout,error=process.stderr,openTty=openControllingTerminal}={}){
13
+ if(!['darwin','linux'].includes(platform))return{kind:'skip',reason:'unsupported platform'};
14
+ if(['CI','CONTINUOUS_INTEGRATION','BUILD_NUMBER','RUN_ID'].some(name=>env[name]&&env[name]!=='false'&&env[name]!=='0'))return{kind:'skip',reason:'non-interactive environment'};
15
+ let owned,io;
16
+ try{if(input.isTTY&&output.isTTY)io={input,output,error};else{owned=openTty();io=owned.io;}if(!io.input.isTTY||!io.output.isTTY)throw Error('Terminal is not interactive');}catch{owned?.dispose();return{kind:'skip',reason:'no interactive terminal'};}
17
+ const wasRaw=Boolean(io.input.isRaw),wasFlowing=io.input.readableFlowing;let disposed=false;
18
+ return{kind:'interactive',io,dispose(){if(disposed)return;disposed=true;try{if(io.input.setRawMode&&!io.input.destroyed)io.input.setRawMode(wasRaw);if(wasFlowing!==true)io.input.pause();}finally{owned?.dispose();}}};
19
+ }
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ const packageRoot=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
6
+ const usage='Usage: arc upgrade [--help]';
7
+ const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
8
+ const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
9
+ function identity(root){const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(pkg.name!=='@yunhozz/arc'||!/^\d+\.\d+\.\d+$/.test(pkg.version))throw configurationError('Invalid ARC package identity');return pkg;}
10
+ async function runtime(deps){
11
+ const [context,state,upgrade,npm]=await Promise.all([import('../npm/context.mjs'),import('../installer/upgrade-state.mjs'),import('../installer/upgrade.mjs'),import('../npm/upgrade.mjs')]);
12
+ let safeFs=deps.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:root=>openRoot(root,{native})};}
13
+ return{...context,...state,...upgrade,...npm,...deps,safeFs};
14
+ }
15
+ function report(result,output,error){
16
+ for(const item of result.results){output.write(item.platform+': '+item.status+'; rollback: '+item.rollback+'\n');for(const filename of item.residualPaths??[])output.write('Residual path: '+filename+'\n');if(item.error)error.write('Error: '+item.error.message+'\n');}
17
+ if(result.interrupted)return 130;
18
+ if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\nOpen a new shell session to use updated completion.\nzsh completion: eval "$(arc completion zsh)" for this session\n');return 0;}
19
+ error.write('Harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return 1;
20
+ }
21
+ function failure(cause,error){error.write('Error: '+cause.message+'\n');return ['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(cause.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(cause.code)?2:1;}
22
+ export async function runCli(args=process.argv.slice(2),io={},deps={}){
23
+ const output=io.output??process.stdout,error=io.error??process.stderr;let controller,onInterrupt,packageStarted=false;
24
+ try{
25
+ if(args.length===1&&args[0]==='--help'){output.write(usage+'\nUpdates the global CLI and installed harness; npm exec updates the harness only.\n');return 0;}
26
+ if(args.length)throw inputError('Expected no arguments or --help');
27
+ const root=deps.packageRoot??packageRoot,pkg=identity(root),env=io.env??process.env,cwd=fs.realpathSync(io.cwd??process.cwd()),api=await runtime(deps);
28
+ const context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
29
+ if(!['global','npx'].includes(context.kind))throw configurationError('Run upgrade from a verified global ARC installation or npx --yes @yunhozz/arc@latest upgrade');
30
+ const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});
31
+ if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
32
+ controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
33
+ if(context.kind==='npx')return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
34
+ const latest=await api.resolveLatest({context,env,cwd,signal:controller.signal});packageStarted=true;
35
+ const replaced=await api.upgradeGlobal({context,expectedVersion:latest.version,env,cwd,signal:controller.signal});
36
+ if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
37
+ output.write('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
38
+ const result=await api.runUpgradeChild({entryPath:path.join(context.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:controller.signal});
39
+ if(result.stdout)output.write(result.stdout);if(result.stderr)error.write(result.stderr);
40
+ if(result.code===0&&!result.signal)return 0;
41
+ error.write('CLI was updated but the harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return result.code===130?130:1;
42
+ }catch(cause){const code=failure(cause,error);if(cause.code==='ARC_CLI_INPUT')error.write(usage+'\n');if(packageStarted)error.write('CLI state may have changed. Retry arc upgrade; if it is unavailable, run npx --yes @yunhozz/arc@latest upgrade.\n');return code;}
43
+ finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
44
+ }
45
+ export async function runChild(request,io={},deps={}){
46
+ const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
47
+ try{
48
+ if(!request||Object.keys(request).sort().join(',')!=='expectedStateToken,expectedVersion,invocationCwd,protocol'||request.protocol!==1||typeof request.expectedVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(request.expectedVersion)||typeof request.expectedStateToken!=='string'||!/^[a-f0-9]{64}$/.test(request.expectedStateToken)||typeof request.invocationCwd!=='string'||!path.isAbsolute(request.invocationCwd)||fs.realpathSync(request.invocationCwd)!==request.invocationCwd)throw inputError('Invalid private upgrade request');
49
+ const root=deps.packageRoot??packageRoot,pkg=identity(root);if(pkg.version!==request.expectedVersion)throw configurationError('The new ARC package version does not match the requested upgrade');
50
+ const api=await runtime(deps),env=io.env??process.env,cwd=request.invocationCwd,context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc-upgrade.mjs'),env,cwd});
51
+ if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
52
+ const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});if(state.stateToken!==request.expectedStateToken)throw configurationError('Installed ARC settings changed during CLI replacement; retry upgrade');
53
+ if(!state.targets.length)throw configurationError('No installed ARC harness found');
54
+ const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
55
+ return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
56
+ }catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
57
+ }
@@ -2,6 +2,7 @@ import {installClaudeHook} from '../policy/claude-hook.mjs';
2
2
  import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
3
3
  import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
4
4
  import{resolveLimits}from'../limits.mjs';import{parseLimits,serializeLimits}from'./limits.mjs';import{readRuntimeConcurrency,renderCodexConcurrency,renderClaudeConcurrency}from'./runtime-config.mjs';import{assertPlanSnapshot}from'./transaction.mjs';
5
+ import{validateModelConfig}from'../models/config.mjs';
5
6
  import path from'node:path';import{loadModelConfig,applyPreset,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
6
7
  function fail(message){throw new TargetConfigurationError(message);}
7
8
  export function previewLimits({target,safeFs}){
@@ -16,7 +17,7 @@ export function previewLanguage({target,safeFs}){
16
17
  let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
17
18
  return{existing,snapshot};
18
19
  }
19
- export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview}){
20
+ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
20
21
  validateLanguage(language);
21
22
  if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
22
23
  const languageState=languagePreview??previewLanguage({target,safeFs});
@@ -24,7 +25,7 @@ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,
24
25
  const preview=limitsPreview??previewLimits({target,safeFs});
25
26
  limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
26
27
  const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
27
- const config=applyPreset(loadModelConfig(input.models,target.platform),preset),profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
28
+ const config=modelConfig===undefined?applyPreset(loadModelConfig(input.models,target.platform),preset):validateModelConfig(modelConfig);if(config.platform!==target.platform)fail('Model config platform does not match target');const profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
28
29
  if(target.platform==='claude'){try{const script=payload.files.get('claude/skill/hooks/role-git-guard.mjs'),policy=payload.files.get('claude/skill/hooks/git-policy.json');if(!Buffer.isBuffer(script)||!script.length||!Buffer.isBuffer(policy))throw Error('Missing hook assets');validateGitPolicy(JSON.parse(policy.toString('utf8')));for(const[name,bytes]of profiles)profiles.set(name,installClaudeHook(bytes,path.join(target.home,'skills/arc')));}catch(cause){fail(`Invalid Claude Git guard: ${cause.message}`);}}
29
30
  const skill=new Map();for(const[name,bytes]of payload.files)if(name.startsWith(target.payloadPrefix+'/skill/'))skill.set(name.slice((target.payloadPrefix+'/skill/').length),Buffer.from(bytes));skill.set('SKILL.md',renderLanguagePolicy(skill.get('SKILL.md'),language));skill.set('settings.toml',serializeLanguage(language));skill.set('models.toml',modelBytes);skill.set('limits.toml',serializeLimits(limits));
30
31
  const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import {createHash} from 'node:crypto';
3
+ import {resolveTarget,inspectPath,TargetConfigurationError} from './targets.mjs';
4
+ import {loadModelConfig} from '../models/index.mjs';
5
+ import {parseLanguage} from './language.mjs';
6
+ import {parseLimits} from './limits.mjs';
7
+ import {removeManagedBlock} from './global-config.mjs';
8
+ import {renderShellCompletion} from './shell-completion.mjs';
9
+ import {readRuntimeConcurrency} from './runtime-config.mjs';
10
+ const fail=message=>{throw new TargetConfigurationError(message);};
11
+ export function collectInstalledState({safeFs,userHome,env={},cwd}){
12
+ if(env.CODEX_HOME!==undefined&&!path.isAbsolute(env.CODEX_HOME)&&env.CODEX_HOME!=='~'&&!env.CODEX_HOME.startsWith('~/')&&(!cwd||!path.isAbsolute(cwd)))fail('A verified invocation cwd is required for relative CODEX_HOME');
13
+ const targets=[];
14
+ for(const platform of ['codex','claude']){
15
+ const target=resolveTarget(platform,{userHome,env,cwd}),snapshot=new Map(),arc=path.join(target.home,'skills/arc');
16
+ const skill=inspectPath(safeFs,arc,{kind:'directory',tree:true,allowLinks:true,snapshot});
17
+ const profiles=target.profileNames.map(name=>inspectPath(safeFs,path.join(target.home,'agents',name),{kind:'file',read:true,snapshot}));
18
+ const instruction=inspectPath(safeFs,target.instructionPath,{kind:'file',read:true,snapshot}),managed=instruction.exists?removeManagedBlock(instruction.bytes):{changed:false};
19
+ if(!skill.exists){if(profiles.some(item=>item.exists)||managed.changed)fail(`Incomplete ARC installation: ${target.home}`);continue;}
20
+ function mandatory(name){const found=inspectPath(safeFs,path.join(arc,name),{kind:'file',read:true,snapshot});if(!found.exists||!found.bytes.length)fail(`Incomplete ARC installation: missing ${name} at ${target.home}`);return found.bytes;}
21
+ mandatory('SKILL.md');if(profiles.some(item=>!item.exists||!item.bytes.length))fail(`Incomplete ARC installation: missing profile at ${target.home}`);
22
+ let language,modelConfig,limits;try{language=parseLanguage(mandatory('settings.toml')).language;modelConfig=loadModelConfig(mandatory('models.toml'),platform);limits=parseLimits(mandatory('limits.toml'));}catch(cause){throw new TargetConfigurationError(`Invalid installed ARC settings at ${target.home}: ${cause.message}`,{cause});}
23
+ const native=inspectPath(safeFs,target.runtimeConfigPath,{kind:'file',read:true,snapshot});if(native.exists)try{readRuntimeConcurrency(platform,native.bytes);}catch(cause){throw new TargetConfigurationError(`Invalid native configuration at ${target.runtimeConfigPath}: ${cause.message}`,{cause});}
24
+ const shell=inspectPath(safeFs,path.join(userHome,'.zshrc'),{kind:'file',read:true,snapshot});if(shell.exists)renderShellCompletion(shell.bytes);
25
+ targets.push({target,language,modelConfig,limits,snapshot});
26
+ }
27
+ const hash=createHash('sha256');for(const entry of targets){hash.update(entry.target.platform+'\0'+entry.target.home+'\0');for(const [name,value]of [...entry.snapshot].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([name,value],(key,value)=>key==='atimeNs'?undefined:typeof value==='bigint'?value.toString():value));}
28
+ return{targets,stateToken:hash.digest('hex')};
29
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {buildInstallPlan,validatePlanSet} from './plan.mjs';
5
+ import {assertPlanSnapshot} from './transaction.mjs';
6
+ import {inspectPath} from './targets.mjs';
7
+ import {renderManagedBlock,removeManagedBlock,agentsBlockBody} from './global-config.mjs';
8
+ import {renderShellCompletion} from './shell-completion.mjs';
9
+ import {verifyPayload} from './manifest.mjs';
10
+ import {installTargets} from './install.mjs';
11
+ import {acquireExternalSkills} from './acquisition.mjs';
12
+ export function buildUpgradePlans({payload,state,safeFs}){
13
+ for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs});
14
+ const plans=state.targets.map(entry=>{
15
+ const {target,language,modelConfig,limits}=entry;
16
+ const plan=buildInstallPlan({payload,target,language,modelConfig,preset:modelConfig.preset,limits,skipGlobal:true,safeFs});
17
+ for(const [name,value]of entry.snapshot)plan.snapshot.set(name,value);
18
+ const current=inspectPath(safeFs,target.instructionPath,{kind:'file',read:true,snapshot:plan.snapshot});
19
+ if(current.exists&&removeManagedBlock(current.bytes).changed){const rendered=renderManagedBlock(current.bytes,agentsBlockBody(target.home,target.userHome,language,target.platform));plan.managedPaths.push(target.instructionPath);if(!rendered.content.equals(current.bytes))plan.globalChanges.push({path:target.instructionPath,original:current.bytes,...rendered});}
20
+ return plan;
21
+ });
22
+ const first=plans[0];if(first){const filename=path.join(first.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:first.snapshot});
23
+ if(current.exists){const content=renderShellCompletion(current.bytes);if(current.bytes.includes(Buffer.from('# >>> arc completion >>>'))){first.managedPaths.push(filename);if(!content.equals(current.bytes))first.globalChanges.push({path:filename,original:current.bytes,content,action:'update completion'});}}
24
+ for(const plan of plans.slice(1))plan.snapshot.delete(filename);
25
+ }
26
+ validatePlanSet(plans);return plans;
27
+ }
28
+ export async function upgradeInstalledTargets({packageRoot,state,safeFs,signal,acquisition}){
29
+ const payload=verifyPayload({packageRoot,safeFs}),version=JSON.parse(payload.files.get('package.json').toString('utf8')).version,plans=buildUpgradePlans({payload,state,safeFs});
30
+ const acquire=acquisition??(async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));let acquired;
31
+ try{acquired=await acquireExternalSkills({work,safeFs,signal});}catch(error){if(!error.residualPaths?.length)try{fs.rmdirSync(work);}catch(cleanup){throw Object.assign(new AggregateError([error,cleanup],'Cannot clean private acquisition work'),{code:'ARC_ACQUISITION_CLEANUP',residualPaths:[work]});}throw error;}
32
+ return{...acquired,dispose:async()=>{await acquired.dispose();fs.rmdirSync(work);}};});
33
+ return{version,...await installTargets(plans,{safeFs,signal,acquisition:acquire})};
34
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {runNpm as defaultRunNpm} from './process.mjs';
4
+ const unsupported=reason=>({kind:'unsupported',reason});
5
+ function realDirectory(value){if(typeof value!=='string'||!path.isAbsolute(value))throw Error('Expected an absolute directory');const real=fs.realpathSync(value);if(!fs.statSync(real).isDirectory())throw Error('Expected a directory');return real;}
6
+ function npmExecutable(env){
7
+ const candidates=env.npm_execpath?[env.npm_execpath]:(env.PATH??'').split(path.delimiter).filter(p=>path.isAbsolute(p)).map(p=>path.join(p,'npm'));
8
+ for(const candidate of candidates)try{if(!path.isAbsolute(candidate))continue;const cli=fs.realpathSync(candidate),root=path.dirname(path.dirname(cli));if(path.basename(cli)==='npm-cli.js'&&path.basename(path.dirname(cli))==='bin'&&JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).name==='npm'&&fs.statSync(cli).isFile())return cli;}catch{}
9
+ throw Error('Cannot verify npm CLI');
10
+ }
11
+ export function resolveInvocationCwd({env={},cwd,lifecycle=false}){
12
+ if(lifecycle){
13
+ if(env.INIT_CWD!==undefined)return realDirectory(env.INIT_CWD);
14
+ if(env.CODEX_HOME!==undefined&&!path.isAbsolute(env.CODEX_HOME)&&env.CODEX_HOME!=='~'&&!env.CODEX_HOME.startsWith('~/'))throw Error('Original invocation directory (INIT_CWD) is required for relative CODEX_HOME');
15
+ }
16
+ return realDirectory(cwd);
17
+ }
18
+ export async function inspectNpmContext({packageRoot,entryPath,env=process.env,cwd=process.cwd(),execPath=process.execPath,runNpm=defaultRunNpm}){
19
+ try{
20
+ if(fs.realpathSync(execPath)!==fs.realpathSync(process.execPath))return unsupported('Node executable does not match this runtime');
21
+ const root=realDirectory(packageRoot),entry=fs.realpathSync(entryPath);
22
+ if(!entry.startsWith(root+path.sep)||!fs.statSync(entry).isFile()||JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).name!=='@yunhozz/arc')return unsupported('ARC entry is outside the package');
23
+ const npmCli=npmExecutable(env);
24
+ async function query(args){const result=await runNpm(npmCli,args,{cwd,env});if(result.code!==0||result.signal)throw Error('npm context query failed');const value=result.stdout.trim();if(!path.isAbsolute(value)||value.includes('\n'))throw Error('npm returned an invalid context path');try{return fs.realpathSync(value);}catch(error){if(error.code!=='ENOENT')throw error;return path.resolve(value);}}
25
+ const prefix=await query(['prefix','--global']),npmRoot=await query(['root','--global']);
26
+ if([path.join(prefix,'lib/node_modules'),path.join(prefix,'node_modules')].includes(npmRoot)&&root===path.join(npmRoot,'@yunhozz/arc'))return{kind:'global',packageRoot:root,entryPath:entry,prefix,npmCli};
27
+ if(env.npm_command==='exec'){
28
+ const cache=realDirectory(await query(['config','get','cache'])),relative=path.relative(path.join(cache,'_npx'),root),parts=relative.split(path.sep);
29
+ if(parts.length===4&&parts[0]&&parts[0]!=='.'&&parts[0]!=='..'&&parts.slice(1).join('/')==='node_modules/@yunhozz/arc'){
30
+ const bin=path.join(cache,'_npx',parts[0],'node_modules/.bin'),paths=(env.PATH??'').split(path.delimiter);
31
+ if(paths.some(value=>{try{return realDirectory(value)===realDirectory(bin);}catch{return false;}})&&realDirectory(env.INIT_CWD))return{kind:'npx',packageRoot:root,entryPath:entry,npmCli};
32
+ }
33
+ }
34
+ return unsupported('ARC is not in a verified global or npm exec installation');
35
+ }catch{return unsupported('Cannot verify npm installation context');}
36
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+ function failure(message,code,cause){return Object.assign(new Error(message,{cause}),{code});}
5
+ export async function runNpm(npmCli,args,{cwd,env,signal}={}){
6
+ if(signal?.aborted)throw failure('npm cancelled','ARC_NPM_CANCELLED');
7
+ if(!path.isAbsolute(npmCli)||!Array.isArray(args)||args.some(arg=>typeof arg!=='string'))throw failure('Invalid npm invocation','ARC_NPM_START');
8
+ let cli;try{cli=fs.realpathSync(npmCli);if(!fs.statSync(cli).isFile())throw Error('Not a regular file');}catch(cause){throw failure('Cannot resolve npm CLI','ARC_NPM_START',cause);}
9
+ return new Promise((resolve,reject)=>{
10
+ const child=spawn(process.execPath,[cli,...args],{cwd,env,stdio:['ignore','pipe','pipe'],shell:false});
11
+ const stdout=[],stderr=[];let startError,cancelled=false;
12
+ child.stdout.on('data',bytes=>stdout.push(bytes));child.stderr.on('data',bytes=>stderr.push(bytes));
13
+ const abort=()=>{cancelled=true;child.kill('SIGTERM');};signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
14
+ child.once('error',error=>{startError=error;});
15
+ child.once('close',(code,killedBy)=>{signal?.removeEventListener('abort',abort);if(cancelled)reject(failure('npm cancelled','ARC_NPM_CANCELLED'));else if(startError)reject(failure('Cannot start npm','ARC_NPM_START',startError));else resolve({code,signal:killedBy,stdout:Buffer.concat(stdout).toString('utf8'),stderr:Buffer.concat(stderr).toString('utf8')});});
16
+ });
17
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+ import {runNpm as defaultRunNpm} from './process.mjs';
5
+ const validVersion=value=>typeof value==='string'&&/^\d+\.\d+\.\d+$/.test(value);
6
+ function cancelled(){return Object.assign(Error('Upgrade cancelled'),{code:'ARC_CANCELLED'});}
7
+ export async function resolveLatest({context,env,cwd,signal,runNpm=defaultRunNpm}){
8
+ const result=await runNpm(context.npmCli,['view','@yunhozz/arc@latest','--json'],{env,cwd,signal});
9
+ let value;try{value=JSON.parse(result.stdout);}catch{}
10
+ if(result.code!==0||result.signal||value?.name!=='@yunhozz/arc'||!validVersion(value.version))throw Error('Cannot resolve a valid latest ARC package');
11
+ return{name:'@yunhozz/arc',version:value.version};
12
+ }
13
+ export async function upgradeGlobal({context,expectedVersion,env,cwd,signal,runNpm=defaultRunNpm}){
14
+ if(context.kind!=='global'||!path.isAbsolute(context.prefix)||!validVersion(expectedVersion))throw Error('Invalid global upgrade version or prefix');
15
+ const result=await runNpm(context.npmCli,['install','--global','@yunhozz/arc@'+expectedVersion,'--prefix',context.prefix,'--ignore-scripts'],{env,cwd,signal});
16
+ return result.code===0&&!result.signal?{packageStatus:'updated',version:expectedVersion}:{packageStatus:'failed',error:Error('CLI replacement failed; the package may have changed')};
17
+ }
18
+ export async function runUpgradeChild({entryPath,request,env,cwd,signal}){
19
+ if(signal?.aborted)throw cancelled();
20
+ if(!path.isAbsolute(entryPath))throw Error('Invalid upgrade child path');
21
+ const entry=fs.realpathSync(entryPath);if(!fs.statSync(entry).isFile())throw Error('Invalid upgrade child');
22
+ return new Promise((resolve,reject)=>{
23
+ const child=spawn(process.execPath,[entry],{cwd,env,stdio:['pipe','pipe','pipe'],shell:false});let stdout='',stderr='',startError,aborted=false;
24
+ child.stdout.on('data',bytes=>{stdout+=bytes;});child.stderr.on('data',bytes=>{stderr+=bytes;});child.stdin.on('error',()=>{});
25
+ const abort=()=>{aborted=true;child.kill('SIGTERM');};signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
26
+ child.once('error',error=>{startError=error;});child.once('close',(code,killedBy)=>{signal?.removeEventListener('abort',abort);if(aborted)resolve({code:130,signal:killedBy,stdout,stderr});else if(startError)reject(Error('Cannot start the new ARC CLI',{cause:startError}));else resolve({code,signal:killedBy,stdout,stderr});});
27
+ child.stdin.end(JSON.stringify(request));
28
+ });
29
+ }
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it.",
153
+ "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Coder. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Coder per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Coder and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Coder even within the task. Before reusing Coder, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -0,0 +1,163 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import {gunzipSync} from 'node:zlib';
6
+ import {isDeepStrictEqual} from 'node:util';
7
+ import {spawnSync} from 'node:child_process';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {openRoot} from '../safe-fs/index.mjs';
10
+ import {verifyPayload} from '../installer/manifest.mjs';
11
+ import {readCheckedFile,typeOf,sameIdentity} from '../installer/targets.mjs';
12
+ import {runNpm} from '../npm/process.mjs';
13
+
14
+ const HOOKS=['preinstall','install','postinstall','prepublish','prepare','prepublishOnly','prepack','postpack','publish','postpublish','preuninstall','uninstall','postuninstall'];
15
+ const NAME='@yunhozz/arc',HOOK='node scripts/postinstall.mjs';
16
+ const same=(a,b)=>a.dev===b.dev&&a.ino===b.ino;
17
+ const digest=(bytes,algorithm='sha256')=>crypto.createHash(algorithm).update(bytes).digest('hex');
18
+ const json=file=>JSON.parse(fs.readFileSync(file,'utf8'));
19
+ function abort(signal){if(signal?.aborted)throw new Error('Release artifact generation cancelled');}
20
+ function inside(parent,child){const relative=path.relative(parent,child);return relative===''||(!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));}
21
+ function regular(file){const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink())throw new Error('Expected regular release input: '+file);return stat;}
22
+ function metadata(pkg,lock,{artifact=false}={}){
23
+ if(pkg.name!==NAME||!/^\d+\.\d+\.\d+$/.test(pkg.version)||pkg.private===true)throw new Error('Invalid release package identity');
24
+ for(const hook of HOOKS)if(pkg.scripts?.[hook]!==undefined&&!(artifact&&hook==='postinstall'&&pkg.scripts[hook]===HOOK))throw new Error('Unapproved package lifecycle: '+hook);
25
+ if(artifact&&pkg.scripts?.postinstall!==HOOK)throw new Error('Artifact postinstall is missing');
26
+ if(Object.hasOwn(pkg.scripts??{},'build')||Object.hasOwn(pkg,'workspaces'))throw new Error('Source Git preparation trigger is not allowed');
27
+ if(lock){
28
+ const root=lock.packages?.[''];if(lock.name!==pkg.name||lock.version!==pkg.version||root?.name!==pkg.name||root?.version!==pkg.version)throw new Error('Package lock identity mismatch');
29
+ for(const key of ['bin','dependencies','optionalDependencies','devDependencies','peerDependencies','engines'])if(!isDeepStrictEqual(root[key],pkg[key]))throw new Error('Package lock metadata mismatch: '+key);
30
+ if(artifact?root.hasInstallScript!==true:Object.hasOwn(root,'hasInstallScript'))throw new Error('Package lock install script metadata mismatch');
31
+ }
32
+ }
33
+ function inputNames(pkg){const names=new Set(['README.md','package.json','package-lock.json','MANIFEST.sha256']);for(const entry of pkg.files??[]){const name=entry.replace(/\/$/,'');if(!name||name.startsWith('/')||name.includes('\\')||name.split('/').some(part=>!part||part==='.'||part==='..')||/[*?\[\]]/.test(name))throw new Error('Unsupported package files entry');names.add(name);}return [...names].sort();}
34
+ function sourceMetadata(root){
35
+ const directory=openRoot(root);
36
+ try{const result={};for(const name of ['package.json','package-lock.json']){const stat=directory.lstat(name);if(typeOf(stat)!==0o100000)throw new Error('Expected regular source metadata: '+name);result[name]=readCheckedFile(directory,name,stat).bytes;}return result;}
37
+ finally{directory.close();}
38
+ }
39
+ function inventory(root,pkg){
40
+ const files=new Map(),directory=openRoot(root);
41
+ function visit(parent,name,relative){
42
+ const stat=parent.lstat(name),type=typeOf(stat);
43
+ if(type===0o040000){
44
+ const child=parent.openDir(name);try{if(!sameIdentity(stat,child.stat()))throw new Error('Release source directory changed');
45
+ const before=child.stat();for(const leaf of child.readDir())visit(child,leaf,relative+'/'+leaf);if(!sameIdentity(before,child.stat()))throw new Error('Release source directory changed while reading');
46
+ }finally{child.close();}
47
+ }else if(type===0o100000){const item=readCheckedFile(parent,name,stat);files.set(relative,{bytes:item.bytes,mode:Number(stat.mode&0o777n)});}
48
+ else throw new Error('Unsafe release source file: '+relative);
49
+ }
50
+ try{
51
+ for(const name of inputNames(pkg)){
52
+ const parts=name.split('/'),opened=[];let parent=directory;
53
+ try{for(const part of parts.slice(0,-1)){const stat=parent.lstat(part);if(typeOf(stat)!==0o040000)throw new Error('Unsafe release source parent');const child=parent.openDir(part);opened.push(child);if(!sameIdentity(stat,child.stat()))throw new Error('Release source parent changed');parent=child;}visit(parent,parts.at(-1),name);}
54
+ finally{for(const child of opened.reverse())child.close();}
55
+ }
56
+ }finally{directory.close();}
57
+ const snapshot=digest([...files].sort(([a],[b])=>a<b?-1:a>b?1:0).map(([name,{bytes,mode}])=>name+'\0'+mode+'\0'+bytes.length+'\0'+digest(bytes)+'\n').join(''));
58
+ return{files,snapshot};
59
+ }
60
+ function refresh(root){
61
+ const result=spawnSync(process.execPath,[path.join(root,'scripts/update-manifest.mjs'),'--root',root],{encoding:'utf8',env:{PATH:process.env.PATH}});
62
+ if(result.status!==0)throw new Error('Staging manifest generation failed: '+result.stderr);
63
+ }
64
+ function safeOutput(sourceRoot,outDir){
65
+ if(!path.isAbsolute(outDir)||path.normalize(outDir)!==outDir||inside(sourceRoot,outDir))throw new Error('Release output must be an absolute directory outside source');
66
+ let current=path.parse(outDir).root;for(const component of outDir.slice(current.length).split(path.sep).filter(Boolean)){
67
+ current=path.join(current,component);try{const stat=fs.lstatSync(current);if(stat.isSymbolicLink()||!stat.isDirectory())throw new Error('Unsafe release output path');}catch(error){if(error.code!=='ENOENT')throw error;if(current!==outDir)throw new Error('Release output parent is missing');}
68
+ }
69
+ const parent=fs.realpathSync(path.dirname(outDir));if(inside(sourceRoot,parent))throw new Error('Release output resolves inside source');
70
+ try{fs.mkdirSync(outDir,{mode:0o700});}catch(error){if(error.code!=='EEXIST')throw error;}
71
+ if(fs.readdirSync(outDir).length)throw new Error('Release output is occupied or incomplete; use a new output directory');
72
+ return openRoot(outDir);
73
+ }
74
+ function readText(bytes,start,length){return bytes.subarray(start,start+length).toString('utf8').replace(/\0.*$/s,'');}
75
+ function number(bytes,start,length){const text=readText(bytes,start,length).trim();if(!/^[0-7]*$/.test(text))throw new Error('Unsupported tar number');const value=parseInt(text||'0',8);if(!Number.isSafeInteger(value))throw new Error('Invalid tar size');return value;}
76
+ function pax(bytes){
77
+ const result={};let offset=0;while(offset<bytes.length){const space=bytes.indexOf(32,offset);if(space<0)throw new Error('Malformed tar metadata');const size=Number(bytes.subarray(offset,space).toString());if(!Number.isSafeInteger(size)||size<=space-offset+1||offset+size>bytes.length)throw new Error('Malformed tar metadata');const record=bytes.subarray(space+1,offset+size).toString('utf8');if(!record.endsWith('\n'))throw new Error('Malformed tar metadata');const equal=record.indexOf('=');if(equal<1)throw new Error('Malformed tar metadata');result[record.slice(0,equal)]=record.slice(equal+1,-1);offset+=size;}return result;
78
+ }
79
+ function archive(bytes){
80
+ const raw=gunzipSync(bytes,{maxOutputLength:256*1024*1024}),files=new Map();let extended={},global={};
81
+ for(let offset=0;offset+512<=raw.length;){
82
+ const header=raw.subarray(offset,offset+512);if(header.every(byte=>byte===0))break;
83
+ let sum=0;for(let i=0;i<512;i++)sum+=i>=148&&i<156?32:header[i];if(sum!==number(header,148,8))throw new Error('Invalid tar header checksum');
84
+ const size=number(header,124,12),type=String.fromCharCode(header[156]||48),end=offset+512+size;if(end>raw.length)throw new Error('Truncated tar archive');
85
+ const data=raw.subarray(offset+512,end);offset+=512+Math.ceil(size/512)*512;
86
+ if(type==='x'||type==='g'){const values=pax(data);if(type==='g')global={...global,...values};else extended=values;continue;}
87
+ const attrs={...global,...extended};extended={};if(attrs.linkpath!==undefined)throw new Error('Archive links are forbidden');
88
+ let name=attrs.path??[readText(header,345,155),readText(header,0,100)].filter(Boolean).join('/');
89
+ if(name.endsWith('/'))name=name.slice(0,-1);
90
+ if(name!=='package'&&(!name.startsWith('package/')||name.split('/').some(part=>!part||part==='.'||part==='..')||name.includes('\\')||name.includes('\0')))throw new Error('Unsafe archive path');
91
+ if(type==='5')continue;if(type!=='0')throw new Error('Archive links and special files are forbidden');
92
+ if(name==='package'||files.has(name))throw new Error('Duplicate archive path');
93
+ if(attrs.size!==undefined&&Number(attrs.size)!==size)throw new Error('Unsupported tar size override');
94
+ files.set(name.slice(8),{bytes:Buffer.from(data),mode:number(header,100,8)&0o777});
95
+ }
96
+ if(!files.size)throw new Error('Empty release archive');return files;
97
+ }
98
+ function validateTarball(bytes,{expectedName,expectedVersion}){
99
+ const files=archive(bytes),pkgFile=files.get('package.json');if(!pkgFile)throw new Error('Artifact package metadata is missing');
100
+ const pkg=JSON.parse(pkgFile.bytes);metadata(pkg,undefined,{artifact:true});
101
+ if(pkg.name!==expectedName||pkg.version!==expectedVersion)throw new Error('Artifact package identity mismatch');
102
+ for(const name of ['scripts/postinstall.mjs','lib/cli/postinstall.mjs','lib/cli/terminal.mjs'])if(!files.has(name))throw new Error('Artifact runtime asset missing: '+name);
103
+ if(files.has('package-lock.json')||[...files.keys()].some(name=>name.split('/').some(part=>part==='.git'||part.startsWith('.arc-release'))))throw new Error('Private staging data in artifact');
104
+ const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-release-verify-'));
105
+ try{
106
+ for(const[name,item]of files){const filename=path.join(root,name);fs.mkdirSync(path.dirname(filename),{recursive:true,mode:0o700});fs.writeFileSync(filename,item.bytes,{mode:item.mode});}
107
+ verifyPayload({packageRoot:root,safeFs:{openRoot}});
108
+ }finally{fs.rmSync(root,{recursive:true,force:true});}
109
+ return pkg;
110
+ }
111
+ async function pack({stagingRoot,workRoot,npmCli,signal}){
112
+ const packDir=path.join(workRoot,'packed'),home=path.join(workRoot,'home');fs.mkdirSync(packDir);fs.mkdirSync(home);
113
+ const env={PATH:process.env.PATH,HOME:home,npm_config_userconfig:path.join(home,'user.npmrc'),npm_config_globalconfig:path.join(home,'global.npmrc'),npm_config_cache:path.join(workRoot,'cache'),npm_config_offline:'true',npm_config_ignore_scripts:'true',npm_config_audit:'false',npm_config_fund:'false',npm_config_update_notifier:'false'};
114
+ const result=await runNpm(npmCli,['pack','--ignore-scripts','--offline','--json','--pack-destination',packDir],{cwd:stagingRoot,env,signal});
115
+ if(result.code!==0)throw new Error('Release npm pack failed: '+result.stderr);
116
+ const records=JSON.parse(result.stdout),record=Array.isArray(records)?records[0]:records[NAME];
117
+ if(!record||typeof record.filename!=='string'||path.basename(record.filename)!==record.filename)throw new Error('Invalid npm pack result');
118
+ const filename=path.join(packDir,record.filename);regular(filename);return fs.readFileSync(filename);
119
+ }
120
+ export async function buildReleaseArtifact({sourceRoot,outDir,npmCli,signal,deps={}}){
121
+ if(!path.isAbsolute(sourceRoot)||fs.realpathSync(sourceRoot)!==sourceRoot)throw new Error('Source root must be an absolute real path');
122
+ abort(signal);const inputs=sourceMetadata(sourceRoot),pkg=JSON.parse(inputs['package.json']),lock=JSON.parse(inputs['package-lock.json']);metadata(pkg,lock);
123
+ const source=inventory(sourceRoot,pkg);for(const name of ['package.json','package-lock.json'])if(!inputs[name].equals(source.files.get(name).bytes))throw new Error('Release source metadata changed during validation');
124
+ verifyPayload({packageRoot:sourceRoot,safeFs:{openRoot}});if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during validation');
125
+ let output,lockIdentity,workRoot,workIdentity;const id=crypto.randomUUID(),partial='.arc-release-'+id+'.partial';let partialExists=false;
126
+ try{
127
+ output=safeOutput(sourceRoot,outDir);const lockFile=output.openFile('.arc-release.lock',{createExclusive:true,read:false});try{lockFile.writeAll(JSON.stringify({schema:1,owner:id,pid:process.pid,state:'incomplete'}));lockIdentity=lockFile.stat();lockFile.sync();}finally{lockFile.close();}
128
+ workRoot=fs.mkdtempSync(path.join(fs.realpathSync(path.dirname(outDir)),'.arc-release-stage-'));workIdentity=fs.lstatSync(workRoot);
129
+ const stagingRoot=path.join(workRoot,'package');fs.mkdirSync(stagingRoot,{mode:0o700});
130
+ for(const[name,item]of source.files){const filename=path.join(stagingRoot,name);fs.mkdirSync(path.dirname(filename),{recursive:true,mode:0o700});fs.writeFileSync(filename,item.bytes,{flag:'wx',mode:item.mode});}
131
+ const stagedPkg={...pkg,scripts:{...pkg.scripts,postinstall:HOOK}},stagedLock=structuredClone(lock);stagedLock.packages[''].hasInstallScript=true;
132
+ fs.writeFileSync(path.join(stagingRoot,'package.json'),JSON.stringify(stagedPkg,null,2)+'\n');fs.writeFileSync(path.join(stagingRoot,'package-lock.json'),JSON.stringify(stagedLock,null,2)+'\n');metadata(stagedPkg,stagedLock,{artifact:true});
133
+ fs.writeFileSync(path.join(stagingRoot,'.arc-release-staging.json'),JSON.stringify({schema:1,owner:id,state:'incomplete',sourceSnapshot:source.snapshot}));
134
+ refresh(stagingRoot);verifyPayload({packageRoot:stagingRoot,safeFs:{openRoot}});await deps.onStaged?.({stagingRoot,signal});abort(signal);
135
+ const bytes=await(deps.pack??pack)({stagingRoot,workRoot,npmCli,signal});abort(signal);validateTarball(bytes,{expectedName:pkg.name,expectedVersion:pkg.version});
136
+ if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during generation');abort(signal);
137
+ const tarballPath=path.join(outDir,'arc-release.tgz'),receiptPath=path.join(outDir,'arc-release.json');
138
+ const receipt={schema:1,name:pkg.name,version:pkg.version,tarballPath,sha512:digest(bytes,'sha512'),sourceSnapshot:source.snapshot,verified:true};
139
+ const file=output.openFile(partial,{createExclusive:true,read:false});partialExists=true;try{file.writeAll(bytes);file.sync();}finally{file.close();}
140
+ output.renameNoReplaceTo(partial,output,'arc-release.tgz');partialExists=false;
141
+ const receiptFile=output.openFile(partial,{createExclusive:true,read:false});partialExists=true;try{receiptFile.writeAll(JSON.stringify(receipt,null,2)+'\n');receiptFile.sync();}finally{receiptFile.close();}
142
+ output.renameNoReplaceTo(partial,output,'arc-release.json');partialExists=false;output.sync();
143
+ if(!same(output.stat(),fs.lstatSync(outDir,{bigint:true}))||fs.lstatSync(outDir).isSymbolicLink())throw new Error('Release output directory changed');
144
+ return{...receipt,receiptPath};
145
+ }finally{
146
+ if(workRoot){try{if(same(workIdentity,fs.lstatSync(workRoot)))fs.rmSync(workRoot,{recursive:true,force:true});}catch(error){if(error.code!=='ENOENT')throw error;}}
147
+ if(output){try{if(partialExists)output.unlink(partial);if(lockIdentity&&same(lockIdentity,output.lstat('.arc-release.lock')))output.unlink('.arc-release.lock');}finally{output.close();}}
148
+ }
149
+ }
150
+ export async function verifyReleaseArtifact({outDir,expectedName,expectedVersion}){
151
+ if(!path.isAbsolute(outDir)||fs.realpathSync(outDir)!==outDir)throw new Error('Unsafe release output path');
152
+ if(fs.readdirSync(outDir).includes('.arc-release.lock'))throw new Error('Release output is locked or incomplete');
153
+ regular(path.join(outDir,'arc-release.json'));const receipt=json(path.join(outDir,'arc-release.json'));
154
+ if(receipt.schema!==1||receipt.verified!==true||receipt.name!==expectedName||receipt.version!==expectedVersion||receipt.tarballPath!==path.join(outDir,'arc-release.tgz')||!/^[a-f0-9]{128}$/.test(receipt.sha512)||!/^[a-f0-9]{64}$/.test(receipt.sourceSnapshot))throw new Error('Invalid release receipt');
155
+ regular(receipt.tarballPath);const bytes=fs.readFileSync(receipt.tarballPath);if(digest(bytes,'sha512')!==receipt.sha512)throw new Error('Release tarball digest mismatch');
156
+ validateTarball(bytes,{expectedName,expectedVersion});return receipt;
157
+ }
158
+ export function resolveNpmCli(){
159
+ const executable=process.env.npm_execpath;if(executable&&path.isAbsolute(executable)&&fs.existsSync(executable))return fs.realpathSync(executable);
160
+ const candidate=path.resolve(path.dirname(process.execPath),'../lib/node_modules/npm/bin/npm-cli.js');
161
+ if(fs.existsSync(candidate))return fs.realpathSync(candidate);throw new Error('Cannot resolve npm CLI; use npm run pack:release');
162
+ }
163
+ export const releaseSourceRoot=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -14,7 +14,8 @@
14
14
  "build:native": "node scripts/build-native.mjs",
15
15
  "check:manifest": "node scripts/update-manifest.mjs --check",
16
16
  "update:role-policy": "node scripts/update-role-policy.mjs",
17
- "check:role-policy": "node scripts/update-role-policy.mjs --check"
17
+ "check:role-policy": "node scripts/update-role-policy.mjs --check",
18
+ "pack:release": "node scripts/pack-release.mjs"
18
19
  },
19
20
  "devDependencies": {
20
21
  "node-gyp": "11.5.0",
@@ -0,0 +1,9 @@
1
+ import {buildReleaseArtifact,resolveNpmCli,releaseSourceRoot} from '../lib/release/artifact.mjs';
2
+ const args=process.argv.slice(2),controller=new AbortController();
3
+ const cancel=()=>controller.abort();process.once('SIGINT',cancel);process.once('SIGTERM',cancel);
4
+ try{
5
+ if(args.length!==2||args[0]!=='--out-dir')throw Error('Usage: npm run pack:release -- --out-dir <absolute-output-directory>');
6
+ const result=await buildReleaseArtifact({sourceRoot:releaseSourceRoot,outDir:args[1],npmCli:resolveNpmCli(),signal:controller.signal});
7
+ console.log(JSON.stringify(result,null,2));
8
+ }catch(error){console.error('Release artifact error: '+error.message);process.exitCode=controller.signal.aborted?130:1;}
9
+ finally{process.removeListener('SIGINT',cancel);process.removeListener('SIGTERM',cancel);}
@@ -0,0 +1,4 @@
1
+ import {fileURLToPath} from 'node:url';
2
+ import {runPostinstall} from '../lib/cli/postinstall.mjs';
3
+ const packageRoot=fileURLToPath(new URL('../',import.meta.url));
4
+ process.exitCode=await runPostinstall({packageRoot,cwd:packageRoot});
@@ -0,0 +1,6 @@
1
+ import {verifyReleaseArtifact} from '../lib/release/artifact.mjs';
2
+ const args=process.argv.slice(2);
3
+ try{
4
+ if(args.length!==6||args[0]!=='--out-dir'||args[2]!=='--name'||args[4]!=='--version')throw Error('Usage: node scripts/verify-release.mjs --out-dir <absolute-output-directory> --name <package-name> --version <version>');
5
+ console.log(JSON.stringify(await verifyReleaseArtifact({outDir:args[1],expectedName:args[3],expectedVersion:args[5]}),null,2));
6
+ }catch(error){console.error('Release verification error: '+error.message);process.exitCode=1;}