deepwork 0.3.0__py3-none-any.whl → 0.4.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/hook.py +70 -0
- deepwork/cli/install.py +77 -29
- deepwork/cli/main.py +4 -0
- deepwork/cli/rules.py +32 -0
- deepwork/cli/sync.py +27 -1
- deepwork/core/adapters.py +209 -0
- deepwork/core/command_executor.py +26 -9
- deepwork/core/doc_spec_parser.py +205 -0
- deepwork/core/generator.py +79 -4
- deepwork/core/hooks_syncer.py +15 -2
- deepwork/core/parser.py +64 -2
- deepwork/core/rules_parser.py +58 -10
- deepwork/hooks/__init__.py +9 -3
- deepwork/hooks/check_version.sh +230 -0
- deepwork/hooks/claude_hook.sh +13 -17
- deepwork/hooks/gemini_hook.sh +13 -17
- deepwork/hooks/rules_check.py +269 -24
- deepwork/hooks/wrapper.py +66 -16
- deepwork/schemas/doc_spec_schema.py +64 -0
- deepwork/schemas/job_schema.py +25 -3
- deepwork/schemas/rules_schema.py +38 -6
- deepwork/standard_jobs/deepwork_jobs/doc_specs/job_spec.md +190 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +41 -8
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +68 -2
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +3 -3
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +74 -5
- deepwork/standard_jobs/deepwork_jobs/steps/review_job_spec.md +208 -0
- deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.example +86 -0
- deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.template +26 -0
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +21 -10
- deepwork/standard_jobs/deepwork_rules/job.yml +13 -3
- deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +1 -0
- deepwork/templates/claude/skill-job-meta.md.jinja +7 -0
- deepwork/templates/claude/skill-job-step.md.jinja +60 -7
- deepwork/templates/gemini/skill-job-step.toml.jinja +18 -3
- deepwork/utils/fs.py +36 -0
- deepwork/utils/yaml_utils.py +24 -0
- {deepwork-0.3.0.dist-info → deepwork-0.4.0.dist-info}/METADATA +41 -2
- deepwork-0.4.0.dist-info/RECORD +71 -0
- deepwork-0.3.0.dist-info/RECORD +0 -62
- {deepwork-0.3.0.dist-info → deepwork-0.4.0.dist-info}/WHEEL +0 -0
- {deepwork-0.3.0.dist-info → deepwork-0.4.0.dist-info}/entry_points.txt +0 -0
- {deepwork-0.3.0.dist-info → deepwork-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
deepwork/cli/hook.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Hook command for DeepWork CLI.
|
|
2
|
+
|
|
3
|
+
This command runs hook scripts, allowing hooks to use the `deepwork` CLI
|
|
4
|
+
instead of `python -m deepwork.hooks.*`, which works regardless of how
|
|
5
|
+
deepwork was installed (flake, pipx, uv, etc.).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
deepwork hook rules_check
|
|
9
|
+
deepwork hook <hook_name>
|
|
10
|
+
|
|
11
|
+
This is meant to be called from hook wrapper scripts (claude_hook.sh, gemini_hook.sh).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HookError(Exception):
|
|
24
|
+
"""Exception raised for hook errors."""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
@click.argument("hook_name")
|
|
31
|
+
def hook(hook_name: str) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Run a DeepWork hook by name.
|
|
34
|
+
|
|
35
|
+
HOOK_NAME: Name of the hook to run (e.g., 'rules_check')
|
|
36
|
+
|
|
37
|
+
This command imports and runs the hook module from deepwork.hooks.{hook_name}.
|
|
38
|
+
The hook receives stdin input and outputs to stdout, following the hook protocol.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
deepwork hook rules_check
|
|
42
|
+
echo '{}' | deepwork hook rules_check
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# Import the hook module
|
|
46
|
+
# If the hook_name contains a dot, treat it as a full module path
|
|
47
|
+
# Otherwise, assume it's a hook in the deepwork.hooks package
|
|
48
|
+
if "." in hook_name:
|
|
49
|
+
module_name = hook_name
|
|
50
|
+
else:
|
|
51
|
+
module_name = f"deepwork.hooks.{hook_name}"
|
|
52
|
+
try:
|
|
53
|
+
module = importlib.import_module(module_name)
|
|
54
|
+
except ModuleNotFoundError:
|
|
55
|
+
raise HookError(
|
|
56
|
+
f"Hook '{hook_name}' not found. Available hooks are in the deepwork.hooks package."
|
|
57
|
+
) from None
|
|
58
|
+
|
|
59
|
+
# Run the hook's main function if it exists
|
|
60
|
+
if hasattr(module, "main"):
|
|
61
|
+
sys.exit(module.main())
|
|
62
|
+
else:
|
|
63
|
+
raise HookError(f"Hook module '{module_name}' does not have a main() function")
|
|
64
|
+
|
|
65
|
+
except HookError as e:
|
|
66
|
+
console.print(f"[red]Error:[/red] {e}", style="bold red")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
console.print(f"[red]Unexpected error running hook:[/red] {e}", style="bold red")
|
|
70
|
+
sys.exit(1)
|
deepwork/cli/install.py
CHANGED
|
@@ -8,7 +8,7 @@ from rich.console import Console
|
|
|
8
8
|
|
|
9
9
|
from deepwork.core.adapters import AgentAdapter
|
|
10
10
|
from deepwork.core.detector import PlatformDetector
|
|
11
|
-
from deepwork.utils.fs import ensure_dir
|
|
11
|
+
from deepwork.utils.fs import ensure_dir, fix_permissions
|
|
12
12
|
from deepwork.utils.git import is_git_repo
|
|
13
13
|
from deepwork.utils.yaml_utils import load_yaml, save_yaml
|
|
14
14
|
|
|
@@ -52,9 +52,24 @@ def _inject_standard_job(job_name: str, jobs_dir: Path, project_path: Path) -> N
|
|
|
52
52
|
shutil.rmtree(target_dir)
|
|
53
53
|
|
|
54
54
|
shutil.copytree(standard_jobs_dir, target_dir)
|
|
55
|
+
# Fix permissions - source may have restrictive permissions (e.g., read-only)
|
|
56
|
+
fix_permissions(target_dir)
|
|
55
57
|
console.print(
|
|
56
58
|
f" [green]✓[/green] Installed {job_name} ({target_dir.relative_to(project_path)})"
|
|
57
59
|
)
|
|
60
|
+
|
|
61
|
+
# Copy any doc specs from the standard job to .deepwork/doc_specs/
|
|
62
|
+
doc_specs_source = standard_jobs_dir / "doc_specs"
|
|
63
|
+
doc_specs_target = project_path / ".deepwork" / "doc_specs"
|
|
64
|
+
if doc_specs_source.exists():
|
|
65
|
+
for doc_spec_file in doc_specs_source.glob("*.md"):
|
|
66
|
+
target_doc_spec = doc_specs_target / doc_spec_file.name
|
|
67
|
+
shutil.copy(doc_spec_file, target_doc_spec)
|
|
68
|
+
# Fix permissions for copied doc spec
|
|
69
|
+
fix_permissions(target_doc_spec)
|
|
70
|
+
console.print(
|
|
71
|
+
f" [green]✓[/green] Installed doc spec {doc_spec_file.name} ({target_doc_spec.relative_to(project_path)})"
|
|
72
|
+
)
|
|
58
73
|
except Exception as e:
|
|
59
74
|
raise InstallError(f"Failed to install {job_name}: {e}") from e
|
|
60
75
|
|
|
@@ -91,26 +106,47 @@ def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
|
|
|
91
106
|
"""
|
|
92
107
|
Create .gitignore file in .deepwork/ directory.
|
|
93
108
|
|
|
94
|
-
This ensures that
|
|
109
|
+
This ensures that runtime artifacts are not committed while keeping
|
|
110
|
+
the tmp directory structure in version control.
|
|
95
111
|
|
|
96
112
|
Args:
|
|
97
113
|
deepwork_dir: Path to .deepwork directory
|
|
98
114
|
"""
|
|
99
115
|
gitignore_path = deepwork_dir / ".gitignore"
|
|
100
|
-
gitignore_content = """# DeepWork
|
|
101
|
-
# These files are
|
|
116
|
+
gitignore_content = """# DeepWork runtime artifacts
|
|
117
|
+
# These files are generated during sessions and should not be committed
|
|
102
118
|
.last_work_tree
|
|
119
|
+
.last_head_ref
|
|
120
|
+
|
|
121
|
+
# Temporary files (but keep the directory via .gitkeep)
|
|
122
|
+
tmp/*
|
|
123
|
+
!tmp/.gitkeep
|
|
103
124
|
"""
|
|
104
125
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
# Always overwrite to ensure correct content
|
|
127
|
+
gitignore_path.write_text(gitignore_content)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _create_tmp_directory(deepwork_dir: Path) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Create the .deepwork/tmp directory with a .gitkeep file.
|
|
133
|
+
|
|
134
|
+
This ensures the tmp directory exists in version control, which is required
|
|
135
|
+
for file permissions to work correctly when Claude Code starts fresh.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
deepwork_dir: Path to .deepwork directory
|
|
139
|
+
"""
|
|
140
|
+
tmp_dir = deepwork_dir / "tmp"
|
|
141
|
+
ensure_dir(tmp_dir)
|
|
142
|
+
|
|
143
|
+
gitkeep_file = tmp_dir / ".gitkeep"
|
|
144
|
+
if not gitkeep_file.exists():
|
|
145
|
+
gitkeep_file.write_text(
|
|
146
|
+
"# This file ensures the .deepwork/tmp directory exists in version control.\n"
|
|
147
|
+
"# The tmp directory is used for temporary files during DeepWork operations.\n"
|
|
148
|
+
"# Do not delete this file.\n"
|
|
149
|
+
)
|
|
114
150
|
|
|
115
151
|
|
|
116
152
|
def _create_rules_directory(project_path: Path) -> bool:
|
|
@@ -142,6 +178,8 @@ def _create_rules_directory(project_path: Path) -> bool:
|
|
|
142
178
|
for example_file in example_rules_dir.glob("*.md.example"):
|
|
143
179
|
dest_file = rules_dir / example_file.name
|
|
144
180
|
shutil.copy(example_file, dest_file)
|
|
181
|
+
# Fix permissions for copied rule template
|
|
182
|
+
fix_permissions(dest_file)
|
|
145
183
|
|
|
146
184
|
# Create a README file explaining the rules system
|
|
147
185
|
readme_content = """# DeepWork Rules
|
|
@@ -283,28 +321,34 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
283
321
|
available_adapters = detector.detect_all_platforms()
|
|
284
322
|
|
|
285
323
|
if not available_adapters:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
# No platforms detected - default to Claude Code
|
|
325
|
+
console.print(" [dim]•[/dim] No AI platform detected, defaulting to Claude Code")
|
|
326
|
+
|
|
327
|
+
# Create .claude directory
|
|
328
|
+
claude_dir = project_path / ".claude"
|
|
329
|
+
ensure_dir(claude_dir)
|
|
330
|
+
console.print(f" [green]✓[/green] Created {claude_dir.relative_to(project_path)}/")
|
|
331
|
+
|
|
332
|
+
# Get Claude adapter
|
|
333
|
+
claude_adapter_class = AgentAdapter.get("claude")
|
|
334
|
+
claude_adapter = claude_adapter_class(project_root=project_path)
|
|
335
|
+
platforms_to_add = [claude_adapter.name]
|
|
336
|
+
detected_adapters = [claude_adapter]
|
|
337
|
+
else:
|
|
338
|
+
# Add all detected platforms
|
|
339
|
+
for adapter in available_adapters:
|
|
340
|
+
console.print(f" [green]✓[/green] {adapter.display_name} detected")
|
|
341
|
+
platforms_to_add.append(adapter.name)
|
|
342
|
+
detected_adapters = available_adapters
|
|
301
343
|
|
|
302
344
|
# Step 3: Create .deepwork/ directory structure
|
|
303
345
|
console.print("[yellow]→[/yellow] Creating DeepWork directory structure...")
|
|
304
346
|
deepwork_dir = project_path / ".deepwork"
|
|
305
347
|
jobs_dir = deepwork_dir / "jobs"
|
|
348
|
+
doc_specs_dir = deepwork_dir / "doc_specs"
|
|
306
349
|
ensure_dir(deepwork_dir)
|
|
307
350
|
ensure_dir(jobs_dir)
|
|
351
|
+
ensure_dir(doc_specs_dir)
|
|
308
352
|
console.print(f" [green]✓[/green] Created {deepwork_dir.relative_to(project_path)}/")
|
|
309
353
|
|
|
310
354
|
# Step 3b: Inject standard jobs (core job definitions)
|
|
@@ -316,7 +360,11 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
316
360
|
_create_deepwork_gitignore(deepwork_dir)
|
|
317
361
|
console.print(" [green]✓[/green] Created .deepwork/.gitignore")
|
|
318
362
|
|
|
319
|
-
# Step 3d: Create
|
|
363
|
+
# Step 3d: Create tmp directory with .gitkeep file for version control
|
|
364
|
+
_create_tmp_directory(deepwork_dir)
|
|
365
|
+
console.print(" [green]✓[/green] Created .deepwork/tmp/.gitkeep")
|
|
366
|
+
|
|
367
|
+
# Step 3e: Create rules directory with v2 templates
|
|
320
368
|
if _create_rules_directory(project_path):
|
|
321
369
|
console.print(" [green]✓[/green] Created .deepwork/rules/ with example templates")
|
|
322
370
|
else:
|
deepwork/cli/main.py
CHANGED
|
@@ -14,11 +14,15 @@ def cli() -> None:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
# Import commands
|
|
17
|
+
from deepwork.cli.hook import hook # noqa: E402
|
|
17
18
|
from deepwork.cli.install import install # noqa: E402
|
|
19
|
+
from deepwork.cli.rules import rules # noqa: E402
|
|
18
20
|
from deepwork.cli.sync import sync # noqa: E402
|
|
19
21
|
|
|
20
22
|
cli.add_command(install)
|
|
21
23
|
cli.add_command(sync)
|
|
24
|
+
cli.add_command(hook)
|
|
25
|
+
cli.add_command(rules)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
if __name__ == "__main__":
|
deepwork/cli/rules.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Rules command for DeepWork CLI."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from deepwork.core.rules_queue import RulesQueue
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def rules() -> None:
|
|
13
|
+
"""Manage DeepWork rules and queue."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@rules.command(name="clear_queue")
|
|
18
|
+
def clear_queue() -> None:
|
|
19
|
+
"""
|
|
20
|
+
Clear all entries from the rules queue.
|
|
21
|
+
|
|
22
|
+
Removes all JSON files from .deepwork/tmp/rules/queue/.
|
|
23
|
+
This is useful for resetting the queue between tests or after
|
|
24
|
+
manual verification of rule states.
|
|
25
|
+
"""
|
|
26
|
+
queue = RulesQueue()
|
|
27
|
+
count = queue.clear()
|
|
28
|
+
|
|
29
|
+
if count == 0:
|
|
30
|
+
console.print("[yellow]Queue is already empty[/yellow]")
|
|
31
|
+
else:
|
|
32
|
+
console.print(f"[green]Cleared {count} queue entry/entries[/green]")
|
deepwork/cli/sync.py
CHANGED
|
@@ -136,11 +136,15 @@ def sync_skills(project_path: Path) -> None:
|
|
|
136
136
|
ensure_dir(skills_dir)
|
|
137
137
|
|
|
138
138
|
# Generate skills for all jobs
|
|
139
|
+
all_skill_paths: list[Path] = []
|
|
139
140
|
if jobs:
|
|
140
141
|
console.print(" [dim]•[/dim] Generating skills...")
|
|
141
142
|
for job in jobs:
|
|
142
143
|
try:
|
|
143
|
-
job_paths = generator.generate_all_skills(
|
|
144
|
+
job_paths = generator.generate_all_skills(
|
|
145
|
+
job, adapter, platform_dir, project_root=project_path
|
|
146
|
+
)
|
|
147
|
+
all_skill_paths.extend(job_paths)
|
|
144
148
|
stats["skills"] += len(job_paths)
|
|
145
149
|
console.print(f" [green]✓[/green] {job.name} ({len(job_paths)} skills)")
|
|
146
150
|
except Exception as e:
|
|
@@ -157,6 +161,28 @@ def sync_skills(project_path: Path) -> None:
|
|
|
157
161
|
except Exception as e:
|
|
158
162
|
console.print(f" [red]✗[/red] Failed to sync hooks: {e}")
|
|
159
163
|
|
|
164
|
+
# Sync required permissions to platform settings
|
|
165
|
+
console.print(" [dim]•[/dim] Syncing permissions...")
|
|
166
|
+
try:
|
|
167
|
+
perms_count = adapter.sync_permissions(project_path)
|
|
168
|
+
if perms_count > 0:
|
|
169
|
+
console.print(f" [green]✓[/green] Added {perms_count} base permission(s)")
|
|
170
|
+
else:
|
|
171
|
+
console.print(" [dim]•[/dim] Base permissions already configured")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
console.print(f" [red]✗[/red] Failed to sync permissions: {e}")
|
|
174
|
+
|
|
175
|
+
# Add skill permissions for generated skills (if adapter supports it)
|
|
176
|
+
if all_skill_paths and hasattr(adapter, "add_skill_permissions"):
|
|
177
|
+
try:
|
|
178
|
+
skill_perms_count = adapter.add_skill_permissions(project_path, all_skill_paths)
|
|
179
|
+
if skill_perms_count > 0:
|
|
180
|
+
console.print(
|
|
181
|
+
f" [green]✓[/green] Added {skill_perms_count} skill permission(s)"
|
|
182
|
+
)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f" [red]✗[/red] Failed to sync skill permissions: {e}")
|
|
185
|
+
|
|
160
186
|
stats["platforms"] += 1
|
|
161
187
|
synced_adapters.append(adapter)
|
|
162
188
|
|
deepwork/core/adapters.py
CHANGED
|
@@ -241,6 +241,25 @@ class AgentAdapter(ABC):
|
|
|
241
241
|
"""
|
|
242
242
|
pass
|
|
243
243
|
|
|
244
|
+
def sync_permissions(self, project_path: Path) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Sync required permissions to platform settings.
|
|
247
|
+
|
|
248
|
+
This method adds any permissions that DeepWork requires to function
|
|
249
|
+
properly (e.g., access to .deepwork/tmp/ directory).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
project_path: Path to project root
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Number of permissions added
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
AdapterError: If sync fails
|
|
259
|
+
"""
|
|
260
|
+
# Default implementation does nothing - subclasses can override
|
|
261
|
+
return 0
|
|
262
|
+
|
|
244
263
|
|
|
245
264
|
def _hook_already_present(hooks: list[dict[str, Any]], script_path: str) -> bool:
|
|
246
265
|
"""Check if a hook with the given script path is already in the list."""
|
|
@@ -344,6 +363,196 @@ class ClaudeAdapter(AgentAdapter):
|
|
|
344
363
|
total = sum(len(hooks_list) for hooks_list in hooks.values())
|
|
345
364
|
return total
|
|
346
365
|
|
|
366
|
+
def _load_settings(self, project_path: Path) -> dict[str, Any]:
|
|
367
|
+
"""
|
|
368
|
+
Load settings.json from the project.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
project_path: Path to project root
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Settings dictionary (empty dict if file doesn't exist)
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
AdapterError: If file exists but cannot be read
|
|
378
|
+
"""
|
|
379
|
+
settings_file = project_path / self.config_dir / "settings.json"
|
|
380
|
+
if settings_file.exists():
|
|
381
|
+
try:
|
|
382
|
+
with open(settings_file, encoding="utf-8") as f:
|
|
383
|
+
result: dict[str, Any] = json.load(f)
|
|
384
|
+
return result
|
|
385
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
386
|
+
raise AdapterError(f"Failed to read settings.json: {e}") from e
|
|
387
|
+
return {}
|
|
388
|
+
|
|
389
|
+
def _save_settings(self, project_path: Path, settings: dict[str, Any]) -> None:
|
|
390
|
+
"""
|
|
391
|
+
Save settings.json to the project.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
project_path: Path to project root
|
|
395
|
+
settings: Settings dictionary to save
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
AdapterError: If file cannot be written
|
|
399
|
+
"""
|
|
400
|
+
settings_file = project_path / self.config_dir / "settings.json"
|
|
401
|
+
try:
|
|
402
|
+
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
403
|
+
with open(settings_file, "w", encoding="utf-8") as f:
|
|
404
|
+
json.dump(settings, f, indent=2)
|
|
405
|
+
except OSError as e:
|
|
406
|
+
raise AdapterError(f"Failed to write settings.json: {e}") from e
|
|
407
|
+
|
|
408
|
+
def add_permission(
|
|
409
|
+
self, project_path: Path, permission: str, settings: dict[str, Any] | None = None
|
|
410
|
+
) -> bool:
|
|
411
|
+
"""
|
|
412
|
+
Add a single permission to settings.json allow list.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
project_path: Path to project root
|
|
416
|
+
permission: The permission string to add (e.g., "Read(./.deepwork/tmp/**)")
|
|
417
|
+
settings: Optional pre-loaded settings dict. If provided, modifies in-place
|
|
418
|
+
and does NOT save to disk (caller is responsible for saving).
|
|
419
|
+
If None, loads settings, adds permission, and saves.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if permission was added, False if already present
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
AdapterError: If settings cannot be read/written
|
|
426
|
+
"""
|
|
427
|
+
save_after = settings is None
|
|
428
|
+
if settings is None:
|
|
429
|
+
settings = self._load_settings(project_path)
|
|
430
|
+
|
|
431
|
+
# Ensure permissions structure exists
|
|
432
|
+
if "permissions" not in settings:
|
|
433
|
+
settings["permissions"] = {}
|
|
434
|
+
if "allow" not in settings["permissions"]:
|
|
435
|
+
settings["permissions"]["allow"] = []
|
|
436
|
+
|
|
437
|
+
# Add permission if not already present
|
|
438
|
+
allow_list = settings["permissions"]["allow"]
|
|
439
|
+
if permission not in allow_list:
|
|
440
|
+
allow_list.append(permission)
|
|
441
|
+
if save_after:
|
|
442
|
+
self._save_settings(project_path, settings)
|
|
443
|
+
return True
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def sync_permissions(self, project_path: Path) -> int:
|
|
447
|
+
"""
|
|
448
|
+
Sync required permissions to Claude Code settings.json.
|
|
449
|
+
|
|
450
|
+
Adds permissions for:
|
|
451
|
+
- .deepwork/** - full access to deepwork directory
|
|
452
|
+
- All deepwork CLI commands (deepwork:*)
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
project_path: Path to project root
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Number of permissions added
|
|
459
|
+
|
|
460
|
+
Raises:
|
|
461
|
+
AdapterError: If sync fails
|
|
462
|
+
"""
|
|
463
|
+
# Define required permissions for DeepWork functionality
|
|
464
|
+
# Uses ./ prefix for paths relative to project root (per Claude Code docs)
|
|
465
|
+
required_permissions = [
|
|
466
|
+
# Full access to .deepwork directory
|
|
467
|
+
"Read(./.deepwork/**)",
|
|
468
|
+
"Edit(./.deepwork/**)",
|
|
469
|
+
"Write(./.deepwork/**)",
|
|
470
|
+
# All deepwork CLI commands
|
|
471
|
+
"Bash(deepwork:*)",
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
# Load settings once, add all permissions, then save once
|
|
475
|
+
settings = self._load_settings(project_path)
|
|
476
|
+
added_count = 0
|
|
477
|
+
|
|
478
|
+
for permission in required_permissions:
|
|
479
|
+
if self.add_permission(project_path, permission, settings):
|
|
480
|
+
added_count += 1
|
|
481
|
+
|
|
482
|
+
# Save if any permissions were added
|
|
483
|
+
if added_count > 0:
|
|
484
|
+
self._save_settings(project_path, settings)
|
|
485
|
+
|
|
486
|
+
return added_count
|
|
487
|
+
|
|
488
|
+
def add_skill_permissions(self, project_path: Path, skill_paths: list[Path]) -> int:
|
|
489
|
+
"""
|
|
490
|
+
Add Skill permissions for generated skills to settings.json.
|
|
491
|
+
|
|
492
|
+
This allows Claude to invoke the skills without permission prompts.
|
|
493
|
+
Uses the Skill(name) permission syntax.
|
|
494
|
+
|
|
495
|
+
Note: Skill permissions are an emerging Claude Code feature and
|
|
496
|
+
behavior may vary between versions.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
project_path: Path to project root
|
|
500
|
+
skill_paths: List of paths to generated skill files
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Number of permissions added
|
|
504
|
+
|
|
505
|
+
Raises:
|
|
506
|
+
AdapterError: If sync fails
|
|
507
|
+
"""
|
|
508
|
+
if not skill_paths:
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
# Load settings once
|
|
512
|
+
settings = self._load_settings(project_path)
|
|
513
|
+
added_count = 0
|
|
514
|
+
|
|
515
|
+
for skill_path in skill_paths:
|
|
516
|
+
# Extract skill name from path
|
|
517
|
+
# Path format: .claude/skills/job_name/SKILL.md -> job_name
|
|
518
|
+
# Path format: .claude/skills/job_name.step_id/SKILL.md -> job_name.step_id
|
|
519
|
+
skill_name = self._extract_skill_name(skill_path)
|
|
520
|
+
if skill_name:
|
|
521
|
+
permission = f"Skill({skill_name})"
|
|
522
|
+
if self.add_permission(project_path, permission, settings):
|
|
523
|
+
added_count += 1
|
|
524
|
+
|
|
525
|
+
# Save if any permissions were added
|
|
526
|
+
if added_count > 0:
|
|
527
|
+
self._save_settings(project_path, settings)
|
|
528
|
+
|
|
529
|
+
return added_count
|
|
530
|
+
|
|
531
|
+
def _extract_skill_name(self, skill_path: Path) -> str | None:
|
|
532
|
+
"""
|
|
533
|
+
Extract skill name from a skill file path.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
skill_path: Path to skill file (e.g., .claude/skills/job_name/SKILL.md)
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Skill name (e.g., "job_name") or None if cannot extract
|
|
540
|
+
"""
|
|
541
|
+
# Handle both absolute and relative paths
|
|
542
|
+
parts = skill_path.parts
|
|
543
|
+
|
|
544
|
+
# Find 'skills' directory and get the next part
|
|
545
|
+
try:
|
|
546
|
+
skills_idx = parts.index("skills")
|
|
547
|
+
if skills_idx + 1 < len(parts):
|
|
548
|
+
# The skill name is the directory after 'skills'
|
|
549
|
+
# e.g., skills/job_name/SKILL.md -> job_name
|
|
550
|
+
return parts[skills_idx + 1]
|
|
551
|
+
except ValueError:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
return None
|
|
555
|
+
|
|
347
556
|
|
|
348
557
|
class GeminiAdapter(AgentAdapter):
|
|
349
558
|
"""Adapter for Gemini CLI.
|
|
@@ -159,15 +159,32 @@ def all_commands_succeeded(results: list[CommandResult]) -> bool:
|
|
|
159
159
|
return all(r.success for r in results)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def format_command_errors(
|
|
163
|
-
|
|
162
|
+
def format_command_errors(
|
|
163
|
+
results: list[CommandResult],
|
|
164
|
+
rule_name: str | None = None,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Format detailed error messages from failed commands.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
results: List of command execution results
|
|
170
|
+
rule_name: Optional rule name to include in error message
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Formatted error message with command, exit code, stdout, and stderr
|
|
174
|
+
"""
|
|
164
175
|
errors: list[str] = []
|
|
165
176
|
for result in results:
|
|
166
177
|
if not result.success:
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
parts: list[str] = []
|
|
179
|
+
if rule_name:
|
|
180
|
+
parts.append(f"Rule: {rule_name}")
|
|
181
|
+
parts.append(f"Command: {result.command}")
|
|
182
|
+
parts.append(f"Exit code: {result.exit_code}")
|
|
183
|
+
if result.stdout and result.stdout.strip():
|
|
184
|
+
parts.append(f"Stdout:\n{result.stdout.strip()}")
|
|
185
|
+
if result.stderr and result.stderr.strip():
|
|
186
|
+
parts.append(f"Stderr:\n{result.stderr.strip()}")
|
|
187
|
+
if not result.stdout.strip() and not result.stderr.strip():
|
|
188
|
+
parts.append("(no output)")
|
|
189
|
+
errors.append("\n".join(parts))
|
|
190
|
+
return "\n\n".join(errors)
|