claude-dev-env 2.0.0 → 2.0.1

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.
@@ -69,6 +69,36 @@ def _write_sidecar(agent_transcript_file: pathlib.Path, agent_type: str) -> None
69
69
  )
70
70
 
71
71
 
72
+ def _write_verdict_transcript(agent_transcript_file: pathlib.Path, verdict_body: str) -> None:
73
+ verdict_text = f"ok\n```verdict\n{verdict_body}\n```\n"
74
+ agent_transcript_file.write_text(
75
+ json.dumps(
76
+ {
77
+ "type": "assistant",
78
+ "message": {"content": [{"type": "text", "text": verdict_text}]},
79
+ }
80
+ )
81
+ + "\n",
82
+ encoding="utf-8",
83
+ )
84
+
85
+
86
+ def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
87
+ repo_root = tmp_path / "repo"
88
+ repo_root.mkdir()
89
+ return repo_root
90
+
91
+
92
+ def _mint_payload(
93
+ agent_transcript_file: pathlib.Path, repo_root: pathlib.Path, agent_id: str
94
+ ) -> dict[str, str]:
95
+ return {
96
+ "agent_transcript_path": str(agent_transcript_file),
97
+ "cwd": str(repo_root),
98
+ "agent_id": agent_id,
99
+ }
100
+
101
+
72
102
  def test_resolves_subagent_type_from_sidecar(tmp_path: pathlib.Path) -> None:
73
103
  agent_transcript = tmp_path / "agent-7.jsonl"
74
104
  agent_transcript.write_text("", encoding="utf-8")
@@ -141,7 +171,7 @@ def test_verifier_type_without_a_verdict_mints_nothing(tmp_path: pathlib.Path) -
141
171
  assert mint_for_payload(payload) is None
142
172
 
143
173
 
144
- def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
174
+ def _init_repo_with_upstream(repo_root: pathlib.Path) -> None:
145
175
  subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
146
176
  subprocess.run(
147
177
  ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"], check=True
@@ -151,37 +181,20 @@ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
151
181
  subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
152
182
  subprocess.run(["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True)
153
183
  subprocess.run(["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"], check=True)
184
+
185
+
186
+ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
187
+ _init_repo_with_upstream(repo_root)
154
188
  (repo_root / "module.py").write_text("answer = 2\n", encoding="utf-8")
155
189
 
156
190
 
157
191
  def test_clean_verifier_verdict_mints_a_verdict_file(tmp_path: pathlib.Path) -> None:
158
- repo_root = tmp_path / "repo"
159
- repo_root.mkdir()
192
+ repo_root = _make_repo(tmp_path)
160
193
  _init_repo_with_upstream_and_edit(repo_root)
161
194
  agent_transcript = tmp_path / "agent-7.jsonl"
162
- agent_transcript.write_text(
163
- json.dumps(
164
- {
165
- "type": "assistant",
166
- "message": {
167
- "content": [
168
- {
169
- "type": "text",
170
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
171
- }
172
- ]
173
- },
174
- }
175
- )
176
- + "\n",
177
- encoding="utf-8",
178
- )
195
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
179
196
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
180
- payload = {
181
- "agent_transcript_path": str(agent_transcript),
182
- "cwd": str(repo_root),
183
- "agent_id": "a02b9583eedc74093",
184
- }
197
+ payload = _mint_payload(agent_transcript, repo_root, "a02b9583eedc74093")
185
198
  verdict_path = mint_for_payload(payload)
186
199
  try:
187
200
  assert verdict_path is not None
@@ -199,7 +212,8 @@ def _deny_rules() -> list[str]:
199
212
 
200
213
 
201
214
  def test_settings_deny_verdict_directory_write() -> None:
202
- assert "Write($HOME/.claude/verification/**)" in _deny_rules()
215
+ assert "Edit($HOME/.claude/verification/**)" in _deny_rules()
216
+ assert "Write($HOME/.claude/verification/**)" not in _deny_rules()
203
217
 
204
218
 
205
219
  def test_settings_deny_verdict_directory_edit() -> None:
@@ -209,87 +223,28 @@ def test_settings_deny_verdict_directory_edit() -> None:
209
223
  def test_minter_refuses_when_attested_hash_equals_empty_surface_hash(
210
224
  tmp_path: pathlib.Path,
211
225
  ) -> None:
212
- repo_root = tmp_path / "repo"
213
- repo_root.mkdir()
226
+ repo_root = _make_repo(tmp_path)
214
227
  _init_repo_with_upstream_and_edit(repo_root)
215
228
  attested_empty = empty_surface_hash()
216
229
  verdict_fence = json.dumps(
217
230
  {"all_pass": True, "findings": [], "manifest_sha256": attested_empty}
218
231
  )
219
232
  agent_transcript = tmp_path / "agent-7.jsonl"
220
- agent_transcript.write_text(
221
- json.dumps(
222
- {
223
- "type": "assistant",
224
- "message": {
225
- "content": [
226
- {
227
- "type": "text",
228
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
229
- }
230
- ]
231
- },
232
- }
233
- )
234
- + "\n",
235
- encoding="utf-8",
236
- )
233
+ _write_verdict_transcript(agent_transcript, verdict_fence)
237
234
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
238
- payload = {
239
- "agent_transcript_path": str(agent_transcript),
240
- "cwd": str(repo_root),
241
- "agent_id": "empty-surface-1",
242
- }
235
+ payload = _mint_payload(agent_transcript, repo_root, "empty-surface-1")
243
236
  assert mint_for_payload(payload) is None
244
237
 
245
238
 
246
239
  def test_minter_refuses_when_recomputed_surface_is_empty(
247
240
  tmp_path: pathlib.Path,
248
241
  ) -> None:
249
- repo_root = tmp_path / "repo"
250
- repo_root.mkdir()
251
- subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
252
- subprocess.run(
253
- ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"],
254
- check=True,
255
- )
256
- subprocess.run(
257
- ["git", "-C", str(repo_root), "config", "user.name", "verifier"],
258
- check=True,
259
- )
260
- (repo_root / "module.py").write_text("answer = 1\n", encoding="utf-8")
261
- subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
262
- subprocess.run(
263
- ["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True
264
- )
265
- subprocess.run(
266
- ["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"],
267
- check=True,
268
- )
242
+ repo_root = _make_repo(tmp_path)
243
+ _init_repo_with_upstream(repo_root)
269
244
  agent_transcript = tmp_path / "agent-7.jsonl"
270
- agent_transcript.write_text(
271
- json.dumps(
272
- {
273
- "type": "assistant",
274
- "message": {
275
- "content": [
276
- {
277
- "type": "text",
278
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
279
- }
280
- ]
281
- },
282
- }
283
- )
284
- + "\n",
285
- encoding="utf-8",
286
- )
245
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
287
246
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
288
- payload = {
289
- "agent_transcript_path": str(agent_transcript),
290
- "cwd": str(repo_root),
291
- "agent_id": "empty-recompute-1",
292
- }
247
+ payload = _mint_payload(agent_transcript, repo_root, "empty-recompute-1")
293
248
  assert mint_for_payload(payload) is None
294
249
 
295
250
 
@@ -297,23 +252,7 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
297
252
  tmp_path: pathlib.Path,
298
253
  ) -> None:
299
254
  agent_transcript = tmp_path / "agent-7.jsonl"
300
- agent_transcript.write_text(
301
- json.dumps(
302
- {
303
- "type": "assistant",
304
- "message": {
305
- "content": [
306
- {
307
- "type": "text",
308
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
309
- }
310
- ]
311
- },
312
- }
313
- )
314
- + "\n",
315
- encoding="utf-8",
316
- )
255
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
317
256
  payload = {"agent_transcript_path": str(agent_transcript)}
318
257
  assert resolved_subagent_type(payload) is None
319
258
 
@@ -321,67 +260,25 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
321
260
  def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
322
261
  tmp_path: pathlib.Path,
323
262
  ) -> None:
324
- repo_root = tmp_path / "repo"
325
- repo_root.mkdir()
263
+ repo_root = _make_repo(tmp_path)
326
264
  _init_repo_with_upstream_and_edit(repo_root)
327
265
  agent_transcript = tmp_path / "agent-7.jsonl"
328
- agent_transcript.write_text(
329
- json.dumps(
330
- {
331
- "type": "assistant",
332
- "message": {
333
- "content": [
334
- {
335
- "type": "text",
336
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
337
- }
338
- ]
339
- },
340
- }
341
- )
342
- + "\n",
343
- encoding="utf-8",
344
- )
345
- payload = {
346
- "agent_transcript_path": str(agent_transcript),
347
- "cwd": str(repo_root),
348
- "agent_id": "no-sidecar-1",
349
- }
266
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
267
+ payload = _mint_payload(agent_transcript, repo_root, "no-sidecar-1")
350
268
  assert mint_for_payload(payload) is None
351
269
 
352
270
 
353
271
  def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
354
- repo_root = tmp_path / "repo"
355
- repo_root.mkdir()
272
+ repo_root = _make_repo(tmp_path)
356
273
  _init_repo_with_upstream_and_edit(repo_root)
357
274
  attested_hash = "c" * 64
358
- agent_transcript = tmp_path / "agent-7.jsonl"
359
275
  verdict_fence = json.dumps(
360
276
  {"all_pass": True, "findings": [], "manifest_sha256": attested_hash}
361
277
  )
362
- agent_transcript.write_text(
363
- json.dumps(
364
- {
365
- "type": "assistant",
366
- "message": {
367
- "content": [
368
- {
369
- "type": "text",
370
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
371
- }
372
- ]
373
- },
374
- }
375
- )
376
- + "\n",
377
- encoding="utf-8",
378
- )
278
+ agent_transcript = tmp_path / "agent-7.jsonl"
279
+ _write_verdict_transcript(agent_transcript, verdict_fence)
379
280
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
380
- payload = {
381
- "agent_transcript_path": str(agent_transcript),
382
- "cwd": str(repo_root),
383
- "agent_id": "attest-1",
384
- }
281
+ payload = _mint_payload(agent_transcript, repo_root, "attest-1")
385
282
  verdict_path = mint_for_payload(payload)
386
283
  try:
387
284
  assert verdict_path is not None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,12 +21,8 @@ delegating to Sonnet-5 workers came out cheaper and faster
21
21
  than a solo frontier agent held to the same verification rigor, with
22
22
  84-98% of the team's input tokens billed at the worker rate.
23
23
 
24
- Claude Code has no `multiagent` coordinator field or Managed-Agents-style
25
- `create_agent`/`send_to_agent` primitives; this skill reaches the same
26
- shape with the tools Claude Code already has. Under this skill the session
27
- is the orchestrator. In Claude Code the user always talks to the session and never to a
28
- subagent, so the session is the user's sole interface: all user-facing
29
- communication flows through it. It spawns and resumes executor subagents
24
+ Under this skill the session
25
+ is the orchestrator. It spawns and resumes executor subagents
30
26
  — `clean-coder` and the like — and those executors do every bit of the
31
27
  execution: the code edits, the build runs, the test runs. The orchestrating
32
28
  session drives the plan and routes hard decisions to the shared advisor
@@ -64,16 +60,11 @@ session drives the plan and routes hard decisions to the shared advisor
64
60
  shared advisor bind (Claude warm `session-advisor`, or a third-party host's
65
61
  Claude CLI bind — run step 3 only when none exists yet), then skip straight to step 4.
66
62
 
67
- 2. **Register the discipline reminder.** By default, schedule it with
68
- `ScheduleWakeup` at `delaySeconds: 1200`, prompt `/orchestrator-refresh`,
69
- where each refresh re-schedules the next one — a 1200-second wakeup costs
70
- one prompt-cache miss per firing and nothing more (see Gotchas). The loop
71
- mechanism (`/loop 20m /orchestrator-refresh`) is the escape hatch when
72
- `ScheduleWakeup` is not available. Either way the reminder is the
73
- enforcement surface: each firing re-asserts the discipline while the run is
74
- in flight.
63
+ 2. **Register the discipline reminder.** schedule it with
64
+ `ScheduleWakeup` at `delaySeconds: 1800`, prompt `/orchestrator-refresh`,
65
+ where each refresh re-schedules the next one.
75
66
 
76
- 3. **Bind the shared advisor before any executor.** Detect the host profile
67
+ 3. **Bind a shared advisor before any executor.** Detect the host profile
77
68
  first (see Host profiles in
78
69
  [`_shared/advisor/advisor-protocol.md`](../../_shared/advisor/advisor-protocol.md)).
79
70
  On a **Claude host**, follow the Warm-up procedure in that doc (Agent spawn
@@ -160,7 +151,7 @@ Routing rules:
160
151
 
161
152
  ## Agent reuse (non-negotiable)
162
153
 
163
- - **Resume before you spawn, always.** A warm agent carries its context and
154
+ - **Resume before you spawn, always.** A warm agent (active within the past 59 minutes) carries its context and
164
155
  cached tokens; a fresh spawn starts cold and pays to rebuild both.
165
156
  Resume the existing workflow agent by name or `agentId` when it holds
166
157
  relevant context. Prefer that path every time an existing workflow agent
@@ -170,6 +161,24 @@ Routing rules:
170
161
  - **Name the agent to resume.** When a PLAN from the shared advisor fits a warm
171
162
  agent, name which agent to resume and where.
172
163
 
164
+ ## Task ledger discipline
165
+
166
+ The task list is the run's ledger, and it must be reconcilable against the live
167
+ agents at any moment. Four invariants hold at all times:
168
+
169
+ 1. **No untracked work.** Every unit of delegated work has a task BEFORE its
170
+ executor spawns — TaskCreate first, then Agent.
171
+ 2. **Ownership is live.** At spawn, set the task `in_progress` with `owner` =
172
+ the executor's agent name. One task, one owner.
173
+ 3. **Completion follows evidence.** A task turns `completed` only when the
174
+ executor's result is back AND merged into run state — never on dispatch,
175
+ never on a self-report alone (see `workers-done-before-complete`).
176
+ 4. **Dependencies mirror the plan.** Phase order is encoded as `blockedBy`
177
+ links, updated the moment the plan changes.
178
+
179
+ Reconcile on every state change (spawn, completion notification, plan change)
180
+ and on every `/orchestrator-refresh` firing.
181
+
173
182
  ## Constraints
174
183
 
175
184
  - One `/orchestrator` per session; the invocation guard blocks a second
@@ -15,10 +15,18 @@ Detect the host profile first (see Host profiles in
15
15
  Re-assert the discipline for that host only — do not invent an Agent-tool
16
16
  Claude `session-advisor` spawn on a third-party host.
17
17
 
18
- 1. **You are the orchestrator.** Orchestrate and hold the user conversation;
18
+ 1. **Reconcile the task ledger first.** Call `TaskList` before anything else
19
+ this firing. The ledger is stale when any of these holds: a running or
20
+ finished executor has no `in_progress` task naming it as owner; a finished
21
+ executor's task is still open (or was closed without its result merged);
22
+ the next phase you will dispatch has no pending task; a `blockedBy` link
23
+ contradicts the actual run order. Fix every mismatch with TaskCreate /
24
+ TaskUpdate in this same firing — never defer reconciliation to "when the
25
+ agent reports".
26
+ 2. **You are the orchestrator.** Orchestrate and hold the user conversation;
19
27
  spawn executor subagents to do all the work — every code edit and build or
20
28
  test run.
21
- 2. **Hard decisions go to the shared advisor.**
29
+ 3. **Hard decisions go to the shared advisor.**
22
30
  - **Claude host:** executors consult the warm `session-advisor` via
23
31
  `SendMessage` and receive one of four signals — ENDORSE, CORRECTION, PLAN,
24
32
  or STOP. The orchestrating session routes its own hard decisions the same
@@ -30,11 +38,11 @@ Claude `session-advisor` spawn on a third-party host.
30
38
  session; consult the Claude CLI advisor and relay ENDORSE / CORRECTION /
31
39
  PLAN / STOP. When the CLI bind is unreachable, fail closed and report to
32
40
  the user — do not answer the four signals as this third-party session.
33
- 3. **Resume before you spawn.** `SendMessage` an existing *executor* agent by
41
+ 4. **Resume before you spawn.** `SendMessage` an existing *executor* agent by
34
42
  name or `agentId` to reuse its warm context; prefer that over a cold spawn.
35
43
  (On a third-party host this is executor reuse only — advisor re-bind stays on the CLI
36
44
  chain path in the shared protocol.)
37
- 4. **Fresh spawn only for a genuine task switch.** No tool compacts or clears a
45
+ 5. **Fresh spawn only for a genuine task switch.** No tool compacts or clears a
38
46
  subagent's context, so a clean context comes from a fresh spawn — never tell
39
47
  an agent to compact.
40
48
  5. **Re-schedule the next refresh** (about 1200 seconds out) when the loop