Skip to content

Commit 864bc25

Browse files
nhortonclaude
andauthored
feat: teach learn workflow to create deepschemas and deepreview rules (#327)
* feat: inject sorted git diff into broad review rule prompts Rules with strategy all_changed_files or matches_together and a **/* matcher now get git diff main..HEAD pre-fetched and injected into the review instruction file. The diff is sorted by filepath to group files by directory, reducing reviewer turn count. Diff is scoped to the rule's source_dir so subdirectory .deepreview files get narrower diffs. Also streamlines the /review skill to skip the get_configured_reviews call and go straight to get_review_instructions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: teach learn workflow to create deepschemas and deepreview rules The learn workflow now evaluates whether deepschemas or deepreview rules could prevent issues encountered during a session, covering both process mistakes and the substance of the work. Adds a "Preventable" classification, a new Step 7 with actionable instructions for creating anonymous/named deepschemas and deepreview rules, and a process requirement ensuring prevention opportunities are always evaluated. Also adds a filesystem-verifiable requirement to the deepschema meta-schema, ensuring all deepschema requirements can be checked from filesystem assets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add claude_settings deepschema and enforce JSON-schema-first requirements - Add .deepwork/schemas/claude_settings/ with deepschema.yml (semantic requirements) and vendored SchemaStore JSON Schema for structural validation - Add no-structural-requirements rule to deepschema standard schema: requirements must not restate what json_schema_path already enforces - Update /deepschema skill with JSON Schema First guidance: maximize structural validation, check SchemaStore for existing schemas, vendor local copies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update architecture.md to reflect learn workflow's preventive automation The learn workflow now evaluates prevention opportunities and creates DeepSchemas and DeepReview rules. Updated three sections in architecture.md to document this capability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move knowledge-based deepschema requirements to instructions Requirements about author understanding (sandbox path semantics, automode defaults replacement, attribution deprecation, sandbox write paths) are not filesystem-verifiable. Moved them to the instructions section and kept only requirements that can be checked by examining file contents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix ruff formatting in review/instructions.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fc8e1bf commit 864bc25

14 files changed

Lines changed: 2469 additions & 9 deletions

File tree

.deepwork/schemas/claude_settings/claude_settings.schema.json

Lines changed: 1801 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
summary: "Claude Code settings.json and settings.local.json configuration files."
2+
3+
instructions: |
4+
Claude Code settings files control permissions, hooks, sandbox, model preferences,
5+
and plugin configuration. There are three scopes with distinct purposes:
6+
7+
- `.claude/settings.json` — shared project settings, committed to git. Team-wide
8+
permissions, hooks, and MCP server approvals.
9+
- `.claude/settings.local.json` — personal per-project overrides, gitignored.
10+
Sensitive credentials, personal MCP configs, local permission tweaks.
11+
- `~/.claude/settings.json` — user-global defaults for all projects.
12+
13+
Precedence (highest to lowest): managed → CLI args → settings.local.json →
14+
settings.json → ~/.claude/settings.json → defaults. Deny rules at ANY level
15+
cannot be overridden by allow rules at any other level.
16+
17+
Key sections: permissions (allow/ask/deny tool rules), hooks (lifecycle event
18+
handlers), sandbox (OS-level isolation), env (environment variables), and
19+
various global config fields (model, attribution, autoMode, etc.).
20+
21+
Gotchas to keep in mind when editing these files:
22+
23+
- Sandbox path settings use DIFFERENT prefix semantics than permission rules:
24+
`/path` means absolute (not project-relative), `~/` means home, `./` or
25+
bare path means project root. Do not confuse these with Read/Edit path rules.
26+
- When `autoMode.allow` or `autoMode.soft_deny` arrays are set, they completely
27+
replace the built-in defaults rather than merging. Review defaults via
28+
`claude auto-mode defaults` before overriding.
29+
- When `sandbox.enabled` is true, `sandbox.filesystem.allowWrite` should include
30+
the project directory and any tool output directories. An overly restrictive
31+
sandbox without necessary write paths will cause tool failures.
32+
- The `attribution` object supersedes the deprecated `includeCoAuthoredBy`
33+
boolean. Prefer `attribution` for new configurations.
34+
35+
matchers:
36+
- ".claude/settings.json"
37+
- ".claude/settings.local.json"
38+
- "**/.claude/settings.json"
39+
- "**/.claude/settings.local.json"
40+
41+
json_schema_path: "claude_settings.schema.json"
42+
43+
requirements:
44+
bash-wildcard-word-boundaries: >
45+
Bash permission patterns MUST use the `:*` style to enforce word
46+
boundaries (e.g., `Bash(npm run:*)`) and only use regular * if it is
47+
clearly a situation where the whitespace is not appropriate. Patterns
48+
MUST be reviewed for unintended broad matching.
49+
50+
read-edit-path-prefixes: >
51+
Read and Edit permission specifiers MUST use the correct path prefix
52+
convention: `./**` or bare paths for project-relative, `~` for home
53+
directory, `//` (double slash) for absolute filesystem paths. A single
54+
leading `/` is project-relative, NOT absolute — this is a common mistake.
55+
56+
no-secrets-in-shared-settings: >
57+
`.claude/settings.json` (the shared, committed file) MUST NOT contain
58+
secrets, API keys, tokens, or credentials. Sensitive values MUST go in
59+
`.claude/settings.local.json` (gitignored) or environment variables.
60+
61+
automode-not-in-shared-settings: >
62+
`autoMode` configuration MUST NOT appear in `.claude/settings.json` (shared
63+
project settings). Claude Code ignores autoMode from shared project settings
64+
to prevent malicious repos from weakening security. Use user-level or
65+
managed settings instead.
66+
67+
mcp-tools-pattern-valid: >
68+
MCP tool permission patterns MUST follow the format
69+
`mcp__<server_name>__<tool_name>` or use a wildcard `mcp__<server_name>__*`
70+
for all tools from a server. The double-underscore separators are required.
71+
MCP tool permissions MUST NOT use `:*`.
72+
73+
references:
74+
- path: "https://json.schemastore.org/claude-code-settings.json"
75+
description: "Community-maintained JSON Schema on SchemaStore — the most comprehensive structural schema available. Consider syncing our local schema with this periodically."
76+
- path: "https://docs.anthropic.com/en/docs/claude-code/settings"
77+
description: "Official Claude Code settings documentation — complete field reference and precedence rules."
78+
- path: "https://docs.anthropic.com/en/docs/claude-code/security"
79+
description: "Claude Code security model — permissions, sandboxing, and managed settings."

doc/architecture.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ DeepWork includes a built-in job called `deepwork_jobs` for managing jobs. It pr
483483
- **`repair`** workflow: `fix_settings` → `fix_jobs` → `errata`
484484
- Cleans up and migrates DeepWork configurations from prior versions
485485
- **`learn`** workflow: `learn`
486-
- Analyzes conversation history to improve job instructions and capture learnings
486+
- Analyzes conversation history to improve job instructions, capture learnings, and create preventive automation (DeepSchemas and DeepReview rules)
487487
488488
These are auto-discovered at runtime by the MCP server from the Python package.
489489
@@ -532,7 +532,7 @@ User: /deepwork new_job
532532
533533
### The `learn` Workflow
534534
535-
Analyzes conversation history to improve job instructions and capture learnings:
535+
Analyzes conversation history to improve job instructions, capture learnings, and create preventive automation:
536536
537537
```
538538
User: /deepwork_jobs.learn
@@ -551,10 +551,14 @@ Claude: I'll analyze this conversation for DeepWork job executions...
551551
Bespoke learnings captured:
552552
✓ Created AGENTS.md with project-specific notes about this competitive research instance
553553

554+
Prevention opportunities evaluated:
555+
✓ Created DeepSchema for competitor_profiles/ output format
556+
✓ Added DeepReview rule to enforce source prioritization in research steps
557+
554558
Job instructions updated in place. Changes take effect on next workflow run.
555559
```
556560
557-
This standalone skill can be run anytime after executing a job to capture learnings and improve instructions.
561+
This standalone skill can be run anytime after executing a job to capture learnings, improve instructions, and create preventive automation (DeepSchemas and DeepReview rules).
558562
559563
### Step Instructions at Runtime
560564
@@ -742,6 +746,7 @@ The `/deepwork_jobs.define` command:
742746
The `/deepwork_jobs.learn` command:
743747
1. Identifies doc spec-related learnings (quality criteria issues, structure changes)
744748
2. Updates doc spec files with improvements
749+
3. Evaluates prevention opportunities and creates DeepSchemas and DeepReview rules
745750

746751
See `doc/doc-specs.md` for complete documentation.
747752

plugins/claude/skills/deepschema/SKILL.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ matchers:
5151
- "src/configs/**/*.json"
5252

5353
requirements:
54-
has-version: "Every config file MUST include a version field."
54+
# Semantic rules only — structural constraints go in config.schema.json
5555
documented-fields: "All fields SHOULD have inline comments explaining their purpose."
5656
no-secrets: "Config files MUST NOT contain secrets or credentials."
5757

58-
# Optional: structural validation
58+
# Structural validation — enforce types, required fields, enums, etc. here
5959
json_schema_path: "config.schema.json"
6060

6161
# Optional: custom validation commands (file path passed as $1)
@@ -72,13 +72,53 @@ Place a `.deepschema.<filename>.yml` file next to the target file:
7272
```yaml
7373
requirements:
7474
api-key-rotated: "The API key MUST be rotated every 90 days."
75-
format-valid: "The file MUST be valid YAML."
75+
no-plaintext-secrets: "Credentials MUST use environment variable references, not literal values."
7676
7777
# Reference a named schema for shared requirements
7878
parent_deep_schemas:
7979
- api_endpoint
8080
```
8181

82+
## JSON Schema First: Maximize Structural Validation
83+
84+
**The `json_schema_path` file is the primary enforcement mechanism.** Every constraint that _can_ be expressed structurally MUST go in the JSON Schema, not in requirements. Requirements exist only for semantic rules that JSON Schema cannot express.
85+
86+
Put in the JSON Schema (not requirements):
87+
- File format validity (valid JSON, valid YAML)
88+
- Field types (string, number, boolean, array, object)
89+
- Required fields
90+
- Allowed property names (`additionalProperties: false`)
91+
- Enum values and allowed constants
92+
- Array item types and constraints (`minItems`, `uniqueItems`)
93+
- Numeric ranges (`minimum`, `maximum`)
94+
- String patterns (`pattern`, `format`)
95+
- Conditional field presence (`if`/`then` — e.g., "when type is 'http', url is required")
96+
- Nested object shapes and their constraints
97+
98+
Put in requirements (not the JSON Schema):
99+
- Semantic rules about _meaning_ ("secrets MUST NOT appear in shared settings")
100+
- Cross-file concerns ("this field MUST reference an existing named schema")
101+
- Behavioral gotchas ("sandbox paths use different prefix semantics than permission paths")
102+
- Design guidance ("deny rules SHOULD be used for hard security boundaries, not soft preferences")
103+
- Anything requiring judgment or context a machine validator cannot assess
104+
105+
**Build the JSON Schema to be as strict and comprehensive as possible.** Use `additionalProperties: false` to catch typos. Use enums for closed sets. Use `if/then` for conditional requirements. Use `pattern` for string formats. Use `$defs` and `$ref` for reusable types. Use `anyOf` for discriminated unions. Use `uniqueItems`, `minLength`, `minItems` where appropriate. A good JSON Schema catches errors at write time before a reviewer ever sees the file. Requirements that duplicate what the schema already enforces are noise — they dilute the reviewer's attention and risk contradicting the schema.
106+
107+
### Check SchemaStore for Existing Schemas
108+
109+
Before writing a JSON Schema from scratch, check whether a published schema already exists at [SchemaStore](https://www.schemastore.org/) (`https://json.schemastore.org/<name>.json`). SchemaStore hosts community-maintained schemas for hundreds of config file formats.
110+
111+
If a good schema exists:
112+
1. **Vendor a local copy** into your schema directory (e.g., `claude_settings.schema.json`)
113+
2. **Add a `_source` field** at the top of the file with the original URL and sync date:
114+
```json
115+
{
116+
"_source": "Vendored from https://json.schemastore.org/example.json. To update: fetch the latest version from that URL and replace this file. Last synced: 2026-04-01."
117+
}
118+
```
119+
3. **Point `json_schema_path`** at the local copy — this avoids network dependencies during validation
120+
4. **Periodically re-fetch** the upstream schema to pick up improvements — the `_source` field tells future maintainers where to look
121+
82122
## Schema Fields Reference
83123

84124
| Field | Description |

specs/deepwork/review/REVIEW-REQ-004-rule-matching-and-strategies.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ After discovering review rules (REVIEW-REQ-002) and changed files (REVIEW-REQ-00
1919
### REVIEW-REQ-004.2: Review Task Data Model
2020

2121
1. Each review task MUST be represented as a `ReviewTask` dataclass.
22-
2. The `ReviewTask` MUST contain: `rule_name` (str), `files_to_review` (list[str] — paths relative to repo root), `instructions` (str), `agent_name` (str | None), `source_location` (str — formatted as `"path:line"`), `additional_files` (list[str] — unchanged matching files, relative to repo root), `all_changed_filenames` (list[str] | None).
22+
2. The `ReviewTask` MUST contain: `rule_name` (str), `files_to_review` (list[str] — paths relative to repo root), `instructions` (str), `agent_name` (str | None), `source_location` (str — formatted as `"path:line"`), `additional_files` (list[str] — unchanged matching files, relative to repo root), `all_changed_filenames` (list[str] | None), `git_diff_output` (str | None — pre-fetched diff for broad rules, see REVIEW-REQ-004.11).
2323
3. `files_to_review` MUST always contain at least one file path.
2424
4. `source_location` MUST be formatted as `"{relative_path}:{line_number}"` where the path is relative to the project root (e.g., `"src/.deepreview:5"`).
2525

@@ -71,3 +71,12 @@ After discovering review rules (REVIEW-REQ-002) and changed files (REVIEW-REQ-00
7171
1. Rules with the same name defined in different `.deepreview` files MUST produce independent `ReviewTask` objects. The system MUST NOT merge or combine matched files across rules from different source directories.
7272
2. When two `.deepreview` files in different directories define a rule with the same name and the same strategy, and changed files match both rules, the system MUST create separate `ReviewTask` objects — one per directory — each containing only the files that matched within its own `source_dir`.
7373
3. This isolation is a consequence of REVIEW-REQ-004.1.2 (files outside `source_dir` do not match) but is stated explicitly because `.deepreview` files can be templated or symlinked across directories, making same-name rules a common scenario.
74+
75+
### REVIEW-REQ-004.11: Git Diff Injection for Broad Rules
76+
77+
1. When a rule has strategy `"all_changed_files"` or `"matches_together"` AND its `include` patterns contain `**/*`, the system MUST run `git diff <merge-base>..HEAD` and attach the output to the resulting `ReviewTask` as `git_diff_output`.
78+
2. The git diff MUST be computed at most once per unique `source_dir` per `match_files_to_rules` invocation, even if multiple rules with the same `source_dir` qualify for injection.
79+
3. If the git diff command fails or produces empty output, `git_diff_output` MUST be `None`.
80+
4. Rules with strategy `"individual"` MUST NOT receive `git_diff_output`, regardless of their include patterns.
81+
5. The `ReviewTask` dataclass MUST include a `git_diff_output: str | None` field defaulting to `None`.
82+
6. When a rule's `source_dir` is a subdirectory of the project root, the git diff MUST be scoped to that subdirectory (via `-- <relpath>` pathspec). When `source_dir` equals the project root, the diff MUST cover the entire repository.

specs/deepwork/review/REVIEW-REQ-005-instruction-generation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ For each `ReviewTask`, the system generates a self-contained markdown instructio
1616
6. When the task has `additional_files` (unchanged matching files), the file MUST contain an "Unchanged Matching Files" section listing those file paths.
1717
7. When the task has `all_changed_filenames`, the file MUST contain an "All Changed Files" section listing every changed filename for context.
1818

19+
### REVIEW-REQ-005.7: Git Diff Section
20+
21+
1. When a `ReviewTask` has a non-null `git_diff_output`, the instruction file MUST contain a section headed `## Output from \`git diff main..HEAD\` for you to review (sorted by filepath)`.
22+
2. The diff output MUST be rendered inside a fenced code block with the `diff` language tag.
23+
3. This section MUST appear after the "Files to Review" section and before the "All Changed Files" section.
24+
4. When `git_diff_output` is `None`, this section MUST be omitted.
25+
1926
### REVIEW-REQ-005.2: File Path Formatting
2027

2128
1. File paths in the "Files to Review" section MUST be prefixed with `@` to trigger Claude Code's file-reading behavior (e.g., `@src/app.py`).

src/deepwork/review/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ReviewTask:
4949
source_location: str = "" # e.g. "src/.deepreview:5"
5050
additional_files: list[str] = field(default_factory=list) # Unchanged matching files
5151
all_changed_filenames: list[str] | None = None
52+
git_diff_output: str | None = None # Pre-fetched git diff for broad rules
5253

5354

5455
def parse_deepreview_file(filepath: Path) -> list[ReviewRule]:

src/deepwork/review/instructions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ def build_instruction_file(task: ReviewTask, review_id: str = "") -> str:
155155
parts.append(f"- @{filepath}")
156156
parts.append("")
157157

158+
# Pre-fetched git diff for broad rules
159+
if task.git_diff_output:
160+
parts.append(
161+
"## Output from `git diff main..HEAD` for you to review (sorted by filepath)\n"
162+
)
163+
parts.append("```diff")
164+
parts.append(task.git_diff_output.rstrip())
165+
parts.append("```")
166+
parts.append("")
167+
158168
# Additional context: all changed filenames
159169
if task.all_changed_filenames:
160170
parts.append("## All Changed Files\n")

src/deepwork/review/matcher.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,75 @@ def _git_untracked_files(project_root: Path) -> list[str]:
192192
raise GitDiffError(f"git ls-files failed: {e.stderr.strip()}") from e
193193

194194

195+
def _should_inject_diff(rule: ReviewRule) -> bool:
196+
"""Check if a rule qualifies for git diff injection.
197+
198+
Rules with ``all_changed_files`` or ``matches_together`` strategy and
199+
a ``**/*`` include pattern get the diff injected to reduce reviewer
200+
turn count.
201+
"""
202+
if rule.strategy not in ("all_changed_files", "matches_together"):
203+
return False
204+
return "**/*" in rule.include_patterns
205+
206+
207+
def _sort_diff_by_path(diff_text: str) -> str:
208+
"""Sort a unified diff's file hunks by path.
209+
210+
Splits on ``diff --git`` boundaries, sorts the chunks
211+
alphabetically by file path (which naturally groups by directory),
212+
and rejoins them.
213+
"""
214+
if not diff_text.strip():
215+
return diff_text
216+
217+
# Split into per-file chunks. The first element before the first
218+
# "diff --git" marker is usually empty or whitespace — preserve it.
219+
parts = re.split(r"(?=^diff --git )", diff_text, flags=re.MULTILINE)
220+
chunks: list[tuple[str, str]] = []
221+
preamble = ""
222+
for part in parts:
223+
if part.startswith("diff --git "):
224+
# Extract the b-side path: "diff --git a/x b/y" → "y"
225+
first_line = part.split("\n", 1)[0]
226+
b_path = first_line.rsplit(" b/", 1)[-1] if " b/" in first_line else first_line
227+
chunks.append((b_path, part))
228+
elif part.strip():
229+
preamble += part
230+
231+
chunks.sort(key=lambda c: c[0])
232+
sorted_parts = [c[1] for c in chunks]
233+
if preamble:
234+
sorted_parts.insert(0, preamble)
235+
return "".join(sorted_parts)
236+
237+
238+
def _get_git_diff(project_root: Path, scope_dir: Path | None = None) -> str:
239+
"""Run ``git diff <base>..HEAD`` and return the output, sorted by path.
240+
241+
Uses the same base-ref detection logic as changed-file detection.
242+
When *scope_dir* is provided and differs from *project_root*, the
243+
diff is restricted to that subdirectory via ``-- <relpath>``.
244+
The output is sorted by file path so that files in the same
245+
directory are grouped together.
246+
Returns an empty string on failure.
247+
"""
248+
base_ref = _detect_base_ref(project_root)
249+
merge_base = _get_merge_base(project_root, base_ref)
250+
args = ["diff", f"{merge_base}..HEAD"]
251+
if scope_dir is not None and scope_dir != project_root:
252+
try:
253+
rel = scope_dir.relative_to(project_root)
254+
args += ["--", str(rel)]
255+
except ValueError:
256+
pass
257+
try:
258+
result = _run_git(project_root, *args)
259+
return _sort_diff_by_path(result.stdout)
260+
except subprocess.CalledProcessError:
261+
return ""
262+
263+
195264
def match_files_to_rules(
196265
changed_files: list[str],
197266
rules: list[ReviewRule],
@@ -213,6 +282,8 @@ def match_files_to_rules(
213282
List of ReviewTask objects.
214283
"""
215284
tasks: list[ReviewTask] = []
285+
# Lazy-compute diff per source_dir, shared across qualifying rules
286+
_cached_diffs: dict[Path, str] = {}
216287

217288
for rule in rules:
218289
matched = match_rule(changed_files, rule, project_root)
@@ -223,6 +294,14 @@ def match_files_to_rules(
223294
all_filenames = changed_files if rule.all_changed_filenames else None
224295
source_location = format_source_location(rule, project_root)
225296

297+
# Lazily fetch diff for broad rules to reduce reviewer turn count
298+
diff_output: str | None = None
299+
if _should_inject_diff(rule):
300+
scope = rule.source_dir
301+
if scope not in _cached_diffs:
302+
_cached_diffs[scope] = _get_git_diff(project_root, scope)
303+
diff_output = _cached_diffs[scope] or None
304+
226305
if rule.strategy == "individual":
227306
for filepath in matched:
228307
tasks.append(
@@ -249,6 +328,7 @@ def match_files_to_rules(
249328
source_location=source_location,
250329
additional_files=additional,
251330
all_changed_filenames=all_filenames,
331+
git_diff_output=diff_output,
252332
)
253333
)
254334

@@ -261,6 +341,7 @@ def match_files_to_rules(
261341
agent_name=agent_name,
262342
source_location=source_location,
263343
all_changed_filenames=all_filenames,
344+
git_diff_output=diff_output,
264345
)
265346
)
266347

0 commit comments

Comments
 (0)