deepwork 0.2.0__py3-none-any.whl → 0.3.1__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 +116 -71
- deepwork/cli/sync.py +20 -20
- deepwork/core/adapters.py +88 -51
- deepwork/core/command_executor.py +173 -0
- deepwork/core/generator.py +148 -31
- deepwork/core/hooks_syncer.py +51 -25
- deepwork/core/parser.py +8 -0
- deepwork/core/pattern_matcher.py +271 -0
- deepwork/core/rules_parser.py +559 -0
- deepwork/core/rules_queue.py +321 -0
- deepwork/hooks/README.md +181 -0
- deepwork/hooks/__init__.py +77 -1
- deepwork/hooks/claude_hook.sh +55 -0
- deepwork/hooks/gemini_hook.sh +55 -0
- deepwork/hooks/rules_check.py +700 -0
- deepwork/hooks/wrapper.py +363 -0
- deepwork/schemas/job_schema.py +14 -1
- deepwork/schemas/rules_schema.py +135 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +35 -53
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +9 -6
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +28 -26
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +2 -2
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +30 -0
- deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +47 -0
- deepwork/standard_jobs/deepwork_rules/rules/.gitkeep +13 -0
- deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example +10 -0
- deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example +10 -0
- deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example +11 -0
- deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +46 -0
- deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example +13 -0
- deepwork/standard_jobs/deepwork_rules/steps/define.md +249 -0
- deepwork/templates/claude/skill-job-meta.md.jinja +70 -0
- deepwork/templates/claude/skill-job-step.md.jinja +198 -0
- deepwork/templates/gemini/skill-job-meta.toml.jinja +76 -0
- deepwork/templates/gemini/skill-job-step.toml.jinja +147 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/METADATA +56 -25
- deepwork-0.3.1.dist-info/RECORD +62 -0
- deepwork/core/policy_parser.py +0 -295
- deepwork/hooks/evaluate_policies.py +0 -376
- deepwork/schemas/policy_schema.py +0 -78
- deepwork/standard_jobs/deepwork_policy/hooks/capture_prompt_work_tree.sh +0 -27
- deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +0 -8
- deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +0 -56
- deepwork/standard_jobs/deepwork_policy/job.yml +0 -35
- deepwork/standard_jobs/deepwork_policy/steps/define.md +0 -195
- deepwork/templates/claude/command-job-step.md.jinja +0 -210
- deepwork/templates/default_policy.yml +0 -53
- deepwork/templates/gemini/command-job-step.toml.jinja +0 -169
- deepwork-0.2.0.dist-info/RECORD +0 -49
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/WHEEL +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/entry_points.txt +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook wrapper module for cross-platform hook compatibility.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for normalizing hook input/output between
|
|
5
|
+
different AI CLI platforms (Claude Code, Gemini CLI, etc.).
|
|
6
|
+
|
|
7
|
+
The wrapper system allows writing hooks once in Python and running them
|
|
8
|
+
on any supported platform. Platform-specific shell scripts handle the
|
|
9
|
+
input/output translation, while Python hooks work with a normalized format.
|
|
10
|
+
|
|
11
|
+
Normalized Format:
|
|
12
|
+
Input:
|
|
13
|
+
- session_id: str
|
|
14
|
+
- transcript_path: str
|
|
15
|
+
- cwd: str
|
|
16
|
+
- event: str (normalized: 'after_agent', 'before_tool', 'before_prompt')
|
|
17
|
+
- tool_name: str (normalized: 'write_file', 'shell', etc.)
|
|
18
|
+
- tool_input: dict
|
|
19
|
+
- prompt: str (for agent events)
|
|
20
|
+
- raw_input: dict (original platform-specific input)
|
|
21
|
+
|
|
22
|
+
Output:
|
|
23
|
+
- decision: str ('block', 'allow', 'deny')
|
|
24
|
+
- reason: str (explanation for blocking)
|
|
25
|
+
- context: str (additional context to add)
|
|
26
|
+
- raw_output: dict (will be merged into final output)
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
# In a Python hook:
|
|
30
|
+
from deepwork.hooks.wrapper import HookInput, HookOutput, normalize_input, denormalize_output
|
|
31
|
+
|
|
32
|
+
def my_hook(input_data: HookInput) -> HookOutput:
|
|
33
|
+
if should_block:
|
|
34
|
+
return HookOutput(decision='block', reason='Must do X first')
|
|
35
|
+
return HookOutput() # Allow
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import sys
|
|
42
|
+
from collections.abc import Callable
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from enum import Enum
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Platform(str, Enum):
|
|
49
|
+
"""Supported AI CLI platforms."""
|
|
50
|
+
|
|
51
|
+
CLAUDE = "claude"
|
|
52
|
+
GEMINI = "gemini"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NormalizedEvent(str, Enum):
|
|
56
|
+
"""Normalized hook event names."""
|
|
57
|
+
|
|
58
|
+
AFTER_AGENT = "after_agent"
|
|
59
|
+
BEFORE_TOOL = "before_tool"
|
|
60
|
+
BEFORE_PROMPT = "before_prompt"
|
|
61
|
+
SESSION_START = "session_start"
|
|
62
|
+
SESSION_END = "session_end"
|
|
63
|
+
AFTER_TOOL = "after_tool"
|
|
64
|
+
BEFORE_MODEL = "before_model"
|
|
65
|
+
AFTER_MODEL = "after_model"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Event name mappings from platform-specific to normalized
|
|
69
|
+
EVENT_TO_NORMALIZED: dict[Platform, dict[str, NormalizedEvent]] = {
|
|
70
|
+
Platform.CLAUDE: {
|
|
71
|
+
"Stop": NormalizedEvent.AFTER_AGENT,
|
|
72
|
+
"SubagentStop": NormalizedEvent.AFTER_AGENT,
|
|
73
|
+
"PreToolUse": NormalizedEvent.BEFORE_TOOL,
|
|
74
|
+
"PostToolUse": NormalizedEvent.AFTER_TOOL,
|
|
75
|
+
"UserPromptSubmit": NormalizedEvent.BEFORE_PROMPT,
|
|
76
|
+
"SessionStart": NormalizedEvent.SESSION_START,
|
|
77
|
+
"SessionEnd": NormalizedEvent.SESSION_END,
|
|
78
|
+
},
|
|
79
|
+
Platform.GEMINI: {
|
|
80
|
+
"AfterAgent": NormalizedEvent.AFTER_AGENT,
|
|
81
|
+
"BeforeTool": NormalizedEvent.BEFORE_TOOL,
|
|
82
|
+
"AfterTool": NormalizedEvent.AFTER_TOOL,
|
|
83
|
+
"BeforeAgent": NormalizedEvent.BEFORE_PROMPT,
|
|
84
|
+
"SessionStart": NormalizedEvent.SESSION_START,
|
|
85
|
+
"SessionEnd": NormalizedEvent.SESSION_END,
|
|
86
|
+
"BeforeModel": NormalizedEvent.BEFORE_MODEL,
|
|
87
|
+
"AfterModel": NormalizedEvent.AFTER_MODEL,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Normalized event to platform-specific event name
|
|
92
|
+
NORMALIZED_TO_EVENT: dict[Platform, dict[NormalizedEvent, str]] = {
|
|
93
|
+
Platform.CLAUDE: {
|
|
94
|
+
NormalizedEvent.AFTER_AGENT: "Stop",
|
|
95
|
+
NormalizedEvent.BEFORE_TOOL: "PreToolUse",
|
|
96
|
+
NormalizedEvent.AFTER_TOOL: "PostToolUse",
|
|
97
|
+
NormalizedEvent.BEFORE_PROMPT: "UserPromptSubmit",
|
|
98
|
+
NormalizedEvent.SESSION_START: "SessionStart",
|
|
99
|
+
NormalizedEvent.SESSION_END: "SessionEnd",
|
|
100
|
+
},
|
|
101
|
+
Platform.GEMINI: {
|
|
102
|
+
NormalizedEvent.AFTER_AGENT: "AfterAgent",
|
|
103
|
+
NormalizedEvent.BEFORE_TOOL: "BeforeTool",
|
|
104
|
+
NormalizedEvent.AFTER_TOOL: "AfterTool",
|
|
105
|
+
NormalizedEvent.BEFORE_PROMPT: "BeforeAgent",
|
|
106
|
+
NormalizedEvent.SESSION_START: "SessionStart",
|
|
107
|
+
NormalizedEvent.SESSION_END: "SessionEnd",
|
|
108
|
+
NormalizedEvent.BEFORE_MODEL: "BeforeModel",
|
|
109
|
+
NormalizedEvent.AFTER_MODEL: "AfterModel",
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Tool name mappings from platform-specific to normalized (snake_case)
|
|
114
|
+
TOOL_TO_NORMALIZED: dict[Platform, dict[str, str]] = {
|
|
115
|
+
Platform.CLAUDE: {
|
|
116
|
+
"Write": "write_file",
|
|
117
|
+
"Edit": "edit_file",
|
|
118
|
+
"Read": "read_file",
|
|
119
|
+
"Bash": "shell",
|
|
120
|
+
"Glob": "glob",
|
|
121
|
+
"Grep": "grep",
|
|
122
|
+
"WebFetch": "web_fetch",
|
|
123
|
+
"WebSearch": "web_search",
|
|
124
|
+
"Task": "task",
|
|
125
|
+
},
|
|
126
|
+
Platform.GEMINI: {
|
|
127
|
+
# Gemini already uses snake_case
|
|
128
|
+
"write_file": "write_file",
|
|
129
|
+
"edit_file": "edit_file",
|
|
130
|
+
"read_file": "read_file",
|
|
131
|
+
"shell": "shell",
|
|
132
|
+
"glob": "glob",
|
|
133
|
+
"grep": "grep",
|
|
134
|
+
"web_fetch": "web_fetch",
|
|
135
|
+
"web_search": "web_search",
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Normalized tool names to platform-specific
|
|
140
|
+
NORMALIZED_TO_TOOL: dict[Platform, dict[str, str]] = {
|
|
141
|
+
Platform.CLAUDE: {
|
|
142
|
+
"write_file": "Write",
|
|
143
|
+
"edit_file": "Edit",
|
|
144
|
+
"read_file": "Read",
|
|
145
|
+
"shell": "Bash",
|
|
146
|
+
"glob": "Glob",
|
|
147
|
+
"grep": "Grep",
|
|
148
|
+
"web_fetch": "WebFetch",
|
|
149
|
+
"web_search": "WebSearch",
|
|
150
|
+
"task": "Task",
|
|
151
|
+
},
|
|
152
|
+
Platform.GEMINI: {
|
|
153
|
+
# Gemini already uses snake_case
|
|
154
|
+
"write_file": "write_file",
|
|
155
|
+
"edit_file": "edit_file",
|
|
156
|
+
"read_file": "read_file",
|
|
157
|
+
"shell": "shell",
|
|
158
|
+
"glob": "glob",
|
|
159
|
+
"grep": "grep",
|
|
160
|
+
"web_fetch": "web_fetch",
|
|
161
|
+
"web_search": "web_search",
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class HookInput:
|
|
168
|
+
"""Normalized hook input data."""
|
|
169
|
+
|
|
170
|
+
platform: Platform
|
|
171
|
+
event: NormalizedEvent
|
|
172
|
+
session_id: str = ""
|
|
173
|
+
transcript_path: str = ""
|
|
174
|
+
cwd: str = ""
|
|
175
|
+
tool_name: str = ""
|
|
176
|
+
tool_input: dict[str, Any] = field(default_factory=dict)
|
|
177
|
+
tool_response: str = ""
|
|
178
|
+
prompt: str = ""
|
|
179
|
+
raw_input: dict[str, Any] = field(default_factory=dict)
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_dict(cls, data: dict[str, Any], platform: Platform) -> HookInput:
|
|
183
|
+
"""Create HookInput from raw platform-specific input."""
|
|
184
|
+
# Get event name and normalize
|
|
185
|
+
raw_event = data.get("hook_event_name", "")
|
|
186
|
+
event_map = EVENT_TO_NORMALIZED.get(platform, {})
|
|
187
|
+
event = event_map.get(raw_event, NormalizedEvent.AFTER_AGENT)
|
|
188
|
+
|
|
189
|
+
# Get tool name and normalize
|
|
190
|
+
raw_tool = data.get("tool_name", "")
|
|
191
|
+
tool_map = TOOL_TO_NORMALIZED.get(platform, {})
|
|
192
|
+
tool_name = tool_map.get(raw_tool, raw_tool.lower())
|
|
193
|
+
|
|
194
|
+
return cls(
|
|
195
|
+
platform=platform,
|
|
196
|
+
event=event,
|
|
197
|
+
session_id=data.get("session_id", ""),
|
|
198
|
+
transcript_path=data.get("transcript_path", ""),
|
|
199
|
+
cwd=data.get("cwd", ""),
|
|
200
|
+
tool_name=tool_name,
|
|
201
|
+
tool_input=data.get("tool_input", {}),
|
|
202
|
+
tool_response=data.get("tool_response", ""),
|
|
203
|
+
prompt=data.get("prompt", ""),
|
|
204
|
+
raw_input=data,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class HookOutput:
|
|
210
|
+
"""Normalized hook output data."""
|
|
211
|
+
|
|
212
|
+
decision: str = "" # 'block', 'allow', 'deny', '' (empty = allow)
|
|
213
|
+
reason: str = "" # Explanation for blocking
|
|
214
|
+
context: str = "" # Additional context to add
|
|
215
|
+
continue_loop: bool = True # False to terminate agent loop
|
|
216
|
+
stop_reason: str = "" # Message when stopping
|
|
217
|
+
suppress_output: bool = False # Hide from transcript
|
|
218
|
+
raw_output: dict[str, Any] = field(default_factory=dict)
|
|
219
|
+
|
|
220
|
+
def to_dict(self, platform: Platform, event: NormalizedEvent) -> dict[str, Any]:
|
|
221
|
+
"""Convert to platform-specific output format."""
|
|
222
|
+
result: dict[str, Any] = {}
|
|
223
|
+
|
|
224
|
+
# Handle decision
|
|
225
|
+
if self.decision:
|
|
226
|
+
if platform == Platform.GEMINI and self.decision == "block":
|
|
227
|
+
# Gemini prefers 'deny'
|
|
228
|
+
result["decision"] = "deny"
|
|
229
|
+
else:
|
|
230
|
+
result["decision"] = self.decision
|
|
231
|
+
|
|
232
|
+
# Handle reason
|
|
233
|
+
if self.reason:
|
|
234
|
+
result["reason"] = self.reason
|
|
235
|
+
|
|
236
|
+
# Handle continue_loop
|
|
237
|
+
if not self.continue_loop:
|
|
238
|
+
result["continue"] = False
|
|
239
|
+
if self.stop_reason:
|
|
240
|
+
result["stopReason"] = self.stop_reason
|
|
241
|
+
|
|
242
|
+
# Handle suppress_output
|
|
243
|
+
if self.suppress_output:
|
|
244
|
+
result["suppressOutput"] = True
|
|
245
|
+
|
|
246
|
+
# Handle context (platform-specific)
|
|
247
|
+
if self.context:
|
|
248
|
+
if platform == Platform.CLAUDE:
|
|
249
|
+
# Claude uses different fields depending on event
|
|
250
|
+
if event == NormalizedEvent.SESSION_START:
|
|
251
|
+
result.setdefault("hookSpecificOutput", {})
|
|
252
|
+
result["hookSpecificOutput"]["hookEventName"] = NORMALIZED_TO_EVENT[platform][
|
|
253
|
+
event
|
|
254
|
+
]
|
|
255
|
+
result["hookSpecificOutput"]["additionalContext"] = self.context
|
|
256
|
+
else:
|
|
257
|
+
result["systemMessage"] = self.context
|
|
258
|
+
else:
|
|
259
|
+
# Gemini
|
|
260
|
+
result.setdefault("hookSpecificOutput", {})
|
|
261
|
+
result["hookSpecificOutput"]["hookEventName"] = NORMALIZED_TO_EVENT[platform].get(
|
|
262
|
+
event, str(event)
|
|
263
|
+
)
|
|
264
|
+
result["hookSpecificOutput"]["additionalContext"] = self.context
|
|
265
|
+
|
|
266
|
+
# Merge any raw output
|
|
267
|
+
for key, value in self.raw_output.items():
|
|
268
|
+
if key not in result:
|
|
269
|
+
result[key] = value
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def normalize_input(raw_json: str, platform: Platform) -> HookInput:
|
|
275
|
+
"""
|
|
276
|
+
Parse raw JSON input and normalize it.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
raw_json: JSON string from stdin
|
|
280
|
+
platform: Source platform
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Normalized HookInput
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
data = json.loads(raw_json) if raw_json.strip() else {}
|
|
287
|
+
except json.JSONDecodeError:
|
|
288
|
+
data = {}
|
|
289
|
+
|
|
290
|
+
return HookInput.from_dict(data, platform)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def denormalize_output(output: HookOutput, platform: Platform, event: NormalizedEvent) -> str:
|
|
294
|
+
"""
|
|
295
|
+
Convert normalized output to platform-specific JSON.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
output: Normalized HookOutput
|
|
299
|
+
platform: Target platform
|
|
300
|
+
event: The event being processed
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
JSON string for stdout
|
|
304
|
+
"""
|
|
305
|
+
result = output.to_dict(platform, event)
|
|
306
|
+
return json.dumps(result) if result else "{}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def read_stdin() -> str:
|
|
310
|
+
"""Read all input from stdin."""
|
|
311
|
+
if sys.stdin.isatty():
|
|
312
|
+
return ""
|
|
313
|
+
try:
|
|
314
|
+
return sys.stdin.read()
|
|
315
|
+
except Exception:
|
|
316
|
+
return ""
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def write_stdout(data: str) -> None:
|
|
320
|
+
"""Write output to stdout."""
|
|
321
|
+
print(data)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def run_hook(
|
|
325
|
+
hook_fn: Callable[[HookInput], HookOutput],
|
|
326
|
+
platform: Platform,
|
|
327
|
+
) -> int:
|
|
328
|
+
"""
|
|
329
|
+
Run a hook function with normalized input/output.
|
|
330
|
+
|
|
331
|
+
This is the main entry point for Python hooks. It:
|
|
332
|
+
1. Reads raw input from stdin
|
|
333
|
+
2. Normalizes the input
|
|
334
|
+
3. Calls the hook function
|
|
335
|
+
4. Denormalizes the output
|
|
336
|
+
5. Writes to stdout
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
hook_fn: Function that takes HookInput and returns HookOutput
|
|
340
|
+
platform: The platform calling this hook
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Exit code (0 for success, 2 for blocking)
|
|
344
|
+
"""
|
|
345
|
+
# Read and normalize input
|
|
346
|
+
raw_input = read_stdin()
|
|
347
|
+
hook_input = normalize_input(raw_input, platform)
|
|
348
|
+
|
|
349
|
+
# Call the hook
|
|
350
|
+
try:
|
|
351
|
+
hook_output = hook_fn(hook_input)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
# On error, allow the action but log
|
|
354
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
355
|
+
hook_output = HookOutput()
|
|
356
|
+
|
|
357
|
+
# Denormalize and write output
|
|
358
|
+
output_json = denormalize_output(hook_output, platform, hook_input.event)
|
|
359
|
+
write_stdout(output_json)
|
|
360
|
+
|
|
361
|
+
# Always return 0 when using JSON output format
|
|
362
|
+
# The decision field in the JSON controls blocking behavior
|
|
363
|
+
return 0
|
deepwork/schemas/job_schema.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
# Supported lifecycle hook events (generic names, mapped to platform-specific by adapters)
|
|
6
|
-
# These values must match
|
|
6
|
+
# These values must match SkillLifecycleHook enum in adapters.py
|
|
7
7
|
LIFECYCLE_HOOK_EVENTS = ["after_agent", "before_tool", "before_prompt"]
|
|
8
8
|
|
|
9
9
|
# Schema definition for a single hook action (prompt, prompt_file, or script)
|
|
@@ -203,6 +203,19 @@ JOB_SCHEMA: dict[str, Any] = {
|
|
|
203
203
|
"description": "DEPRECATED: Use hooks.after_agent instead. Stop hooks for quality validation loops.",
|
|
204
204
|
"items": HOOK_ACTION_SCHEMA,
|
|
205
205
|
},
|
|
206
|
+
"exposed": {
|
|
207
|
+
"type": "boolean",
|
|
208
|
+
"description": "If true, skill is user-invocable in menus. Default: false (hidden from menus).",
|
|
209
|
+
"default": False,
|
|
210
|
+
},
|
|
211
|
+
"quality_criteria": {
|
|
212
|
+
"type": "array",
|
|
213
|
+
"description": "Declarative quality criteria. Rendered with standard evaluation framing.",
|
|
214
|
+
"items": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"minLength": 1,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
206
219
|
},
|
|
207
220
|
"additionalProperties": False,
|
|
208
221
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""JSON Schema definition for rule definitions (v2 - frontmatter format)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
# Pattern for string or array of strings
|
|
6
|
+
STRING_OR_ARRAY: dict[str, Any] = {
|
|
7
|
+
"oneOf": [
|
|
8
|
+
{"type": "string", "minLength": 1},
|
|
9
|
+
{"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1},
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# JSON Schema for rule frontmatter (YAML between --- delimiters)
|
|
14
|
+
# Rules are stored as individual .md files in .deepwork/rules/
|
|
15
|
+
RULES_FRONTMATTER_SCHEMA: dict[str, Any] = {
|
|
16
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["name", "compare_to"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Human-friendly name for the rule (displayed in promise tags)",
|
|
24
|
+
},
|
|
25
|
+
# Detection mode: trigger/safety (mutually exclusive with set/pair)
|
|
26
|
+
"trigger": {
|
|
27
|
+
**STRING_OR_ARRAY,
|
|
28
|
+
"description": "Glob pattern(s) for files that trigger this rule",
|
|
29
|
+
},
|
|
30
|
+
"safety": {
|
|
31
|
+
**STRING_OR_ARRAY,
|
|
32
|
+
"description": "Glob pattern(s) that suppress the rule if changed",
|
|
33
|
+
},
|
|
34
|
+
# Detection mode: set (bidirectional correspondence)
|
|
35
|
+
"set": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {"type": "string", "minLength": 1},
|
|
38
|
+
"minItems": 2,
|
|
39
|
+
"description": "Patterns defining bidirectional file correspondence",
|
|
40
|
+
},
|
|
41
|
+
# Detection mode: pair (directional correspondence)
|
|
42
|
+
"pair": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["trigger", "expects"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"trigger": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"description": "Pattern that triggers the rule",
|
|
50
|
+
},
|
|
51
|
+
"expects": {
|
|
52
|
+
**STRING_OR_ARRAY,
|
|
53
|
+
"description": "Pattern(s) for expected corresponding files",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": False,
|
|
57
|
+
"description": "Directional file correspondence (trigger -> expects)",
|
|
58
|
+
},
|
|
59
|
+
# Detection mode: created (fire when files are created matching patterns)
|
|
60
|
+
"created": {
|
|
61
|
+
**STRING_OR_ARRAY,
|
|
62
|
+
"description": "Glob pattern(s) for newly created files that trigger this rule",
|
|
63
|
+
},
|
|
64
|
+
# Action type: command (default is prompt using markdown body)
|
|
65
|
+
"action": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"required": ["command"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"command": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"minLength": 1,
|
|
72
|
+
"description": "Command to run (supports {file}, {files}, {repo_root})",
|
|
73
|
+
},
|
|
74
|
+
"run_for": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": ["each_match", "all_matches"],
|
|
77
|
+
"default": "each_match",
|
|
78
|
+
"description": "Run command for each file or all files at once",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"additionalProperties": False,
|
|
82
|
+
"description": "Command action to run instead of prompting",
|
|
83
|
+
},
|
|
84
|
+
# Common options
|
|
85
|
+
"compare_to": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"enum": ["base", "default_tip", "prompt"],
|
|
88
|
+
"description": "Baseline for detecting file changes",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"additionalProperties": False,
|
|
92
|
+
# Detection mode must be exactly one of: trigger, set, pair, or created
|
|
93
|
+
"oneOf": [
|
|
94
|
+
{
|
|
95
|
+
"required": ["trigger"],
|
|
96
|
+
"not": {
|
|
97
|
+
"anyOf": [
|
|
98
|
+
{"required": ["set"]},
|
|
99
|
+
{"required": ["pair"]},
|
|
100
|
+
{"required": ["created"]},
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"required": ["set"],
|
|
106
|
+
"not": {
|
|
107
|
+
"anyOf": [
|
|
108
|
+
{"required": ["trigger"]},
|
|
109
|
+
{"required": ["pair"]},
|
|
110
|
+
{"required": ["created"]},
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"required": ["pair"],
|
|
116
|
+
"not": {
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{"required": ["trigger"]},
|
|
119
|
+
{"required": ["set"]},
|
|
120
|
+
{"required": ["created"]},
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"required": ["created"],
|
|
126
|
+
"not": {
|
|
127
|
+
"anyOf": [
|
|
128
|
+
{"required": ["trigger"]},
|
|
129
|
+
{"required": ["set"]},
|
|
130
|
+
{"required": ["pair"]},
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
name: deepwork_jobs
|
|
2
|
-
version: "0.
|
|
2
|
+
version: "0.5.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
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
|
-
asking
|
|
9
|
+
asking structured questions about your workflow, understanding each step's inputs and outputs,
|
|
10
10
|
and generating all necessary files.
|
|
11
11
|
|
|
12
12
|
The `learn` command reflects on conversations where DeepWork jobs were run, identifies
|
|
@@ -22,6 +22,8 @@ changelog:
|
|
|
22
22
|
changes: "Added make_new_job.sh script and templates directory; updated instructions to reference templates instead of inline examples"
|
|
23
23
|
- version: "0.4.0"
|
|
24
24
|
changes: "Removed implementation_summary and learning_summary outputs; simplified step outputs"
|
|
25
|
+
- version: "0.5.0"
|
|
26
|
+
changes: "Standardized on 'ask structured questions' phrasing for user input; Updated quality criteria hooks to verify phrase usage; Added guidance in implement.md to use phrase in generated instructions"
|
|
25
27
|
|
|
26
28
|
steps:
|
|
27
29
|
- id: define
|
|
@@ -34,21 +36,15 @@ steps:
|
|
|
34
36
|
outputs:
|
|
35
37
|
- job.yml
|
|
36
38
|
dependencies: []
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
5. **Rich Description**: Does the description provide enough context for future refinement?
|
|
47
|
-
6. **Valid Schema**: Does the job.yml follow the required schema (name, version, summary, steps)?
|
|
48
|
-
7. **File Created**: Has the job.yml file been created in `.deepwork/jobs/[job_name]/job.yml`?
|
|
49
|
-
|
|
50
|
-
If ANY criterion is not met, continue working to address it.
|
|
51
|
-
If ALL criteria are satisfied, include `<promise>✓ Quality Criteria Met</promise>` in your response.
|
|
39
|
+
quality_criteria:
|
|
40
|
+
- "**User Understanding**: Did the agent fully understand the user's workflow by asking structured questions?"
|
|
41
|
+
- "**Structured Questions Used**: Did the agent ask structured questions (using the AskUserQuestion tool) to gather user input?"
|
|
42
|
+
- "**Clear Inputs/Outputs**: Does every step have clearly defined inputs and outputs?"
|
|
43
|
+
- "**Logical Dependencies**: Do step dependencies make sense and avoid circular references?"
|
|
44
|
+
- "**Concise Summary**: Is the summary under 200 characters and descriptive?"
|
|
45
|
+
- "**Rich Description**: Does the description provide enough context for future refinement?"
|
|
46
|
+
- "**Valid Schema**: Does the job.yml follow the required schema (name, version, summary, steps)?"
|
|
47
|
+
- "**File Created**: Has the job.yml file been created in `.deepwork/jobs/[job_name]/job.yml`?"
|
|
52
48
|
|
|
53
49
|
- id: implement
|
|
54
50
|
name: "Implement Job Steps"
|
|
@@ -61,50 +57,36 @@ steps:
|
|
|
61
57
|
- steps/
|
|
62
58
|
dependencies:
|
|
63
59
|
- define
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
6. **Sync Complete**: Has `deepwork sync` been run successfully?
|
|
75
|
-
7. **Commands Available**: Are the slash-commands generated in `.claude/commands/`?
|
|
76
|
-
8. **Policies Considered**: Have you thought about whether policies would benefit this job?
|
|
77
|
-
- If relevant policies were identified, did you explain them and offer to run `/deepwork_policy.define`?
|
|
78
|
-
- Not every job needs policies - only suggest when genuinely helpful.
|
|
79
|
-
|
|
80
|
-
If ANY criterion is not met, continue working to address it.
|
|
81
|
-
If ALL criteria are satisfied, include `<promise>✓ Quality Criteria Met</promise>` in your response.
|
|
60
|
+
quality_criteria:
|
|
61
|
+
- "**Directory Structure**: Is `.deepwork/jobs/[job_name]/` created correctly?"
|
|
62
|
+
- "**Complete Instructions**: Are ALL step instruction files complete (not stubs or placeholders)?"
|
|
63
|
+
- "**Specific & Actionable**: Are instructions tailored to each step's purpose, not generic?"
|
|
64
|
+
- "**Output Examples**: Does each instruction file show what good output looks like?"
|
|
65
|
+
- "**Quality Criteria**: Does each instruction file define quality criteria for its outputs?"
|
|
66
|
+
- "**Ask Structured Questions**: Do step instructions that gather user input explicitly use the phrase \"ask structured questions\"?"
|
|
67
|
+
- "**Sync Complete**: Has `deepwork sync` been run successfully?"
|
|
68
|
+
- "**Commands Available**: Are the slash-commands generated in `.claude/commands/`?"
|
|
69
|
+
- "**Rules Considered**: Has the agent thought about whether rules would benefit this job? If relevant rules were identified, did they explain them and offer to run `/deepwork_rules.define`? Not every job needs rules - only suggest when genuinely helpful."
|
|
82
70
|
|
|
83
71
|
- id: learn
|
|
84
72
|
name: "Learn from Job Execution"
|
|
85
73
|
description: "Reflect on conversation to improve job instructions and capture learnings"
|
|
86
74
|
instructions_file: steps/learn.md
|
|
75
|
+
exposed: true
|
|
87
76
|
inputs:
|
|
88
77
|
- name: job_name
|
|
89
78
|
description: "Name of the job that was run (optional - will auto-detect from conversation)"
|
|
90
79
|
outputs:
|
|
91
80
|
- AGENTS.md
|
|
92
81
|
dependencies: []
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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?
|
|
108
|
-
|
|
109
|
-
If ANY criterion is not met, continue working to address it.
|
|
110
|
-
If ALL criteria are satisfied, include `<promise>✓ Quality Criteria Met</promise>` in your response.
|
|
82
|
+
quality_criteria:
|
|
83
|
+
- "**Conversation Analyzed**: Did the agent review the conversation for DeepWork job executions?"
|
|
84
|
+
- "**Confusion Identified**: Did the agent identify points of confusion, errors, or inefficiencies?"
|
|
85
|
+
- "**Instructions Improved**: Were job instructions updated to address identified issues?"
|
|
86
|
+
- "**Instructions Concise**: Are instructions free of redundancy and unnecessary verbosity?"
|
|
87
|
+
- "**Shared Content Extracted**: Is lengthy/duplicated content extracted into referenced files?"
|
|
88
|
+
- "**Bespoke Learnings Captured**: Were run-specific learnings added to AGENTS.md?"
|
|
89
|
+
- "**File References Used**: Do AGENTS.md entries reference other files where appropriate?"
|
|
90
|
+
- "**Working Folder Correct**: Is AGENTS.md in the correct working folder for the job?"
|
|
91
|
+
- "**Generalizable Separated**: Are generalizable improvements in instructions, not AGENTS.md?"
|
|
92
|
+
- "**Sync Complete**: Has `deepwork sync` been run if instructions were modified?"
|