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
|
|
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
|
|
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
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
250
|
-
repo_root
|
|
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
|
|
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
|
|
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
|
|
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
|
|
329
|
-
|
|
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
|
|
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.
|
|
363
|
-
|
|
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
|
@@ -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
|
-
|
|
25
|
-
|
|
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.**
|
|
68
|
-
`ScheduleWakeup` at `delaySeconds:
|
|
69
|
-
where each refresh re-schedules the next one
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|