deepwork 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- deepwork/cli/install.py +48 -0
- deepwork/cli/sync.py +9 -0
- deepwork/core/adapters.py +17 -0
- deepwork/core/policy_parser.py +10 -0
- deepwork/hooks/evaluate_policies.py +237 -20
- deepwork/schemas/policy_schema.py +10 -0
- deepwork/standard_jobs/deepwork_jobs/AGENTS.md +60 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +30 -22
- deepwork/standard_jobs/deepwork_jobs/make_new_job.sh +134 -0
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +26 -57
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +43 -242
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +288 -0
- deepwork/standard_jobs/deepwork_jobs/steps/supplemental_file_references.md +40 -0
- deepwork/standard_jobs/deepwork_jobs/templates/agents.md.template +32 -0
- deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example +73 -0
- deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template +56 -0
- deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example +82 -0
- deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template +58 -0
- deepwork/standard_jobs/deepwork_policy/hooks/{capture_work_tree.sh → capture_prompt_work_tree.sh} +3 -2
- deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +3 -19
- deepwork/standard_jobs/deepwork_policy/hooks/user_prompt_submit.sh +5 -6
- deepwork/standard_jobs/deepwork_policy/steps/define.md +22 -1
- deepwork/templates/default_policy.yml +53 -0
- {deepwork-0.1.0.dist-info → deepwork-0.2.0.dist-info}/METADATA +52 -128
- deepwork-0.2.0.dist-info/RECORD +49 -0
- deepwork/standard_jobs/deepwork_jobs/steps/refine.md +0 -447
- deepwork/standard_jobs/deepwork_policy/hooks/get_changed_files.sh +0 -30
- deepwork-0.1.0.dist-info/RECORD +0 -41
- {deepwork-0.1.0.dist-info → deepwork-0.2.0.dist-info}/WHEEL +0 -0
- {deepwork-0.1.0.dist-info → deepwork-0.2.0.dist-info}/entry_points.txt +0 -0
- {deepwork-0.1.0.dist-info → deepwork-0.2.0.dist-info}/licenses/LICENSE.md +0 -0
deepwork/cli/install.py
CHANGED
|
@@ -113,6 +113,48 @@ def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
|
|
|
113
113
|
gitignore_path.write_text(gitignore_content)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def _create_default_policy_file(project_path: Path) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Create a default policy file template in the project root.
|
|
119
|
+
|
|
120
|
+
Only creates the file if it doesn't already exist.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
project_path: Path to the project root
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if the file was created, False if it already existed
|
|
127
|
+
"""
|
|
128
|
+
policy_file = project_path / ".deepwork.policy.yml"
|
|
129
|
+
|
|
130
|
+
if policy_file.exists():
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# Copy the template from the templates directory
|
|
134
|
+
template_path = Path(__file__).parent.parent / "templates" / "default_policy.yml"
|
|
135
|
+
|
|
136
|
+
if template_path.exists():
|
|
137
|
+
shutil.copy(template_path, policy_file)
|
|
138
|
+
else:
|
|
139
|
+
# Fallback: create a minimal template inline
|
|
140
|
+
policy_file.write_text(
|
|
141
|
+
"""# DeepWork Policy Configuration
|
|
142
|
+
#
|
|
143
|
+
# Policies are automated guardrails that trigger when specific files change.
|
|
144
|
+
# Use /deepwork_policy.define to create new policies interactively.
|
|
145
|
+
#
|
|
146
|
+
# Format:
|
|
147
|
+
# - name: "Policy name"
|
|
148
|
+
# trigger: "glob/pattern/**/*"
|
|
149
|
+
# safety: "optional/pattern/**/*"
|
|
150
|
+
# instructions: |
|
|
151
|
+
# Instructions for the AI agent...
|
|
152
|
+
"""
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
116
158
|
class DynamicChoice(click.Choice):
|
|
117
159
|
"""A Click Choice that gets its values dynamically from AgentAdapter."""
|
|
118
160
|
|
|
@@ -238,6 +280,12 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
238
280
|
_create_deepwork_gitignore(deepwork_dir)
|
|
239
281
|
console.print(" [green]✓[/green] Created .deepwork/.gitignore")
|
|
240
282
|
|
|
283
|
+
# Step 3d: Create default policy file template
|
|
284
|
+
if _create_default_policy_file(project_path):
|
|
285
|
+
console.print(" [green]✓[/green] Created .deepwork.policy.yml template")
|
|
286
|
+
else:
|
|
287
|
+
console.print(" [dim]•[/dim] .deepwork.policy.yml already exists")
|
|
288
|
+
|
|
241
289
|
# Step 4: Load or create config.yml
|
|
242
290
|
console.print("[yellow]→[/yellow] Updating configuration...")
|
|
243
291
|
config_file = deepwork_dir / "config.yml"
|
deepwork/cli/sync.py
CHANGED
|
@@ -117,6 +117,7 @@ def sync_commands(project_path: Path) -> None:
|
|
|
117
117
|
# Sync each platform
|
|
118
118
|
generator = CommandGenerator()
|
|
119
119
|
stats = {"platforms": 0, "commands": 0, "hooks": 0}
|
|
120
|
+
synced_adapters: list[AgentAdapter] = []
|
|
120
121
|
|
|
121
122
|
for platform_name in platforms:
|
|
122
123
|
try:
|
|
@@ -157,6 +158,7 @@ def sync_commands(project_path: Path) -> None:
|
|
|
157
158
|
console.print(f" [red]✗[/red] Failed to sync hooks: {e}")
|
|
158
159
|
|
|
159
160
|
stats["platforms"] += 1
|
|
161
|
+
synced_adapters.append(adapter)
|
|
160
162
|
|
|
161
163
|
# Summary
|
|
162
164
|
console.print()
|
|
@@ -174,3 +176,10 @@ def sync_commands(project_path: Path) -> None:
|
|
|
174
176
|
|
|
175
177
|
console.print(table)
|
|
176
178
|
console.print()
|
|
179
|
+
|
|
180
|
+
# Show reload instructions for each synced platform
|
|
181
|
+
if synced_adapters and stats["commands"] > 0:
|
|
182
|
+
console.print("[bold]To use the new commands:[/bold]")
|
|
183
|
+
for adapter in synced_adapters:
|
|
184
|
+
console.print(f" [cyan]{adapter.display_name}:[/cyan] {adapter.reload_instructions}")
|
|
185
|
+
console.print()
|
deepwork/core/adapters.py
CHANGED
|
@@ -57,6 +57,12 @@ class AgentAdapter(ABC):
|
|
|
57
57
|
commands_dir: ClassVar[str] = "commands"
|
|
58
58
|
command_template: ClassVar[str] = "command-job-step.md.jinja"
|
|
59
59
|
|
|
60
|
+
# Instructions for reloading commands after sync (shown to users)
|
|
61
|
+
# Subclasses should override with platform-specific instructions.
|
|
62
|
+
reload_instructions: ClassVar[str] = (
|
|
63
|
+
"Restart your AI assistant session to use the new commands."
|
|
64
|
+
)
|
|
65
|
+
|
|
60
66
|
# Mapping from generic CommandLifecycleHook to platform-specific event names.
|
|
61
67
|
# Subclasses should override this to provide platform-specific mappings.
|
|
62
68
|
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
|
|
@@ -253,6 +259,12 @@ class ClaudeAdapter(AgentAdapter):
|
|
|
253
259
|
display_name = "Claude Code"
|
|
254
260
|
config_dir = ".claude"
|
|
255
261
|
|
|
262
|
+
# Claude Code doesn't have a reload command - must restart session
|
|
263
|
+
reload_instructions: ClassVar[str] = (
|
|
264
|
+
"Type 'exit' to leave your current session, then run "
|
|
265
|
+
"'claude --resume' (your history will be maintained)."
|
|
266
|
+
)
|
|
267
|
+
|
|
256
268
|
# Claude Code uses PascalCase event names
|
|
257
269
|
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {
|
|
258
270
|
CommandLifecycleHook.AFTER_AGENT: "Stop",
|
|
@@ -333,6 +345,11 @@ class GeminiAdapter(AgentAdapter):
|
|
|
333
345
|
config_dir = ".gemini"
|
|
334
346
|
command_template = "command-job-step.toml.jinja"
|
|
335
347
|
|
|
348
|
+
# Gemini CLI can reload with /memory refresh
|
|
349
|
+
reload_instructions: ClassVar[str] = (
|
|
350
|
+
"Run '/memory refresh' to reload commands, or restart your Gemini CLI session."
|
|
351
|
+
)
|
|
352
|
+
|
|
336
353
|
# Gemini CLI does NOT support command-level hooks
|
|
337
354
|
# Hooks are global/project-level in settings.json, not per-command
|
|
338
355
|
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
|
deepwork/core/policy_parser.py
CHANGED
|
@@ -17,6 +17,11 @@ class PolicyParseError(Exception):
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
# Valid compare_to values
|
|
21
|
+
COMPARE_TO_VALUES = frozenset({"base", "default_tip", "prompt"})
|
|
22
|
+
DEFAULT_COMPARE_TO = "base"
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
@dataclass
|
|
21
26
|
class Policy:
|
|
22
27
|
"""Represents a single policy definition."""
|
|
@@ -25,6 +30,7 @@ class Policy:
|
|
|
25
30
|
triggers: list[str] # Normalized to list
|
|
26
31
|
safety: list[str] = field(default_factory=list) # Normalized to list, empty if not specified
|
|
27
32
|
instructions: str = "" # Resolved content (either inline or from file)
|
|
33
|
+
compare_to: str = DEFAULT_COMPARE_TO # What to compare against: base, default_tip, or prompt
|
|
28
34
|
|
|
29
35
|
@classmethod
|
|
30
36
|
def from_dict(cls, data: dict[str, Any], base_dir: Path | None = None) -> "Policy":
|
|
@@ -74,11 +80,15 @@ class Policy:
|
|
|
74
80
|
f"Policy '{data['name']}' must have either 'instructions' or 'instructions_file'"
|
|
75
81
|
)
|
|
76
82
|
|
|
83
|
+
# Get compare_to (defaults to DEFAULT_COMPARE_TO)
|
|
84
|
+
compare_to = data.get("compare_to", DEFAULT_COMPARE_TO)
|
|
85
|
+
|
|
77
86
|
return cls(
|
|
78
87
|
name=data["name"],
|
|
79
88
|
triggers=triggers,
|
|
80
89
|
safety=safety,
|
|
81
90
|
instructions=instructions,
|
|
91
|
+
compare_to=compare_to,
|
|
82
92
|
)
|
|
83
93
|
|
|
84
94
|
|
|
@@ -6,12 +6,16 @@ should fire based on changed files and conversation context.
|
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
8
|
python -m deepwork.hooks.evaluate_policies \
|
|
9
|
-
--policy-file .deepwork.policy.yml
|
|
10
|
-
--changed-files "file1.py\nfile2.py"
|
|
9
|
+
--policy-file .deepwork.policy.yml
|
|
11
10
|
|
|
12
11
|
The conversation context is read from stdin and checked for <promise> tags
|
|
13
12
|
that indicate policies have already been addressed.
|
|
14
13
|
|
|
14
|
+
Changed files are computed based on each policy's compare_to setting:
|
|
15
|
+
- base: Compare to merge-base with default branch (default)
|
|
16
|
+
- default_tip: Two-dot diff against default branch tip
|
|
17
|
+
- prompt: Compare to state captured at prompt submission
|
|
18
|
+
|
|
15
19
|
Output is JSON suitable for Claude Code Stop hooks:
|
|
16
20
|
{"decision": "block", "reason": "..."} # Block stop, policies need attention
|
|
17
21
|
{} # No policies fired, allow stop
|
|
@@ -20,16 +24,223 @@ Output is JSON suitable for Claude Code Stop hooks:
|
|
|
20
24
|
import argparse
|
|
21
25
|
import json
|
|
22
26
|
import re
|
|
27
|
+
import subprocess
|
|
23
28
|
import sys
|
|
24
29
|
from pathlib import Path
|
|
25
30
|
|
|
26
31
|
from deepwork.core.policy_parser import (
|
|
32
|
+
Policy,
|
|
27
33
|
PolicyParseError,
|
|
28
|
-
|
|
34
|
+
evaluate_policy,
|
|
29
35
|
parse_policy_file,
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
|
|
39
|
+
def get_default_branch() -> str:
|
|
40
|
+
"""
|
|
41
|
+
Get the default branch name (main or master).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Default branch name, or "main" if cannot be determined.
|
|
45
|
+
"""
|
|
46
|
+
# Try to get the default branch from remote HEAD
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
check=True,
|
|
53
|
+
)
|
|
54
|
+
# Output is like "refs/remotes/origin/main"
|
|
55
|
+
return result.stdout.strip().split("/")[-1]
|
|
56
|
+
except subprocess.CalledProcessError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Try common default branch names
|
|
60
|
+
for branch in ["main", "master"]:
|
|
61
|
+
try:
|
|
62
|
+
subprocess.run(
|
|
63
|
+
["git", "rev-parse", "--verify", f"origin/{branch}"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
check=True,
|
|
66
|
+
)
|
|
67
|
+
return branch
|
|
68
|
+
except subprocess.CalledProcessError:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Fall back to main
|
|
72
|
+
return "main"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_changed_files_base() -> list[str]:
|
|
76
|
+
"""
|
|
77
|
+
Get files changed relative to the base of the current branch.
|
|
78
|
+
|
|
79
|
+
This finds the merge-base between the current branch and the default branch,
|
|
80
|
+
then returns all files changed since that point.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of changed file paths.
|
|
84
|
+
"""
|
|
85
|
+
default_branch = get_default_branch()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Get the merge-base (where current branch diverged from default)
|
|
89
|
+
result = subprocess.run(
|
|
90
|
+
["git", "merge-base", "HEAD", f"origin/{default_branch}"],
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
check=True,
|
|
94
|
+
)
|
|
95
|
+
merge_base = result.stdout.strip()
|
|
96
|
+
|
|
97
|
+
# Stage all changes so they appear in diff
|
|
98
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
99
|
+
|
|
100
|
+
# Get files changed since merge-base (including staged)
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
["git", "diff", "--name-only", merge_base, "HEAD"],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=True,
|
|
106
|
+
)
|
|
107
|
+
committed_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
108
|
+
|
|
109
|
+
# Also get staged changes not yet committed
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
["git", "diff", "--name-only", "--cached"],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
staged_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
117
|
+
|
|
118
|
+
# Get untracked files
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
check=False,
|
|
124
|
+
)
|
|
125
|
+
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
126
|
+
|
|
127
|
+
all_files = committed_files | staged_files | untracked_files
|
|
128
|
+
return sorted([f for f in all_files if f])
|
|
129
|
+
|
|
130
|
+
except subprocess.CalledProcessError:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_changed_files_default_tip() -> list[str]:
|
|
135
|
+
"""
|
|
136
|
+
Get files changed compared to the tip of the default branch.
|
|
137
|
+
|
|
138
|
+
This does a two-dot diff: what's different between HEAD and origin/default.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of changed file paths.
|
|
142
|
+
"""
|
|
143
|
+
default_branch = get_default_branch()
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# Stage all changes so they appear in diff
|
|
147
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
148
|
+
|
|
149
|
+
# Two-dot diff against default branch tip
|
|
150
|
+
result = subprocess.run(
|
|
151
|
+
["git", "diff", "--name-only", f"origin/{default_branch}..HEAD"],
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
check=True,
|
|
155
|
+
)
|
|
156
|
+
committed_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
157
|
+
|
|
158
|
+
# Also get staged changes not yet committed
|
|
159
|
+
result = subprocess.run(
|
|
160
|
+
["git", "diff", "--name-only", "--cached"],
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
check=False,
|
|
164
|
+
)
|
|
165
|
+
staged_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
166
|
+
|
|
167
|
+
# Get untracked files
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
check=False,
|
|
173
|
+
)
|
|
174
|
+
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
175
|
+
|
|
176
|
+
all_files = committed_files | staged_files | untracked_files
|
|
177
|
+
return sorted([f for f in all_files if f])
|
|
178
|
+
|
|
179
|
+
except subprocess.CalledProcessError:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_changed_files_prompt() -> list[str]:
|
|
184
|
+
"""
|
|
185
|
+
Get files changed since the prompt was submitted.
|
|
186
|
+
|
|
187
|
+
This compares against the baseline captured by capture_prompt_work_tree.sh.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of changed file paths.
|
|
191
|
+
"""
|
|
192
|
+
baseline_path = Path(".deepwork/.last_work_tree")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Stage all changes so we can see them with --cached
|
|
196
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
197
|
+
|
|
198
|
+
# Get all staged files (includes what was just staged)
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["git", "diff", "--name-only", "--cached"],
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
check=False,
|
|
204
|
+
)
|
|
205
|
+
current_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
206
|
+
current_files = {f for f in current_files if f}
|
|
207
|
+
|
|
208
|
+
if baseline_path.exists():
|
|
209
|
+
# Read baseline and find new files
|
|
210
|
+
baseline_files = set(baseline_path.read_text().strip().split("\n"))
|
|
211
|
+
baseline_files = {f for f in baseline_files if f}
|
|
212
|
+
# Return files that are in current but not in baseline
|
|
213
|
+
new_files = current_files - baseline_files
|
|
214
|
+
return sorted(new_files)
|
|
215
|
+
else:
|
|
216
|
+
# No baseline, return all current changes
|
|
217
|
+
return sorted(current_files)
|
|
218
|
+
|
|
219
|
+
except (subprocess.CalledProcessError, OSError):
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_changed_files_for_mode(mode: str) -> list[str]:
|
|
224
|
+
"""
|
|
225
|
+
Get changed files for a specific compare_to mode.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
mode: One of 'base', 'default_tip', or 'prompt'
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of changed file paths.
|
|
232
|
+
"""
|
|
233
|
+
if mode == "base":
|
|
234
|
+
return get_changed_files_base()
|
|
235
|
+
elif mode == "default_tip":
|
|
236
|
+
return get_changed_files_default_tip()
|
|
237
|
+
elif mode == "prompt":
|
|
238
|
+
return get_changed_files_prompt()
|
|
239
|
+
else:
|
|
240
|
+
# Unknown mode, fall back to base
|
|
241
|
+
return get_changed_files_base()
|
|
242
|
+
|
|
243
|
+
|
|
33
244
|
def extract_promise_tags(text: str) -> set[str]:
|
|
34
245
|
"""
|
|
35
246
|
Extract policy names from <promise> tags in text.
|
|
@@ -87,23 +298,9 @@ def main() -> None:
|
|
|
87
298
|
required=True,
|
|
88
299
|
help="Path to .deepwork.policy.yml file",
|
|
89
300
|
)
|
|
90
|
-
parser.add_argument(
|
|
91
|
-
"--changed-files",
|
|
92
|
-
type=str,
|
|
93
|
-
required=True,
|
|
94
|
-
help="Newline-separated list of changed files",
|
|
95
|
-
)
|
|
96
301
|
|
|
97
302
|
args = parser.parse_args()
|
|
98
303
|
|
|
99
|
-
# Parse changed files (newline-separated)
|
|
100
|
-
changed_files = [f.strip() for f in args.changed_files.split("\n") if f.strip()]
|
|
101
|
-
|
|
102
|
-
if not changed_files:
|
|
103
|
-
# No files changed, nothing to evaluate
|
|
104
|
-
print("{}")
|
|
105
|
-
return
|
|
106
|
-
|
|
107
304
|
# Check if policy file exists
|
|
108
305
|
policy_path = Path(args.policy_file)
|
|
109
306
|
if not policy_path.exists():
|
|
@@ -122,7 +319,7 @@ def main() -> None:
|
|
|
122
319
|
# Extract promise tags from conversation
|
|
123
320
|
promised_policies = extract_promise_tags(conversation_context)
|
|
124
321
|
|
|
125
|
-
# Parse
|
|
322
|
+
# Parse policies
|
|
126
323
|
try:
|
|
127
324
|
policies = parse_policy_file(policy_path)
|
|
128
325
|
except PolicyParseError as e:
|
|
@@ -136,8 +333,28 @@ def main() -> None:
|
|
|
136
333
|
print("{}")
|
|
137
334
|
return
|
|
138
335
|
|
|
139
|
-
#
|
|
140
|
-
|
|
336
|
+
# Group policies by compare_to mode to minimize git calls
|
|
337
|
+
policies_by_mode: dict[str, list[Policy]] = {}
|
|
338
|
+
for policy in policies:
|
|
339
|
+
mode = policy.compare_to
|
|
340
|
+
if mode not in policies_by_mode:
|
|
341
|
+
policies_by_mode[mode] = []
|
|
342
|
+
policies_by_mode[mode].append(policy)
|
|
343
|
+
|
|
344
|
+
# Get changed files for each mode and evaluate policies
|
|
345
|
+
fired_policies: list[Policy] = []
|
|
346
|
+
for mode, mode_policies in policies_by_mode.items():
|
|
347
|
+
changed_files = get_changed_files_for_mode(mode)
|
|
348
|
+
if not changed_files:
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
for policy in mode_policies:
|
|
352
|
+
# Skip if already promised
|
|
353
|
+
if policy.name in promised_policies:
|
|
354
|
+
continue
|
|
355
|
+
# Evaluate this policy
|
|
356
|
+
if evaluate_policy(policy, changed_files):
|
|
357
|
+
fired_policies.append(policy)
|
|
141
358
|
|
|
142
359
|
if not fired_policies:
|
|
143
360
|
# No policies fired
|
|
@@ -58,6 +58,16 @@ POLICY_SCHEMA: dict[str, Any] = {
|
|
|
58
58
|
"minLength": 1,
|
|
59
59
|
"description": "Path to a file containing instructions (alternative to inline instructions)",
|
|
60
60
|
},
|
|
61
|
+
"compare_to": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"enum": ["base", "default_tip", "prompt"],
|
|
64
|
+
"description": (
|
|
65
|
+
"What to compare against when detecting changed files. "
|
|
66
|
+
"'base' (default) compares to the base of the current branch. "
|
|
67
|
+
"'default_tip' compares to the tip of the default branch. "
|
|
68
|
+
"'prompt' compares to the state at the start of the prompt."
|
|
69
|
+
),
|
|
70
|
+
},
|
|
61
71
|
},
|
|
62
72
|
"oneOf": [
|
|
63
73
|
{"required": ["instructions"]},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Project Context for deepwork_jobs
|
|
2
|
+
|
|
3
|
+
This is the source of truth for the `deepwork_jobs` standard job.
|
|
4
|
+
|
|
5
|
+
## Codebase Structure
|
|
6
|
+
|
|
7
|
+
- Source location: `src/deepwork/standard_jobs/deepwork_jobs/`
|
|
8
|
+
- Working copy: `.deepwork/jobs/deepwork_jobs/`
|
|
9
|
+
- Templates: `templates/` directory within each location
|
|
10
|
+
|
|
11
|
+
## Dual Location Maintenance
|
|
12
|
+
|
|
13
|
+
**Important**: This job exists in two locations that must be kept in sync:
|
|
14
|
+
|
|
15
|
+
1. **Source of truth**: `src/deepwork/standard_jobs/deepwork_jobs/`
|
|
16
|
+
- This is where changes should be made first
|
|
17
|
+
- Tracked in version control
|
|
18
|
+
|
|
19
|
+
2. **Working copy**: `.deepwork/jobs/deepwork_jobs/`
|
|
20
|
+
- Must be updated after changes to source
|
|
21
|
+
- Used by `deepwork sync` to generate commands
|
|
22
|
+
|
|
23
|
+
After making changes to the source, copy files to the working copy:
|
|
24
|
+
```bash
|
|
25
|
+
cp src/deepwork/standard_jobs/deepwork_jobs/job.yml .deepwork/jobs/deepwork_jobs/
|
|
26
|
+
cp src/deepwork/standard_jobs/deepwork_jobs/steps/*.md .deepwork/jobs/deepwork_jobs/steps/
|
|
27
|
+
cp -r src/deepwork/standard_jobs/deepwork_jobs/templates/* .deepwork/jobs/deepwork_jobs/templates/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## File Organization
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
deepwork_jobs/
|
|
34
|
+
├── AGENTS.md # This file
|
|
35
|
+
├── job.yml # Job definition
|
|
36
|
+
├── make_new_job.sh # Script to create new job structure
|
|
37
|
+
├── steps/
|
|
38
|
+
│ ├── define.md # Define step instructions
|
|
39
|
+
│ ├── implement.md # Implement step instructions
|
|
40
|
+
│ ├── learn.md # Learn step instructions
|
|
41
|
+
│ └── supplemental_file_references.md # Reference documentation
|
|
42
|
+
└── templates/
|
|
43
|
+
├── job.yml.template # Job spec structure
|
|
44
|
+
├── step_instruction.md.template # Step instruction structure
|
|
45
|
+
├── agents.md.template # AGENTS.md structure
|
|
46
|
+
├── job.yml.example # Complete job example
|
|
47
|
+
└── step_instruction.md.example # Complete step example
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Version Management
|
|
51
|
+
|
|
52
|
+
- Version is tracked in `job.yml`
|
|
53
|
+
- Bump patch version (0.0.x) for instruction improvements
|
|
54
|
+
- Bump minor version (0.x.0) for new features or structural changes
|
|
55
|
+
- Always update changelog when bumping version
|
|
56
|
+
|
|
57
|
+
## Last Updated
|
|
58
|
+
|
|
59
|
+
- Date: 2026-01-15
|
|
60
|
+
- From conversation about: Adding make_new_job.sh script and templates directory
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
name: deepwork_jobs
|
|
2
|
-
version: "0.
|
|
2
|
+
version: "0.4.0"
|
|
3
3
|
summary: "DeepWork job management commands"
|
|
4
4
|
description: |
|
|
5
5
|
Core commands for managing DeepWork jobs. These commands help you define new multi-step
|
|
6
|
-
workflows and
|
|
6
|
+
workflows and learn from running them.
|
|
7
7
|
|
|
8
8
|
The `define` command guides you through an interactive process to create a new job by
|
|
9
9
|
asking detailed questions about your workflow, understanding each step's inputs and outputs,
|
|
10
10
|
and generating all necessary files.
|
|
11
11
|
|
|
12
|
-
The `
|
|
13
|
-
|
|
12
|
+
The `learn` command reflects on conversations where DeepWork jobs were run, identifies
|
|
13
|
+
confusion or inefficiencies, and improves job instructions. It also captures bespoke
|
|
14
|
+
learnings specific to the current run into AGENTS.md files in the working folder.
|
|
14
15
|
|
|
15
16
|
changelog:
|
|
16
17
|
- version: "0.1.0"
|
|
17
18
|
changes: "Initial version"
|
|
19
|
+
- version: "0.2.0"
|
|
20
|
+
changes: "Replaced refine command with learn command for conversation-driven improvement"
|
|
21
|
+
- version: "0.3.0"
|
|
22
|
+
changes: "Added make_new_job.sh script and templates directory; updated instructions to reference templates instead of inline examples"
|
|
23
|
+
- version: "0.4.0"
|
|
24
|
+
changes: "Removed implementation_summary and learning_summary outputs; simplified step outputs"
|
|
18
25
|
|
|
19
26
|
steps:
|
|
20
27
|
- id: define
|
|
@@ -51,7 +58,7 @@ steps:
|
|
|
51
58
|
- file: job.yml
|
|
52
59
|
from_step: define
|
|
53
60
|
outputs:
|
|
54
|
-
-
|
|
61
|
+
- steps/
|
|
55
62
|
dependencies:
|
|
56
63
|
- define
|
|
57
64
|
hooks:
|
|
@@ -66,37 +73,38 @@ steps:
|
|
|
66
73
|
5. **Quality Criteria**: Does each instruction file define quality criteria for its outputs?
|
|
67
74
|
6. **Sync Complete**: Has `deepwork sync` been run successfully?
|
|
68
75
|
7. **Commands Available**: Are the slash-commands generated in `.claude/commands/`?
|
|
69
|
-
8. **
|
|
70
|
-
9. **Policies Considered**: Have you thought about whether policies would benefit this job?
|
|
76
|
+
8. **Policies Considered**: Have you thought about whether policies would benefit this job?
|
|
71
77
|
- If relevant policies were identified, did you explain them and offer to run `/deepwork_policy.define`?
|
|
72
78
|
- Not every job needs policies - only suggest when genuinely helpful.
|
|
73
79
|
|
|
74
80
|
If ANY criterion is not met, continue working to address it.
|
|
75
81
|
If ALL criteria are satisfied, include `<promise>✓ Quality Criteria Met</promise>` in your response.
|
|
76
82
|
|
|
77
|
-
- id:
|
|
78
|
-
name: "
|
|
79
|
-
description: "
|
|
80
|
-
instructions_file: steps/
|
|
83
|
+
- id: learn
|
|
84
|
+
name: "Learn from Job Execution"
|
|
85
|
+
description: "Reflect on conversation to improve job instructions and capture learnings"
|
|
86
|
+
instructions_file: steps/learn.md
|
|
81
87
|
inputs:
|
|
82
88
|
- name: job_name
|
|
83
|
-
description: "Name of the job
|
|
89
|
+
description: "Name of the job that was run (optional - will auto-detect from conversation)"
|
|
84
90
|
outputs:
|
|
85
|
-
-
|
|
91
|
+
- AGENTS.md
|
|
86
92
|
dependencies: []
|
|
87
93
|
hooks:
|
|
88
94
|
after_agent:
|
|
89
95
|
- prompt: |
|
|
90
|
-
Verify the
|
|
96
|
+
Verify the learning process meets ALL quality criteria before completing:
|
|
91
97
|
|
|
92
|
-
1. **
|
|
93
|
-
2. **
|
|
94
|
-
3. **
|
|
95
|
-
4. **
|
|
96
|
-
5. **
|
|
97
|
-
6. **
|
|
98
|
-
7. **
|
|
99
|
-
8. **
|
|
98
|
+
1. **Conversation Analyzed**: Did you review the conversation for DeepWork job executions?
|
|
99
|
+
2. **Confusion Identified**: Did you identify points of confusion, errors, or inefficiencies?
|
|
100
|
+
3. **Instructions Improved**: Were job instructions updated to address identified issues?
|
|
101
|
+
4. **Instructions Concise**: Are instructions free of redundancy and unnecessary verbosity?
|
|
102
|
+
5. **Shared Content Extracted**: Is lengthy/duplicated content extracted into referenced files?
|
|
103
|
+
6. **Bespoke Learnings Captured**: Were run-specific learnings added to AGENTS.md?
|
|
104
|
+
7. **File References Used**: Do AGENTS.md entries reference other files where appropriate?
|
|
105
|
+
8. **Working Folder Correct**: Is AGENTS.md in the correct working folder for the job?
|
|
106
|
+
9. **Generalizable Separated**: Are generalizable improvements in instructions, not AGENTS.md?
|
|
107
|
+
10. **Sync Complete**: Has `deepwork sync` been run if instructions were modified?
|
|
100
108
|
|
|
101
109
|
If ANY criterion is not met, continue working to address it.
|
|
102
110
|
If ALL criteria are satisfied, include `<promise>✓ Quality Criteria Met</promise>` in your response.
|