claude-company 0.2.0 → 0.2.3
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/.claude/hooks/guard_models.py +121 -5
- package/.claude/settings.json +3 -0
- package/ORCHESTRATOR.md +5 -0
- package/company/GATES.md +3 -1
- package/company/METHOD.md +10 -0
- package/company/models.json +7 -0
- package/company/state/DECISIONS.md +7 -1
- package/company/state/RESUME.md +40 -8
- package/company/state/STATUS.md +15 -3
- package/company/state/WORRIES.md +4 -0
- package/company/witnesses.json +100 -1
- package/docs/customizing.md +2 -0
- package/install.sh +83 -13
- package/package.json +5 -2
- package/update.sh +118 -8
- package/company/briefs/shipped/brief-adr-hardening.md +0 -100
- package/company/briefs/shipped/brief-cli-self-update.md +0 -168
- package/company/briefs/shipped/brief-cli-update.md +0 -219
- package/company/briefs/shipped/brief-delegation-enforcement.md +0 -484
- package/company/briefs/shipped/brief-docs-sync.md +0 -113
- package/company/briefs/shipped/brief-wave1-enforcement.md +0 -184
- package/company/briefs/shipped/brief-wave2-doctrine.md +0 -162
- package/company/briefs/shipped/brief-wave2-enforce.md +0 -200
- package/company/briefs/shipped/brief-wave3-sdlc.md +0 -180
- package/company/change-requests/CR-UPD-1-freeze-update-artifacts.md +0 -54
- package/company/specs/shipped/spec-cli-self-update.md +0 -409
- package/company/specs/shipped/spec-cli-update.md +0 -431
|
@@ -43,6 +43,26 @@ def load_manifest(root):
|
|
|
43
43
|
return roles
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def load_builtins(root):
|
|
47
|
+
"""Return {type: model} from company/models.json `builtins`, or None.
|
|
48
|
+
|
|
49
|
+
Mirrors load_manifest and fails open (None) on a missing/malformed/
|
|
50
|
+
unreadable file or section. Keys starting with '$' (e.g. "$comment") are
|
|
51
|
+
dropped so they are never treated as a spawnable type, and only string
|
|
52
|
+
model values are kept.
|
|
53
|
+
"""
|
|
54
|
+
cfg = c.read_json_file(os.path.join(root, "company", "models.json"))
|
|
55
|
+
if not isinstance(cfg, dict):
|
|
56
|
+
return None
|
|
57
|
+
builtins = cfg.get("builtins")
|
|
58
|
+
if not isinstance(builtins, dict):
|
|
59
|
+
return None
|
|
60
|
+
return {
|
|
61
|
+
k: v for k, v in builtins.items()
|
|
62
|
+
if not k.startswith("$") and isinstance(v, str)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
46
66
|
def is_hotfix(root):
|
|
47
67
|
task = c.active_task(root)
|
|
48
68
|
return isinstance(task, dict) and task.get("type") == "hotfix"
|
|
@@ -67,15 +87,53 @@ def new_text(tool_name, tool_input):
|
|
|
67
87
|
return "\n".join(parts)
|
|
68
88
|
|
|
69
89
|
|
|
70
|
-
def handle_spawn(root, tool_input, roles):
|
|
90
|
+
def handle_spawn(root, tool_input, roles, builtins):
|
|
71
91
|
role = None
|
|
72
92
|
for field in SPAWN_TYPE_FIELDS:
|
|
73
93
|
val = tool_input.get(field)
|
|
74
94
|
if val:
|
|
75
95
|
role = val
|
|
76
96
|
break
|
|
77
|
-
if role is None
|
|
78
|
-
return #
|
|
97
|
+
if role is None:
|
|
98
|
+
return # no spawn type -> allow
|
|
99
|
+
if role in roles:
|
|
100
|
+
# OQ-MRA-02 assumption: roles governs on any collision - a type present
|
|
101
|
+
# in both `roles` and `builtins` is decided here, by the roles branch.
|
|
102
|
+
handle_role_spawn(root, tool_input, roles, role)
|
|
103
|
+
return
|
|
104
|
+
# Not a manifest role. A built-in agent type has no .claude/agents
|
|
105
|
+
# frontmatter to inherit from, so a bare spawn would silently take the
|
|
106
|
+
# SESSION model - require an explicit matching override.
|
|
107
|
+
if builtins is None:
|
|
108
|
+
return # no builtins section -> builtin enforcement inert (fail open)
|
|
109
|
+
if role not in builtins:
|
|
110
|
+
return # unknown type -> allow
|
|
111
|
+
pin = builtins[role]
|
|
112
|
+
override = tool_input.get("model")
|
|
113
|
+
if override == pin:
|
|
114
|
+
return # explicit matching override -> allow
|
|
115
|
+
if is_hotfix(root):
|
|
116
|
+
c.log_bypass(root, HOOK, "spawn " + role, "hotfix mode")
|
|
117
|
+
return
|
|
118
|
+
if not override:
|
|
119
|
+
c.block(
|
|
120
|
+
root, HOOK, "spawn " + role,
|
|
121
|
+
"builtin spawn no override (pin {})".format(pin),
|
|
122
|
+
"BLOCKED: spawning built-in '{}' with no model override would "
|
|
123
|
+
"inherit the session model - built-in agent types have no "
|
|
124
|
+
"company/models.json frontmatter pin to fall back on.\n"
|
|
125
|
+
"Fix: pass model: '{}'".format(role, pin),
|
|
126
|
+
)
|
|
127
|
+
c.block(
|
|
128
|
+
root, HOOK, "spawn " + role,
|
|
129
|
+
"builtin spawn override {} != pin {}".format(override, pin),
|
|
130
|
+
"BLOCKED: spawning built-in '{}' with model '{}' contradicts "
|
|
131
|
+
"company/models.json (built-in pin: '{}').\n"
|
|
132
|
+
"Fix: pass model: '{}'".format(role, override, pin, pin),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def handle_role_spawn(root, tool_input, roles, role):
|
|
79
137
|
override = tool_input.get("model")
|
|
80
138
|
if not override:
|
|
81
139
|
return # no override -> role inherits the manifest model, allow
|
|
@@ -124,8 +182,53 @@ def handle_frontmatter_edit(root, tool_name, tool_input, roles):
|
|
|
124
182
|
)
|
|
125
183
|
|
|
126
184
|
|
|
185
|
+
def check_spawn_wiring(root):
|
|
186
|
+
"""Assert .claude/settings.json wires guard_models on the Task spawn tool.
|
|
187
|
+
|
|
188
|
+
Passes when some PreToolUse group has a matcher covering the Task tool -
|
|
189
|
+
"Task" is one of its "|"-separated alternatives (the shipped matcher is
|
|
190
|
+
"Task|Agent") - and one of that group's hooks runs a command referencing
|
|
191
|
+
guard_models.py. Only the project settings.json counts; settings.local.json
|
|
192
|
+
is ignored. Returns (ok, fixit_message).
|
|
193
|
+
"""
|
|
194
|
+
fixit = (
|
|
195
|
+
"spawn enforcement is not wired: .claude/settings.json has no "
|
|
196
|
+
"PreToolUse matcher covering the Task spawn tool that runs "
|
|
197
|
+
"guard_models.py - re-run `claude-company install` or `update` to "
|
|
198
|
+
"re-add it."
|
|
199
|
+
)
|
|
200
|
+
settings = c.read_json_file(
|
|
201
|
+
os.path.join(root, ".claude", "settings.json")
|
|
202
|
+
)
|
|
203
|
+
if not isinstance(settings, dict):
|
|
204
|
+
return False, fixit
|
|
205
|
+
hooks = settings.get("hooks")
|
|
206
|
+
if not isinstance(hooks, dict):
|
|
207
|
+
return False, fixit
|
|
208
|
+
groups = hooks.get("PreToolUse")
|
|
209
|
+
if not isinstance(groups, list):
|
|
210
|
+
return False, fixit
|
|
211
|
+
for group in groups:
|
|
212
|
+
if not isinstance(group, dict):
|
|
213
|
+
continue
|
|
214
|
+
matcher = group.get("matcher") or ""
|
|
215
|
+
if "Task" not in matcher.split("|"):
|
|
216
|
+
continue
|
|
217
|
+
for h in group.get("hooks") or []:
|
|
218
|
+
if isinstance(h, dict) and "guard_models.py" in (
|
|
219
|
+
h.get("command") or ""
|
|
220
|
+
):
|
|
221
|
+
return True, ""
|
|
222
|
+
return False, fixit
|
|
223
|
+
|
|
224
|
+
|
|
127
225
|
def run_check(root):
|
|
128
|
-
"""Compare every agent frontmatter against the manifest. Exit 0/1.
|
|
226
|
+
"""Compare every agent frontmatter against the manifest. Exit 0/1.
|
|
227
|
+
|
|
228
|
+
FR-MRA-06: this frontmatter/agent-file comparison is based ONLY on `roles`.
|
|
229
|
+
Built-in types live in the separate `builtins` section and never demand a
|
|
230
|
+
.claude/agents/<type>.md file, so their absence is never a mismatch here.
|
|
231
|
+
"""
|
|
129
232
|
roles = load_manifest(root)
|
|
130
233
|
if roles is None:
|
|
131
234
|
print("company/models.json missing or unreadable")
|
|
@@ -178,6 +281,19 @@ def run_check(root):
|
|
|
178
281
|
print("\nmismatches:")
|
|
179
282
|
for m in mismatches:
|
|
180
283
|
print(" - " + m)
|
|
284
|
+
|
|
285
|
+
# LOUD wiring assertion (FR-MRA-07), gated on builtins presence.
|
|
286
|
+
# OQ-MRA-04 assumption: guard_models asserts only its OWN enforcement
|
|
287
|
+
# wiring. Old manifests without a `builtins` section (load_builtins ->
|
|
288
|
+
# None) skip this entirely and behave exactly as before.
|
|
289
|
+
wiring_ok = True
|
|
290
|
+
if load_builtins(root) is not None:
|
|
291
|
+
wiring_ok, fixit = check_spawn_wiring(root)
|
|
292
|
+
if not wiring_ok:
|
|
293
|
+
print("\nspawn wiring:")
|
|
294
|
+
print(" - " + fixit)
|
|
295
|
+
|
|
296
|
+
if mismatches or not wiring_ok:
|
|
181
297
|
return 1
|
|
182
298
|
print("\nall agent models agree with company/models.json")
|
|
183
299
|
return 0
|
|
@@ -206,7 +322,7 @@ def main():
|
|
|
206
322
|
if roles is None:
|
|
207
323
|
sys.exit(0) # no manifest -> fail open
|
|
208
324
|
if tool_name in ("Task", "Agent"):
|
|
209
|
-
handle_spawn(root, tool_input, roles)
|
|
325
|
+
handle_spawn(root, tool_input, roles, load_builtins(root))
|
|
210
326
|
elif tool_name in ("Edit", "Write", "MultiEdit"):
|
|
211
327
|
handle_frontmatter_edit(root, tool_name, tool_input, roles)
|
|
212
328
|
except SystemExit:
|
package/.claude/settings.json
CHANGED
package/ORCHESTRATOR.md
CHANGED
|
@@ -178,6 +178,11 @@ building agents into isolated worktrees:
|
|
|
178
178
|
git worktree add .claude/worktrees/<task-slug> -b task/<task-slug>
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
The Workflow tool is FORBIDDEN by default: its internal `agent()` spawns fire no
|
|
182
|
+
PreToolUse events, so `guard_models` cannot pin their model - permit it only
|
|
183
|
+
with explicit owner authorization and a `model` pin in every `agent()` call,
|
|
184
|
+
including all early stages (see `company/METHOD.md`).
|
|
185
|
+
|
|
181
186
|
Skeleton for a tech-lead (adapt for direct developer dispatch on `quick`):
|
|
182
187
|
|
|
183
188
|
```
|
package/company/GATES.md
CHANGED
|
@@ -65,7 +65,9 @@ These rungs are wired for this repo; a project inherits them and adds its own.
|
|
|
65
65
|
- **G7 models (`python3 .claude/hooks/guard_models.py --check`).** The model
|
|
66
66
|
routing manifest and agent frontmatter must agree: spawn overrides and
|
|
67
67
|
per-agent model declarations match `models.json`, so no agent silently runs
|
|
68
|
-
on the wrong model.
|
|
68
|
+
on the wrong model. It also asserts the Task|Agent spawn hook is wired into
|
|
69
|
+
`.claude/settings.json`, so the enforcement cannot ship as code without teeth.
|
|
70
|
+
(Shipped in wave 1.)
|
|
69
71
|
- **G8 audit - the last rung, network-bound.** A dependency and CVE scan: no
|
|
70
72
|
known-vulnerable dependency ships. It runs last because it reaches the
|
|
71
73
|
network; keep it blocking anyway.
|
package/company/METHOD.md
CHANGED
|
@@ -163,6 +163,16 @@ All under `company/state/`, all owned by the CEO:
|
|
|
163
163
|
6. Business-policy open questions. Agents run on tagged fallbacks; the owner
|
|
164
164
|
answers the question.
|
|
165
165
|
|
|
166
|
+
## The Workflow tool is outside enforcement
|
|
167
|
+
|
|
168
|
+
The Workflow tool runs its own internal `agent()` spawns that fire NO PreToolUse
|
|
169
|
+
events - the hooks never see them, so `guard_models` cannot pin their model and
|
|
170
|
+
they inherit the main-loop model. It is therefore FORBIDDEN by default in
|
|
171
|
+
company projects. It is permitted only with explicit owner authorization AND a
|
|
172
|
+
`model` pin in EVERY `agent()` call, including all early-stage agents: resuming
|
|
173
|
+
a dead workflow re-runs the incomplete early-stage agents live, so partial
|
|
174
|
+
pinning does not hold.
|
|
175
|
+
|
|
166
176
|
## Writing discipline
|
|
167
177
|
|
|
168
178
|
All writing in this repository stays hook-clean: straight quotes, ' - ' rather
|
package/company/models.json
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
"security-reviewer": "opus", "devops-engineer": "opus",
|
|
8
8
|
"docs-librarian": "opus", "ideation-strategist": "opus"
|
|
9
9
|
},
|
|
10
|
+
"builtins": {
|
|
11
|
+
"$comment": "Built-in agent types (Explore, general-purpose, Plan, claude) have no .claude/agents frontmatter pin, so a bare spawn silently inherits the SESSION model - guard_models therefore requires an explicit matching model override for these. The Workflow tool is hook-invisible (its agent() spawns fire no PreToolUse events) and is forbidden by default - see company/METHOD.md.",
|
|
12
|
+
"Explore": "opus",
|
|
13
|
+
"general-purpose": "opus",
|
|
14
|
+
"Plan": "opus",
|
|
15
|
+
"claude": "opus"
|
|
16
|
+
},
|
|
10
17
|
"pricing": {
|
|
11
18
|
"$comment": "OQ-W1-01 estimate only, NOT billing truth - used by .claude/hooks/cost_capture.py for rough spend visibility in company/state/costs.log and /standup. USD per million tokens (MTok). cache_write is 1.25x input, cache_read is 0.1x input.",
|
|
12
19
|
"opus": {"input": 15, "output": 75, "cache_write": 18.75, "cache_read": 1.5}
|
|
@@ -13,4 +13,10 @@ fallbacks until a row lands here._
|
|
|
13
13
|
| 5 | 2026-07-10 | Enforcement design direction | Owner REJECTED budgets/role bans twice; approved principle-derived gates (mechanism 5 + written-record consistency) and demanded low-token injection + GitHub tracking gate | all future gate design |
|
|
14
14
|
| 6 | 2026-07-15 | cli-update delivery (update subcommand + provenance manifest, PR #57, issues #54-#56) | ACCEPTED - owner: "test it, the merge it to main"; CEO re-tested end-to-end (edit kept, .new written, deleted file restored, config untouched), merged 7726c99, integrated main verified (213 hooks + 40 CLI + 56 engine green), witnesses W-014..W-016 | CLI, installer, frozen registry |
|
|
15
15
|
| 7 | 2026-07-15 | cli-self-update delivery (update refreshes the CLI first via npx handoff, PR #60, issue #59) | ACCEPTED - owner: "push to main, then to npm"; merged a09b463, integrated main verified (213 + 57 green), witnesses W-017..W-018 | CLI driver |
|
|
16
|
-
| 8 | 2026-07-15 | Release v0.2.0 to npm (
|
|
16
|
+
| 8 | 2026-07-15 | Release v0.2.0 to npm (tag v0.2.0 at 5913374; notes: PR #57/#60 bodies) | PUBLISHED 2026-07-15 by the OWNER manually from the clean tag clone (link-based npm 2FA - publishes are always owner-manual or git-CI, never agent-run). Registry verified: dist-tags.latest 0.2.0. Semver minor | npm registry |
|
|
17
|
+
| 9 | 2026-07-15 | DevMesh migration (owner: "use claude-company on this WITHOUT breaking custom changes... preserve DevMesh specific context") | DELIVERED - umbrella root git-initialized (machinery only), primitive company ported to company/, all customizations preserved, stale docs archived (nothing deleted). Verification: invariants gate PASS, 6 frozen surfaces probe-blocked. Found+fixed upstream #67/#68 | DevMesh polyrepo |
|
|
18
|
+
| 10 | 2026-07-15 | Release v0.2.1 to npm (patch: #64 enforcer arming, #67 merger fix + field-heal, #68 record-leak) | PREPPED by CEO (bump PR + tag on merge); publish is the owner's button per standing rule (link-based 2FA). SUPERSEDED by v0.2.2 (#12) before publish - registry never received 0.2.1 | npm registry |
|
|
19
|
+
| 11 | 2026-07-22 | model-routing-arming delivery (builtins enforcement + wiring gate + migration + Workflow doctrine, PR #77, issues #74-#76) | ACCEPTED - owner: "merge everything and give command to push to npm"; CEO merged cd07fb6, integrated main verified (hooks 222 + npm 61 + engine 111 green, gates stamped), live enforcement certified (contradict/bare exit 2, match 0, dormancy probe red), witnesses W-026/W-027/W-028 | hooks, models.json, installer, doctrine |
|
|
20
|
+
| 12 | 2026-07-22 | Release v0.2.2 to npm (rolls up unpublished 0.2.1 + #77 model-routing arming) | PREPPED by CEO; owner pushed tag v0.2.2 (at ad3dda8) but publish SUPERSEDED by v0.2.3 (#14) which adds #83 - registry goes 0.2.0 -> 0.2.3 directly | npm registry |
|
|
21
|
+
| 13 | 2026-07-23 | spawn-depth-shipping delivery (CC 2.1.21 depth-1 flattening; template env + installer env merge + guards, PR #83, issues #79-#82) | ACCEPTED - owner: "merge it"; merged 6061814, CEO independent verification (224/61/123 green, merge probes, byte-identity), witness W-029 | template settings, installer, tests |
|
|
22
|
+
| 14 | 2026-07-23 | Release v0.2.3 to npm (v0.2.2 content + #83 spawn-depth) | PREPPED by CEO per owner direction ("change to v0.2.3"); owner tags v0.2.3 at the release-PR merge commit and publishes from a clean tag clone (link-based 2FA, owner-manual); notes: PR #77/#83 bodies | npm registry |
|
package/company/state/RESUME.md
CHANGED
|
@@ -14,6 +14,35 @@ witnesses/models/tests/audit. Owner acceptance recorded (DECISIONS #3).
|
|
|
14
14
|
|
|
15
15
|
## 2. Next actions, in order
|
|
16
16
|
|
|
17
|
+
000. model-routing-arming SHIPPED 2026-07-22 (PR #77 merged cd07fb6,
|
|
18
|
+
closes #74-#76; acceptance DECISIONS #11; witnesses W-026 wiring
|
|
19
|
+
assertion / W-027 merge byte-identity / W-028 bare-builtin block).
|
|
20
|
+
Integrated main verified by CEO: hooks 222 OK, npm 61, update
|
|
21
|
+
engine 111, --check 0, trace 21/21 after citation fix, live
|
|
22
|
+
certification (contradict/bare exit 2, match 0; dormancy probe:
|
|
23
|
+
Task|Agent group stripped -> --check exit 1 + fix-it). Worktree
|
|
24
|
+
and task branch removed; brief+spec archived to shipped/.
|
|
25
|
+
RELEASE 0.2.2 PREPPED (task release-0.2.2-closeout, quick): rolls
|
|
26
|
+
up unpublished 0.2.1 + #77. Release PR carries version bump,
|
|
27
|
+
trace-citation fix (test-side FR/BR citations + new
|
|
28
|
+
test_template_and_doctrine_shipped covering FR-MRA-01/11/12),
|
|
29
|
+
paperwork archive, DECISIONS #11/#12, board sync. AWAITING OWNER:
|
|
30
|
+
merge release PR if session dies before merge, then tag v0.2.2 at
|
|
31
|
+
its merge commit and publish from a clean tag clone (memory:
|
|
32
|
+
npm-publish-owner-only; registry still on 0.2.0). Near-miss worth
|
|
33
|
+
remembering: a .format arg mismatch made the wrong-override path
|
|
34
|
+
fail OPEN until the live replay caught it - pinned by
|
|
35
|
+
test_builtin_spawn_wrong_override_blocked.
|
|
36
|
+
00. provenance-shipping SHIPPED 2026-07-15 (PR #65, d624cc3, closes #64;
|
|
37
|
+
witnesses W-019/W-020, 20/20): fresh installs now ship
|
|
38
|
+
company/provenance.json (enforcer armed by default); update NEVER
|
|
39
|
+
auto-arms, prints one notice line. Owner field audit found the gap.
|
|
40
|
+
PENDING OWNER BUTTON: 0.2.1 patch release to npm (0.2.0 on the
|
|
41
|
+
registry still has the gap - fresh 0.2.0 installs stay dormant).
|
|
42
|
+
Same flow as 0.2.0: bump PR, tag, owner publishes from clean tag
|
|
43
|
+
clone (memory: npm-publish-owner-only). Field guidance given: owner's
|
|
44
|
+
research repo stays disarmed until they create provenance.json after
|
|
45
|
+
Jul 28; gitignore data/ to stop stop_gate churn from training jobs.
|
|
17
46
|
0. cli-update SHIPPED 2026-07-15: PR #57 merged (7726c99, closes #54-#56),
|
|
18
47
|
owner acceptance DECISIONS #6, witnesses W-014..W-016 recorded (16/16),
|
|
19
48
|
spec+brief archived to shipped/, worktree+branch removed, integrated main
|
|
@@ -29,14 +58,17 @@ witnesses/models/tests/audit. Owner acceptance recorded (DECISIONS #3).
|
|
|
29
58
|
0c. cli-self-update SHIPPED 2026-07-15: PR #60 merged (a09b463, closes
|
|
30
59
|
#59), acceptance DECISIONS #7, witnesses W-017..W-018 (18/18),
|
|
31
60
|
integrated main 213+57 green. Worktree+branch removed.
|
|
32
|
-
0d.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
0d. v0.2.0 PUBLISHED to npm 2026-07-15 (DECISIONS #8): tag v0.2.0 at
|
|
62
|
+
5913374, owner published manually from the clean tag clone. FIELD
|
|
63
|
+
LESSON (memory npm-publish-owner-only.md): owner npm 2FA is
|
|
64
|
+
LINK-BASED - no OTP codes exist; publishes are ALWAYS owner-manual or
|
|
65
|
+
git-CI, agent only prepares (bump PR, tag, clean tag clone, verified
|
|
66
|
+
placeholder gates.config). NEVER publish from the working checkout -
|
|
67
|
+
local gates.config wiring fails the placeholder test. Witnesses
|
|
68
|
+
W-017/18 landed via PR #62 (fab01e1). Candidate next: publish via CI
|
|
69
|
+
workflow (owner hinted "or through git ci"), #36, #37, tarball ships
|
|
70
|
+
repo board state (P3 hygiene, install never copies it), roadmap
|
|
71
|
+
#1-#11.
|
|
40
72
|
1. delegation-enforcement SHIPPED 2026-07-10 (PR #49, f9e5dae, closes
|
|
41
73
|
#42-#47; acceptance DECISIONS #4). Close-out PR in flight if session
|
|
42
74
|
died mid-close: witnesses W-011..W-013 recorded, brief archived to
|
package/company/state/STATUS.md
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
_Maintained by the CEO. Updated after every dispatch, merge, and CR decision._
|
|
4
4
|
_Red stays red until proven green. Never average a status._
|
|
5
5
|
|
|
6
|
-
_Last updated: 2026-07-
|
|
6
|
+
_Last updated: 2026-07-22 - model-routing-arming SHIPPED (PR #77 merged cd07fb6, acceptance DECISIONS #11); release 0.2.2 prepped._
|
|
7
7
|
|
|
8
8
|
## Active tasks
|
|
9
9
|
|
|
10
10
|
| Task | Class | Lead/Agent | State | Gates | Notes |
|
|
11
11
|
|---|---|---|---|---|---|
|
|
12
|
-
|
|
|
12
|
+
| spawn-depth-shipping | quick | developer | SHIPPED 2026-07-23 (PR #83 merged 6061814, closes #79-#82) | 224 hooks + 61 CLI + 123 engine green (dev AND CEO reruns); integrated main stamped | Template ships env CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=2; install/update merge env additively (user pin survives - proven); witness W-029. NOT yet on npm - registry still 0.2.0; owner has v0.2.2 tag+publish pending, and this lands in the NEXT release after that. |
|
|
13
|
+
| release-0.2.3 | release | CEO | BUMP PR IN FLIGHT | 224 hooks + 61 CLI + 123 engine green on main | Supersedes unpublished v0.2.2 (tag pushed, never published): 0.2.3 = 0.2.2 content + #83 spawn-depth. Owner buttons after merge: tag v0.2.3 at merge commit, publish from clean tag clone. Registry 0.2.0 -> 0.2.3 directly. DECISIONS #12-#14. |
|
|
14
|
+
|
|
15
|
+
## Shipped (recent)
|
|
16
|
+
|
|
17
|
+
| Task | Date | Evidence |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| model-routing-arming | 2026-07-22 | PR #77 merged cd07fb6 (closes #74-#76); acceptance DECISIONS #11; witnesses W-026/W-027/W-028; live certification: builtin contradict/bare spawn exit 2, match 0, dormancy probe turns --check red; migration: builtins merge lands on install AND update automatically. |
|
|
13
20
|
|
|
14
21
|
## Wave position (programs only)
|
|
15
22
|
|
|
@@ -18,7 +25,12 @@ _Last updated: 2026-07-15 - cli-update SHIPPED (PR #57 merged 7726c99, acceptanc
|
|
|
18
25
|
| adoption program 1-3 | all | SHIPPED 2026-07-10 | PRs 27/33/34/32/35 merged; acceptance DECISIONS #3 |
|
|
19
26
|
| follow-up pair | docs-sync + adr-hardening | SHIPPED 2026-07-10 | PRs 38/39/40/41 merged; #28 + #31 closed; witnesses 10/10 on main |
|
|
20
27
|
| cli-update | single workstream | SHIPPED 2026-07-15 | PR #57 merged (7726c99, closes #54-#56); integrated main 213+40+56 green; witnesses W-014..W-016; acceptance DECISIONS #6 |
|
|
21
|
-
| cli-self-update | single workstream | SHIPPED 2026-07-15 | PR #60 merged (a09b463, closes #59); integrated main 213+57 green; witnesses W-017..W-018; acceptance DECISIONS #7 |
|
|
28
|
+
| cli-self-update | single workstream | SHIPPED 2026-07-15 | PR #60 merged (a09b463, closes #59); integrated main 213+57 green; witnesses W-017..W-018 (landed via PR #62); acceptance DECISIONS #7 |
|
|
29
|
+
| v0.2.0 npm release | release | PUBLISHED 2026-07-15 | PR #61 merged, tag v0.2.0 (5913374), owner published manually (link-based 2FA); registry latest=0.2.0; DECISIONS #8 |
|
|
30
|
+
| provenance-shipping | quick fix | SHIPPED 2026-07-15 | PR #65 merged (d624cc3, closes #64); fresh installs arm the delegation enforcer, update never auto-arms; witnesses W-019/W-020; NOTE: 0.2.0 on npm still has the gap - 0.2.1 patch release recommended (owner button) |
|
|
31
|
+
| settings-merger-fix | quick fix | SHIPPED 2026-07-15 | PR #69 merged (f17c3c4, closes #67); dedup per (matcher, command) in both engines; update HEALS broken field installs (hand-proven); witnesses W-021/W-022. DevMesh hand-patched meanwhile (its c1ecbf7) |
|
|
32
|
+
| pack-leak-fix | quick fix | SHIPPED 2026-07-15 | PR #71 merged (d714892, closes #68); record trees scaffold empty + tarball negations; seeded negative test; witnesses W-023/W-024 |
|
|
33
|
+
| devmesh-migration | consulting | DELIVERED 2026-07-15 | DevMesh polyrepo migrated to claude-company (its commits b3f0a47/7e08247/c1ecbf7): docs/team ported to company/, frozen registry live (6 surfaces probe-verified), make-gates mirrored, custom agents/skills/memory preserved, 2 upstream bugs found (#67/#68 - both now fixed) |
|
|
22
34
|
|
|
23
35
|
## Open CRs
|
|
24
36
|
|
package/company/state/WORRIES.md
CHANGED
|
@@ -15,8 +15,12 @@ wave/merge; P3 is polish._
|
|
|
15
15
|
| P3 | git -C evades guard_commit subcmd parse | guard_commit.git_subcmd treats -C <dir> as the subcommand token, so `git -C x commit` bypasses commit gates (pre-existing, Mode C inherits) | Anti-adversary, out of doctrine scope; candidate issue against guard_commit |
|
|
16
16
|
| P3 | Mode D lacks worktree-cwd exemption | Stop close-gate block message reads wrong if a worktree session ever gets an active-task.json (moot today - file is untracked) | Auditor worry #3; per-spec (mirrors stop_gate); revisit only if worktree sessions gain task state |
|
|
17
17
|
| P2 | session_start 60-line cap can hide digest | Saturated RESUME+STATUS could truncate the active-task + execution digest lines | Lead worry P2, inherited behavior; candidate tail-reservation tweak in session_start |
|
|
18
|
+
| P2 | provenance audit verdict parser is substring-naive | guard_provenance.py:487 records do-not-ship if the literal phrase appears ANYWHERE in the audit payload - a dispatch prompt that names the verdict vocabulary poisons its own record (hit 3x on release-0.2.2) | Candidate fix: parse the verdict from the response's verdict line only, or match anchored '## Verdict:'; file issue next machinery pass; interim rule: never write the phrase in an auditor prompt |
|
|
19
|
+
| P2 | guard_tests resolves test_scope from main checkout | Worktree builders cannot commit brief-granted test edits until the CEO sets test_scope in MAIN's active-task.json - has now blocked two workstreams (cli-update, model-routing-arming) | Candidate fix: resolve test_scope from the target worktree, or dispatch protocol sets it whenever a brief grants test dirs; follow-up brief |
|
|
20
|
+
| P3 | models.json merge is now a THIRD duplicated heredoc | install.sh/update.sh byte-identical coupling grew (settings, CLAUDE block, now models.json builtins) - divergence risk compounds | Same root cause as existing heredoc row; the byte-identity witness candidate (lead proposal 3) mitigates; fold into any future extraction task |
|
|
18
21
|
| P2 | update.sh duplicates install.sh merge heredocs | The three merges (settings/mcp/CLAUDE block) + CC_BLOCK are copied verbatim; a future install.sh merge change silently diverges update | cli-update lead worry; candidate fixture test asserting install and update produce identical merges; extraction forbidden this pass by additive-only brief |
|
|
19
22
|
| P3 | update spawns python3 per file (~3x/file) | ~180 spawns for 61 files - seconds today, slow at scale | Candidate manifest.py hashtree batch subcommand; not blocking |
|
|
20
23
|
| P3 | manifest emission fails open at install | lib/ or package.json missing -> no manifest, update runs bootstrap-safe forever (never clean "updated") | By design (safe); revisit only if field reports show chronic manifest-less installs |
|
|
21
24
|
| P3 | extensionless files count as source | guard_spec.is_source flags any extensionless file outside company/.claude/docs/.github (e.g. Makefile, a bare "report") - a non-code agent writing one hits the execution gate | Found in owner-requested gate probe 2026-07-15; md/json/state are proven exempt; candidate: extensionless allowlist if it ever bites |
|
|
22
25
|
| P3 | guard_tests gates md inside tests/ | Any write under a tests/ segment is blocked without test_scope regardless of extension, incl. tests/**/*.md (docs-librarian MODULE.md case) | Defensible (tests are the oracle) but extension-blind; candidate: exempt NON_SOURCE_EXT under tests/ if doc syncs start colliding |
|
|
26
|
+
| P3 | background writers stale the gate stamp | Field report 2026-07-15: a training job writing untracked non-ignored files (data/) stales work_hash every turn - stop_gate demands gate reruns | By design (.gitignore IS the ignore mechanism); candidate one-line docs FAQ: gitignore background-process output dirs |
|
package/company/witnesses.json
CHANGED
|
@@ -145,7 +145,106 @@
|
|
|
145
145
|
"regex": false,
|
|
146
146
|
"why": "Manifest byte-determinism contract: sorted keys, no clock field - the bin-vs-bare parity test depends on it",
|
|
147
147
|
"added_at": "2026-07-14T22:13:07Z"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "W-017",
|
|
151
|
+
"task": "cli-self-update",
|
|
152
|
+
"file": "lib/update.js",
|
|
153
|
+
"must_contain": "if (process.env.CC_SELFUPDATE_DONE) return null;",
|
|
154
|
+
"regex": false,
|
|
155
|
+
"why": "Handoff-exactly-once loop guard: the re-exec child never re-checks the registry",
|
|
156
|
+
"added_at": "2026-07-14T23:25:27Z"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"id": "W-018",
|
|
160
|
+
"task": "cli-self-update",
|
|
161
|
+
"file": "lib/update.js",
|
|
162
|
+
"must_contain": "if (versionCompare(latest, PKG.version) <= 0) {",
|
|
163
|
+
"regex": false,
|
|
164
|
+
"why": "Strictly-newer is the only re-exec trigger; equal or older proceeds silently with the current CLI",
|
|
165
|
+
"added_at": "2026-07-14T23:25:27Z"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"id": "W-019",
|
|
169
|
+
"task": "provenance-shipping",
|
|
170
|
+
"file": "install.sh",
|
|
171
|
+
"must_contain": "copy_if_absent \"$SRC/company/provenance.json\" \"$TARGET/company/provenance.json\"",
|
|
172
|
+
"regex": false,
|
|
173
|
+
"why": "Fresh installs ship the arming manifest - the delegation enforcer is armed by default in the field",
|
|
174
|
+
"added_at": "2026-07-15T01:01:51Z"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"id": "W-020",
|
|
178
|
+
"task": "provenance-shipping",
|
|
179
|
+
"file": "update.sh",
|
|
180
|
+
"must_contain": "note: delegation enforcer installed but disarmed - create company/provenance.json to arm",
|
|
181
|
+
"regex": false,
|
|
182
|
+
"why": "update never auto-arms - a payload refresh must not switch a project's enforcement regime",
|
|
183
|
+
"added_at": "2026-07-15T01:01:51Z"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "W-021",
|
|
187
|
+
"task": "settings-merger-fix",
|
|
188
|
+
"file": "install.sh",
|
|
189
|
+
"must_contain": "have = commands_by_matcher(existing_groups) # issue-67: dedup per matcher",
|
|
190
|
+
"regex": false,
|
|
191
|
+
"why": "Settings merge dedups per (matcher, command) - reverting to command-per-event re-drops multi-matcher hook groups",
|
|
192
|
+
"added_at": "2026-07-15T10:34:04Z"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"id": "W-022",
|
|
196
|
+
"task": "settings-merger-fix",
|
|
197
|
+
"file": "update.sh",
|
|
198
|
+
"must_contain": "have = commands_by_matcher(existing_groups) # issue-67: dedup per matcher",
|
|
199
|
+
"regex": false,
|
|
200
|
+
"why": "update.sh merge block stays byte-identical to install.sh - the heal path for broken field installs depends on it",
|
|
201
|
+
"added_at": "2026-07-15T10:34:04Z"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "W-023",
|
|
205
|
+
"task": "pack-leak-fix",
|
|
206
|
+
"file": "install.sh",
|
|
207
|
+
"must_contain": "\"$TARGET/company/change-requests\" # issue-68",
|
|
208
|
+
"regex": false,
|
|
209
|
+
"why": "Record trees are scaffolded empty, never copied - reverting to copy_tree_if_absent re-leaks repo records into git-clone installs",
|
|
210
|
+
"added_at": "2026-07-15T11:14:01Z"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "W-024",
|
|
214
|
+
"task": "pack-leak-fix",
|
|
215
|
+
"file": "package.json",
|
|
216
|
+
"must_contain": "\"!company/briefs/**\",",
|
|
217
|
+
"regex": false,
|
|
218
|
+
"why": "Tarball belt - dropping the files-negations re-ships repo records via npm",
|
|
219
|
+
"added_at": "2026-07-15T11:14:01Z"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "W-026",
|
|
223
|
+
"task": "model-routing-arming",
|
|
224
|
+
"file": ".claude/hooks/guard_models.py",
|
|
225
|
+
"must_contain": "def check_spawn_wiring",
|
|
226
|
+
"regex": false,
|
|
227
|
+
"why": "run_check wiring assertion - regression means the models gate reads green while Task|Agent enforcement is unwired (code-without-wiring)",
|
|
228
|
+
"added_at": "2026-07-22T15:23:53Z"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"id": "W-027",
|
|
232
|
+
"task": "model-routing-arming",
|
|
233
|
+
"file": "update.sh",
|
|
234
|
+
"must_contain": "BYTE-IDENTICAL to the",
|
|
235
|
+
"regex": false,
|
|
236
|
+
"why": "models.json merge heredoc coupling marker - install and update must migrate identically",
|
|
237
|
+
"added_at": "2026-07-22T15:23:53Z"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "W-028",
|
|
241
|
+
"task": "model-routing-arming",
|
|
242
|
+
"file": ".claude/hooks/guard_models.py",
|
|
243
|
+
"must_contain": "inherit the session model",
|
|
244
|
+
"regex": false,
|
|
245
|
+
"why": "Bare builtin-spawn BLOCK path - regression means built-in spawns silently inherit the session model (the live incident)",
|
|
246
|
+
"added_at": "2026-07-22T15:24:05Z"
|
|
148
247
|
}
|
|
149
248
|
],
|
|
150
|
-
"checksum": "
|
|
249
|
+
"checksum": "5789e76d6242c45d3e82f0ca3da8895c3e19753e2b88100fe8a44e6f38b6615f"
|
|
151
250
|
}
|
package/docs/customizing.md
CHANGED
|
@@ -80,6 +80,8 @@ Each role is one markdown file in `.claude/agents/`. The file's frontmatter sets
|
|
|
80
80
|
|
|
81
81
|
The same file carries a `pricing` map (USD per million tokens) that `cost_capture.py` reads to estimate spend in `company/state/costs.log` and the `/standup` Spend line. Update it when your rates differ; leave a model out of it and the standup reports raw token counts for that model instead of dollars. The numbers are estimates for visibility, never a bill.
|
|
82
82
|
|
|
83
|
+
- **Arm or disarm the delegation enforcer**: `company/provenance.json` is the switch. A fresh install ships it, so the enforcer is armed by default; delete it to disarm every provenance mode (same rollout posture as `company/models.json`). An `update` never creates it - if a project lacks the file, update prints a one-line notice and leaves the enforcement regime unchanged, so refreshing the payload never silently arms a project.
|
|
84
|
+
|
|
83
85
|
Two roles are load-bearing; change them carefully. `tech-lead` is the only agent allowed to spawn other agents, and `developer` carries the working rules every builder inherits.
|
|
84
86
|
|
|
85
87
|
## Adjust the founding defaults
|
package/install.sh
CHANGED
|
@@ -123,12 +123,56 @@ info "Installing project config and state (existing files preserved)"
|
|
|
123
123
|
copy_if_absent "$SRC/company/gates.config" "$TARGET/company/gates.config"
|
|
124
124
|
copy_if_absent "$SRC/company/frozen-surfaces.json" "$TARGET/company/frozen-surfaces.json"
|
|
125
125
|
copy_if_absent "$SRC/company/models.json" "$TARGET/company/models.json"
|
|
126
|
+
# FR-MRA-09: arm model routing on a PRE-EXISTING models.json that predates the
|
|
127
|
+
# builtins section by injecting the packaged template's builtins in place. On a
|
|
128
|
+
# fresh install the target was just copied from the template (already carries
|
|
129
|
+
# builtins), so the python no-ops and the file stays byte-identical to it.
|
|
130
|
+
# COUPLING: the heredoc between <<'PY' and PY below is BYTE-IDENTICAL to the
|
|
131
|
+
# models.json builtins-injection block in update.sh. Keep them identical; both
|
|
132
|
+
# must merge the same way.
|
|
133
|
+
MODELS_SRC="$SRC/company/models.json"
|
|
134
|
+
MODELS_DST="$TARGET/company/models.json"
|
|
135
|
+
if [ -f "$MODELS_SRC" ] && [ -f "$MODELS_DST" ]; then
|
|
136
|
+
python3 - "$MODELS_SRC" "$MODELS_DST" <<'PY'
|
|
137
|
+
import json, sys
|
|
126
138
|
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
# OQ-MRA-01 assumption: additive builtins injection by canonical
|
|
140
|
+
# json.load/json.dump re-serialization on the one injecting run. Inject the
|
|
141
|
+
# packaged template's `builtins` only when the target manifest lacks it,
|
|
142
|
+
# preserving the VALUES of roles/pricing/version and any user keys. When
|
|
143
|
+
# `builtins` is already present, emit the target bytes verbatim (no write) so
|
|
144
|
+
# the file stays byte-unchanged.
|
|
145
|
+
src_path, dst_path = sys.argv[1], sys.argv[2]
|
|
146
|
+
with open(src_path) as f:
|
|
147
|
+
src = json.load(f)
|
|
148
|
+
try:
|
|
149
|
+
with open(dst_path) as f:
|
|
150
|
+
tgt = json.load(f)
|
|
151
|
+
except (FileNotFoundError, ValueError):
|
|
152
|
+
sys.exit(0) # nothing to merge into - config-if-absent restores separately
|
|
153
|
+
if not isinstance(tgt, dict) or "builtins" in tgt:
|
|
154
|
+
sys.exit(0) # already armed (or unmergeable) - leave the target untouched
|
|
155
|
+
src_builtins = src.get("builtins")
|
|
156
|
+
if not isinstance(src_builtins, dict):
|
|
157
|
+
sys.exit(0) # template carries no builtins - nothing to inject
|
|
158
|
+
tgt["builtins"] = src_builtins
|
|
159
|
+
with open(dst_path, "w") as f:
|
|
160
|
+
json.dump(tgt, f, indent=2, sort_keys=False)
|
|
161
|
+
f.write("\n")
|
|
162
|
+
PY
|
|
163
|
+
fi
|
|
164
|
+
# issue-64: ship provenance.json copy_if_absent so a fresh install arms the
|
|
165
|
+
# delegation enforcer by default (present is never touched, not in the manifest).
|
|
166
|
+
copy_if_absent "$SRC/company/provenance.json" "$TARGET/company/provenance.json"
|
|
167
|
+
|
|
168
|
+
# work directories - scaffold as EMPTY dirs only. issue-68: these trees are
|
|
169
|
+
# where a target project writes its OWN specs, briefs, and change requests; we
|
|
170
|
+
# never copy this package's records (spec-*.md, brief-*.md, CR-*.md, shipped/**)
|
|
171
|
+
# into a target. Copying them leaked our work records into every install.
|
|
172
|
+
mkdir -p \
|
|
173
|
+
"$TARGET/company/specs" "$TARGET/company/specs/shipped" \
|
|
174
|
+
"$TARGET/company/briefs" "$TARGET/company/briefs/shipped" \
|
|
175
|
+
"$TARGET/company/change-requests" # issue-68
|
|
132
176
|
|
|
133
177
|
# --- 4. scaffold state stubs (only if absent) -----------------------------
|
|
134
178
|
info "Scaffolding company/state"
|
|
@@ -178,14 +222,22 @@ except (FileNotFoundError, ValueError):
|
|
|
178
222
|
theirs = {}
|
|
179
223
|
|
|
180
224
|
# --- merge hooks: append our command entries unless already present -------
|
|
181
|
-
|
|
182
|
-
|
|
225
|
+
# issue-67: dedup key is (matcher, command), not command-per-event. A command
|
|
226
|
+
# may legitimately appear under several matcher groups of one event
|
|
227
|
+
# (guard_provenance under Edit|Write|MultiEdit AND Task|Agent AND Bash); a
|
|
228
|
+
# per-event set dropped every repeat after the first, emptying and then losing
|
|
229
|
+
# whole groups. Keying by matcher keeps each group complete.
|
|
230
|
+
_NO_MATCHER = object() # sentinel key for groups that carry no matcher
|
|
231
|
+
|
|
232
|
+
def commands_by_matcher(groups):
|
|
233
|
+
seen = {}
|
|
183
234
|
for g in groups or []:
|
|
235
|
+
bucket = seen.setdefault(g.get("matcher", _NO_MATCHER), set())
|
|
184
236
|
for h in (g.get("hooks") or []):
|
|
185
237
|
c = h.get("command")
|
|
186
238
|
if c is not None:
|
|
187
|
-
|
|
188
|
-
return
|
|
239
|
+
bucket.add(c)
|
|
240
|
+
return seen
|
|
189
241
|
|
|
190
242
|
our_hooks = ours.get("hooks") or {}
|
|
191
243
|
their_hooks = theirs.setdefault("hooks", {}) if isinstance(theirs.get("hooks", {}), dict) else {}
|
|
@@ -198,16 +250,17 @@ for event, our_groups in our_hooks.items():
|
|
|
198
250
|
if not isinstance(existing_groups, list):
|
|
199
251
|
existing_groups = []
|
|
200
252
|
their_hooks[event] = existing_groups
|
|
201
|
-
have =
|
|
253
|
+
have = commands_by_matcher(existing_groups) # issue-67: dedup per matcher
|
|
202
254
|
for g in (our_groups or []):
|
|
255
|
+
bucket = have.setdefault(g.get("matcher", _NO_MATCHER), set())
|
|
203
256
|
new_hooks = []
|
|
204
257
|
for h in (g.get("hooks") or []):
|
|
205
258
|
c = h.get("command")
|
|
206
|
-
if c is not None and c in
|
|
207
|
-
continue # identical command already present
|
|
259
|
+
if c is not None and c in bucket:
|
|
260
|
+
continue # identical command already present under this matcher
|
|
208
261
|
new_hooks.append(h)
|
|
209
262
|
if c is not None:
|
|
210
|
-
|
|
263
|
+
bucket.add(c)
|
|
211
264
|
if new_hooks:
|
|
212
265
|
ng = dict(g)
|
|
213
266
|
ng["hooks"] = new_hooks
|
|
@@ -229,6 +282,23 @@ if our_deny:
|
|
|
229
282
|
if entry not in their_deny:
|
|
230
283
|
their_deny.append(entry)
|
|
231
284
|
|
|
285
|
+
# --- merge env: add our keys the user lacks, never overwrite theirs --------
|
|
286
|
+
# spawn-depth fix: ship CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=2 so a tech-lead
|
|
287
|
+
# can still spawn its developers/QA (Claude Code 2.1.21 defaulted subagent
|
|
288
|
+
# spawn depth to 1, flattening the org). Additive-only, same spirit as
|
|
289
|
+
# permissions.deny above: a user-pinned value (e.g. depth "3") is NEVER
|
|
290
|
+
# overwritten. A theirs.env that is not a dict is replaced-safe (treated as
|
|
291
|
+
# empty, then ours is set) so the merger never bricks an install.
|
|
292
|
+
our_env = ours.get("env") or {}
|
|
293
|
+
if our_env:
|
|
294
|
+
their_env = theirs.get("env")
|
|
295
|
+
if not isinstance(their_env, dict):
|
|
296
|
+
their_env = {}
|
|
297
|
+
theirs["env"] = their_env
|
|
298
|
+
for key, value in our_env.items():
|
|
299
|
+
if key not in their_env:
|
|
300
|
+
their_env[key] = value
|
|
301
|
+
|
|
232
302
|
with open(dst_path, "w") as f:
|
|
233
303
|
json.dump(theirs, f, indent=2, sort_keys=False)
|
|
234
304
|
f.write("\n")
|