deepwork 0.1.1__py3-none-any.whl → 0.3.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 +121 -32
- 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 +511 -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 +514 -0
- deepwork/hooks/wrapper.py +363 -0
- deepwork/schemas/job_schema.py +14 -1
- deepwork/schemas/rules_schema.py +103 -0
- deepwork/standard_jobs/deepwork_jobs/AGENTS.md +60 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +41 -56
- deepwork/standard_jobs/deepwork_jobs/make_new_job.sh +134 -0
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +29 -63
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +62 -263
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +4 -62
- 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_rules/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +39 -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 +45 -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.1.1.dist-info → deepwork-0.3.0.dist-info}/METADATA +54 -24
- deepwork-0.3.0.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/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/gemini/command-job-step.toml.jinja +0 -169
- deepwork-0.1.1.dist-info/RECORD +0 -41
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/capture_prompt_work_tree.sh +0 -0
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/WHEEL +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/entry_points.txt +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.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,103 @@
|
|
|
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"],
|
|
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
|
+
# Action type: command (default is prompt using markdown body)
|
|
60
|
+
"action": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["command"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"command": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"minLength": 1,
|
|
67
|
+
"description": "Command to run (supports {file}, {files}, {repo_root})",
|
|
68
|
+
},
|
|
69
|
+
"run_for": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": ["each_match", "all_matches"],
|
|
72
|
+
"default": "each_match",
|
|
73
|
+
"description": "Run command for each file or all files at once",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"additionalProperties": False,
|
|
77
|
+
"description": "Command action to run instead of prompting",
|
|
78
|
+
},
|
|
79
|
+
# Common options
|
|
80
|
+
"compare_to": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["base", "default_tip", "prompt"],
|
|
83
|
+
"default": "base",
|
|
84
|
+
"description": "Baseline for detecting file changes",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"additionalProperties": False,
|
|
88
|
+
# Detection mode must be exactly one of: trigger, set, or pair
|
|
89
|
+
"oneOf": [
|
|
90
|
+
{
|
|
91
|
+
"required": ["trigger"],
|
|
92
|
+
"not": {"anyOf": [{"required": ["set"]}, {"required": ["pair"]}]},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"required": ["set"],
|
|
96
|
+
"not": {"anyOf": [{"required": ["trigger"]}, {"required": ["pair"]}]},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"required": ["pair"],
|
|
100
|
+
"not": {"anyOf": [{"required": ["trigger"]}, {"required": ["set"]}]},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}
|
|
@@ -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
|