claude-dev-env 1.67.0 → 1.67.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.67.0",
3
+ "version": "1.67.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,12 @@ description: Workflow-backed implementation planning that creates a deep repo-lo
7
7
 
8
8
  Create a source-grounded plan packet through the Claude Code Workflow runtime. The output is a repo-local `docs/plans/<slug>/` folder with context, spec, implementation, validation, and handoff docs. Stop before implementation.
9
9
 
10
+ ## Isolate first
11
+
12
+ The workflow's background subagents write the packet into the working tree. A background session that has not isolated into a worktree cannot write a shared checkout — the background-isolation guard rejects the write. So put the session in a worktree before launching the workflow:
13
+
14
+ If the working directory is not already inside a worktree — its path does not contain `.claude/worktrees/` — call `EnterWorktree` to create one. The session switches into the worktree, the packet is written under `docs/plans/<slug>/` there, and the build then proceeds in the same worktree. A session already inside a worktree skips this step.
15
+
10
16
  ## Launch
11
17
 
12
18
  Call the workflow with the user request and current working directory. The payload goes in `args` — the Workflow tool exposes `args` to the script as its global `args`, and substitutes the user's full request for `$ARGUMENTS`:
@@ -435,7 +435,7 @@ def test_reuse_audit_without_verdict_keyword_fails(tmp_path: Path) -> None:
435
435
  validator_run = run_validator(packet_directory)
436
436
 
437
437
  assert validator_run.returncode == 2
438
- assert "reuse-audit.md must record a reuse verdict for each new item" in validator_run.stderr
438
+ assert "reuse-audit.md must name a justifying reuse verdict" in validator_run.stderr
439
439
 
440
440
 
441
441
  def test_reuse_audit_with_verdict_keyword_passes(tmp_path: Path) -> None:
@@ -453,3 +453,21 @@ def test_reuse_audit_with_verdict_keyword_passes(tmp_path: Path) -> None:
453
453
  validator_run = run_validator(packet_directory)
454
454
 
455
455
  assert validator_run.returncode == 0, validator_run.stderr
456
+
457
+
458
+ def test_reuse_audit_with_only_unjustified_reproduction_fails(tmp_path: Path) -> None:
459
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
460
+ write_valid_packet(packet_directory)
461
+ (packet_directory / "validation" / "reuse-audit.md").write_text(
462
+ "# Reuse Audit\n\n"
463
+ "| Item | Kind | Verdict | Searched | Found | Decision | Evidence |\n"
464
+ "|---|---|---|---|---|---|---|\n"
465
+ "| duplicate_alert | helper | unjustified-reproduction | shared_utils/alerts | no existing equivalent | duplicate the logic | src/alert.py:9 |\n\n"
466
+ "Summary: unjustified-reproduction 1.\n",
467
+ encoding="utf-8",
468
+ )
469
+
470
+ validator_run = run_validator(packet_directory)
471
+
472
+ assert validator_run.returncode == 2
473
+ assert "reuse-audit.md must name a justifying reuse verdict" in validator_run.stderr
@@ -187,21 +187,37 @@ def tdd_plan_errors(packet_directory: Path) -> list[str]:
187
187
 
188
188
 
189
189
  def reuse_audit_errors(packet_directory: Path) -> list[str]:
190
- """Return errors for a reuse audit that records no per-item verdict.
190
+ """Return errors for a reuse audit that names no justifying reuse verdict.
191
191
 
192
192
  Args:
193
193
  packet_directory: Directory that should contain validation/reuse-audit.md.
194
194
 
195
195
  Returns:
196
- An error string when the reuse audit names no reuse verdict, else an empty list.
196
+ An error string when the reuse audit names none of the justifying reuse
197
+ verdicts (reused, extract-to-shared, new-justified, config-local) as a
198
+ whole token, else an empty list. Each verdict matches only as a whole
199
+ token, so unjustified-reproduction does not satisfy the check through the
200
+ justified or reproduction substrings, and an audit naming only the
201
+ unjustified-reproduction verdict is an error.
197
202
  """
198
203
  reuse_audit_file = packet_directory / "validation" / "reuse-audit.md"
199
204
  if not reuse_audit_file.is_file():
200
205
  return []
201
206
  reuse_audit_text = reuse_audit_file.read_text(encoding="utf-8").lower()
202
- verdict_keywords = ("reused", "extract", "justified", "config-local", "reproduction")
203
- if not any(each_keyword in reuse_audit_text for each_keyword in verdict_keywords):
204
- return ["reuse-audit.md must record a reuse verdict for each new item"]
207
+ justifying_verdict_tokens = (
208
+ "reused",
209
+ "extract-to-shared",
210
+ "new-justified",
211
+ "config-local",
212
+ )
213
+ if not any(
214
+ re.search(r"\b" + re.escape(each_token) + r"\b", reuse_audit_text)
215
+ for each_token in justifying_verdict_tokens
216
+ ):
217
+ return [
218
+ "reuse-audit.md must name a justifying reuse verdict "
219
+ "(reused, extract-to-shared, new-justified, or config-local)"
220
+ ]
205
221
  return []
206
222
 
207
223
 
@@ -31,7 +31,14 @@ def test_skill_no_longer_mentions_single_home_plan_file() -> None:
31
31
  skill_text = SKILL_PATH.read_text(encoding="utf-8")
32
32
 
33
33
  assert "~/.claude/plans/<slug>.md" not in skill_text
34
- assert "single-file" not in skill_text.lower()
34
+ assert "single-file plan" not in skill_text.lower()
35
+
36
+
37
+ def test_skill_isolates_into_a_worktree_before_launch() -> None:
38
+ skill_text = SKILL_PATH.read_text(encoding="utf-8")
39
+
40
+ assert "EnterWorktree" in skill_text
41
+ assert ".claude/worktrees/" in skill_text
35
42
 
36
43
 
37
44
  def test_skill_documents_self_healing_writes() -> None:
@@ -159,6 +159,24 @@ test('workflow runs the reuse audit after writing the packet', () => {
159
159
  assert.ok(writeIndex < reuseAuditIndex);
160
160
  });
161
161
 
162
+ test('reuse audit prompt self-heals a blocked write by staging and copying into place', () => {
163
+ const reuseAuditPromptBody = functionBody('reuseAuditPrompt');
164
+ assert.match(reuseAuditPromptBody, /stag/i);
165
+ assert.match(reuseAuditPromptBody, /copy/i);
166
+ assert.match(reuseAuditPromptBody, /recover/i);
167
+ });
168
+
169
+ test('reuse audit schema carries the recovery signal', () => {
170
+ const reuseAuditSchemaBody = functionBody('reuseAuditSchema');
171
+ assert.match(reuseAuditSchemaBody, /recovered/);
172
+ assert.match(reuseAuditSchemaBody, /recoveryNote/);
173
+ });
174
+
175
+ test('workflow folds reuse-audit recovery into the top-level recovered signal', () => {
176
+ const runBody = functionBody('runPlanPacketWorkflow');
177
+ assert.match(runBody, /recordRecovery\(reuseAudit\)/);
178
+ });
179
+
162
180
  test('workflow folds the reuse audit gate into the clean validation check', () => {
163
181
  const runBody = functionBody('runPlanPacketWorkflow');
164
182
  assert.match(runBody, /reuseAudit\.allJustified/);
@@ -188,6 +206,24 @@ test('visual html prompt names the template and the output file', () => {
188
206
  assert.match(visualHtmlPromptBody, /visual-plan\.html/);
189
207
  });
190
208
 
209
+ test('visual html prompt self-heals a blocked write by staging and copying into place', () => {
210
+ const visualHtmlPromptBody = functionBody('visualHtmlPrompt');
211
+ assert.match(visualHtmlPromptBody, /stag/i);
212
+ assert.match(visualHtmlPromptBody, /copy/i);
213
+ assert.match(visualHtmlPromptBody, /recover/i);
214
+ });
215
+
216
+ test('visual html schema carries the recovery signal', () => {
217
+ const visualHtmlSchemaBody = functionBody('visualHtmlSchema');
218
+ assert.match(visualHtmlSchemaBody, /recovered/);
219
+ assert.match(visualHtmlSchemaBody, /recoveryNote/);
220
+ });
221
+
222
+ test('workflow folds visual-html recovery into the top-level recovered signal', () => {
223
+ const runBody = functionBody('runPlanPacketWorkflow');
224
+ assert.match(runBody, /recordRecovery\(visualHtml\)/);
225
+ });
226
+
191
227
  test('workflow returns the visual html path', () => {
192
228
  const runBody = functionBody('runPlanPacketWorkflow');
193
229
  assert.match(runBody, /visualHtmlPath/);
@@ -105,8 +105,10 @@ function reuseAuditSchema() {
105
105
  },
106
106
  },
107
107
  summary: { type: 'string' },
108
+ recovered: { type: 'boolean' },
109
+ recoveryNote: { type: 'string' },
108
110
  },
109
- required: ['allJustified', 'findings', 'summary'],
111
+ required: ['allJustified', 'findings', 'summary', 'recovered', 'recoveryNote'],
110
112
  }
111
113
  }
112
114
 
@@ -118,8 +120,10 @@ function visualHtmlSchema() {
118
120
  htmlPath: { type: 'string' },
119
121
  sectionsBuilt: { type: 'array', items: { type: 'string' } },
120
122
  summary: { type: 'string' },
123
+ recovered: { type: 'boolean' },
124
+ recoveryNote: { type: 'string' },
121
125
  },
122
- required: ['htmlPath', 'sectionsBuilt', 'summary'],
126
+ required: ['htmlPath', 'sectionsBuilt', 'summary', 'recovered', 'recoveryNote'],
123
127
  }
124
128
  }
125
129
 
@@ -252,7 +256,8 @@ function reuseAuditPrompt(packetPath) {
252
256
  `Read implementation/file-plan.md, spec/interfaces.md, implementation/tdd-plan.md, and spec/scope.md in the packet to enumerate every new file, public symbol, helper, and constant the build introduces.\n\n` +
253
257
  `For each item, search the codebase with grep, serena, or zoekt — repo-wide and specifically under shared_utils — for an existing implementation or near-equivalent behavior.\n\n` +
254
258
  `Assign exactly one verdict per item from: reused (an existing public helper is used), extract-to-shared (an equivalent exists but is not shared or public and should be extracted), new-justified (genuinely new, with the reason reuse or extract was rejected), config-local (a constant living in config/), or unjustified-reproduction (reproduces existing behavior that could be made public or extracted, with no recorded justification).\n\n` +
255
- `Write validation/reuse-audit.md into the packet: a markdown table with columns Item, Kind, Verdict, Searched, Found, Decision, Evidence using real file:line evidence, plus a one-line summary of verdict counts. Write concrete content only — no angle-bracket placeholder tokens and no todo, tbd, or placeholder words.\n\n` +
259
+ `Write validation/reuse-audit.md into the packet with the Write tool: a markdown table with columns Item, Kind, Verdict, Searched, Found, Decision, Evidence using real file:line evidence, plus a one-line summary of verdict counts. Write concrete content only — no angle-bracket placeholder tokens and no todo, tbd, or placeholder words.\n\n` +
260
+ `If the Write tool is blocked by a worktree or isolation guard, recover automatically: write validation/reuse-audit.md under a writable temporary directory such as $CLAUDE_JOB_DIR/tmp/anthropic-plan/<slug>/validation with the Write tool (so the content checks still run), then copy it into the packet path with a filesystem copy (cp -r, Copy-Item, or equivalent). Set recovered=true with recoveryNote describing the staging path and copy; otherwise set recovered=false with an empty recoveryNote.\n\n` +
256
261
  `Return the structured object. Set allJustified=false when any finding has verdict unjustified-reproduction.`
257
262
  )
258
263
  }
@@ -266,7 +271,7 @@ function visualHtmlPrompt(packetPath) {
266
271
  `Write for the reviewer — a person reading the plan, not the computer that runs the code. State every label as what a step accomplishes, in plain language. Drop code symbols from the picture: no function names, selector strings, call traces, or snake_case test names in the visible diagram — those stay in the packet markdown for the build agent. Keep each touched file's repo-relative path, but dim it (the .rpath / .ap style) so it sits quietly beneath the human description.\n\n` +
267
272
  `Render the change (section 05) as edit-recipe step sequences, one recipe per touched file: a plain-language title for what the file accomplishes, the dimmed repo-relative path, then an ordered row of colored steps — reused (green), modified (violet), new (amber). Fold a trivial one-line change into the recipe it supports as an "Also adds" line rather than giving it its own card. Name each test by the behavior it proves, not its function name.\n\n` +
268
273
  `Surface validation/reuse-audit.md as a Reuse audit section with one verdict badge per item (reused, extract-to-shared, new-justified, config-local, unjustified-reproduction), each item titled in plain language with its file path dimmed.\n\n` +
269
- `Write the result to ${packetPath}/visual-plan.html. Inline all CSS and JavaScript; make no network calls and reference no external assets, so the file opens offline. If the Write tool is blocked by a worktree or isolation guard, stage the file under $CLAUDE_JOB_DIR/tmp with the Write tool, then copy it to the packet path.\n\n` +
274
+ `Write the result to ${packetPath}/visual-plan.html. Inline all CSS and JavaScript; make no network calls and reference no external assets, so the file opens offline. If the Write tool is blocked by a worktree or isolation guard, recover automatically: stage the file under a writable temporary directory such as $CLAUDE_JOB_DIR/tmp/anthropic-plan/<slug> with the Write tool, then copy it to the packet path with a filesystem copy (cp -r, Copy-Item, or equivalent). Set recovered=true with recoveryNote describing the staging path and copy; otherwise set recovered=false with an empty recoveryNote.\n\n` +
270
275
  `Return htmlPath set to the written file path, sectionsBuilt listing the section names you included, and a one-line summary.`
271
276
  )
272
277
  }
@@ -360,6 +365,7 @@ async function runPlanPacketWorkflow(rawInput) {
360
365
  packetWrite = await writePacket(runInput, packetPath, discoverySummary)
361
366
  recordRecovery(packetWrite)
362
367
  reuseAudit = await runReuseAudit(packetPath)
368
+ recordRecovery(reuseAudit)
363
369
  deterministicValidation = await runDeterministicValidation(packetPath)
364
370
  semanticValidation = await runSemanticValidator(packetPath)
365
371
  const hasCleanValidation = () =>
@@ -374,6 +380,7 @@ async function runPlanPacketWorkflow(rawInput) {
374
380
  const repair = await repairPacket(packetPath, deterministicValidation, semanticValidation, reuseAudit)
375
381
  recordRecovery(repair)
376
382
  reuseAudit = await runReuseAudit(packetPath)
383
+ recordRecovery(reuseAudit)
377
384
  deterministicValidation = await runDeterministicValidation(packetPath)
378
385
  semanticValidation = await runSemanticValidator(packetPath)
379
386
  }
@@ -381,6 +388,7 @@ async function runPlanPacketWorkflow(rawInput) {
381
388
  const passed = hasCleanValidation()
382
389
  try {
383
390
  const visualHtml = await runVisualHtml(packetPath)
391
+ recordRecovery(visualHtml)
384
392
  visualHtmlPath = visualHtml?.htmlPath || ''
385
393
  } catch (visualHtmlError) {
386
394
  visualHtmlFindings.push(String(visualHtmlError?.message || visualHtmlError))