Dev10x 0.64.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.
- dev10x/__init__.py +1 -0
- dev10x/cli.py +53 -0
- dev10x/commands/__init__.py +0 -0
- dev10x/commands/hook.py +276 -0
- dev10x/commands/init.py +187 -0
- dev10x/commands/permission.py +349 -0
- dev10x/commands/platform.py +104 -0
- dev10x/commands/skill.py +90 -0
- dev10x/commands/validate.py +6 -0
- dev10x/config/__init__.py +3 -0
- dev10x/config/loader.py +111 -0
- dev10x/domain/__init__.py +32 -0
- dev10x/domain/config_loader.py +24 -0
- dev10x/domain/git_context.py +43 -0
- dev10x/domain/hook_input.py +97 -0
- dev10x/domain/plan.py +184 -0
- dev10x/domain/repository_ref.py +20 -0
- dev10x/domain/result.py +41 -0
- dev10x/domain/rule_engine.py +82 -0
- dev10x/domain/session_state.py +200 -0
- dev10x/domain/sql.py +41 -0
- dev10x/domain/validation_rule.py +111 -0
- dev10x/hooks/__init__.py +0 -0
- dev10x/hooks/edit_validator.py +52 -0
- dev10x/hooks/permission_diagnostics.py +342 -0
- dev10x/hooks/session.py +518 -0
- dev10x/hooks/skill.py +95 -0
- dev10x/hooks/task_plan_sync.py +143 -0
- dev10x/mcp/__init__.py +0 -0
- dev10x/mcp/audit.py +74 -0
- dev10x/mcp/db.py +33 -0
- dev10x/mcp/git.py +157 -0
- dev10x/mcp/github.py +621 -0
- dev10x/mcp/monitor.py +50 -0
- dev10x/mcp/permission.py +104 -0
- dev10x/mcp/plan.py +89 -0
- dev10x/mcp/release.py +36 -0
- dev10x/mcp/server_cli.py +832 -0
- dev10x/mcp/server_db.py +35 -0
- dev10x/mcp/skill_index.py +30 -0
- dev10x/mcp/subprocess_utils.py +108 -0
- dev10x/mcp/tests/__init__.py +0 -0
- dev10x/mcp/tests/test_git.py +163 -0
- dev10x/mcp/utilities.py +34 -0
- dev10x/platform/__init__.py +14 -0
- dev10x/platform/registry.py +156 -0
- dev10x/skills/__init__.py +0 -0
- dev10x/skills/audit/__init__.py +0 -0
- dev10x/skills/audit/analyze_actions.py +330 -0
- dev10x/skills/audit/analyze_permissions.py +510 -0
- dev10x/skills/audit/extract_session.py +237 -0
- dev10x/skills/audit/instruction_budget.py +246 -0
- dev10x/skills/database/__init__.py +0 -0
- dev10x/skills/evidence/__init__.py +0 -0
- dev10x/skills/monitor/__init__.py +0 -0
- dev10x/skills/monitor/ci_check_status.py +261 -0
- dev10x/skills/monitor/pr_notify.py +496 -0
- dev10x/skills/notifications/__init__.py +0 -0
- dev10x/skills/notifications/slack_review_request.py +259 -0
- dev10x/skills/permission/__init__.py +0 -0
- dev10x/skills/permission/backup.py +57 -0
- dev10x/skills/permission/clean_project_files.py +472 -0
- dev10x/skills/permission/enumerate_mcp.py +229 -0
- dev10x/skills/permission/file_lock.py +44 -0
- dev10x/skills/permission/merge_worktree_permissions.py +205 -0
- dev10x/skills/permission/update_paths.py +598 -0
- dev10x/skills/release/__init__.py +0 -0
- dev10x/skills/release/collect_prs.py +415 -0
- dev10x/validators/__init__.py +52 -0
- dev10x/validators/base.py +38 -0
- dev10x/validators/command_substitution.py +45 -0
- dev10x/validators/commit_jtbd.py +200 -0
- dev10x/validators/execution_safety.py +135 -0
- dev10x/validators/pr_base.py +67 -0
- dev10x/validators/prefix_friction.py +355 -0
- dev10x/validators/safe_subshell.py +130 -0
- dev10x/validators/skill_redirect.py +193 -0
- dev10x/validators/sql_safety.py +252 -0
- dev10x-0.64.0.dist-info/METADATA +322 -0
- dev10x-0.64.0.dist-info/RECORD +84 -0
- dev10x-0.64.0.dist-info/WHEEL +5 -0
- dev10x-0.64.0.dist-info/entry_points.txt +2 -0
- dev10x-0.64.0.dist-info/licenses/LICENSE +21 -0
- dev10x-0.64.0.dist-info/top_level.txt +1 -0
dev10x/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.46.0.dev0"
|
dev10x/cli.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LazyGroup(click.Group):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
*args: Any,
|
|
13
|
+
lazy_subcommands: dict[str, str] | None = None,
|
|
14
|
+
**kwargs: Any,
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
self._lazy_subcommands: dict[str, str] = lazy_subcommands or {}
|
|
18
|
+
|
|
19
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
20
|
+
base = super().list_commands(ctx)
|
|
21
|
+
lazy = sorted(self._lazy_subcommands.keys())
|
|
22
|
+
return base + lazy
|
|
23
|
+
|
|
24
|
+
def get_command(
|
|
25
|
+
self,
|
|
26
|
+
ctx: click.Context,
|
|
27
|
+
cmd_name: str,
|
|
28
|
+
) -> click.BaseCommand | None:
|
|
29
|
+
if cmd_name in self._lazy_subcommands:
|
|
30
|
+
return self._load_lazy(cmd_name)
|
|
31
|
+
return super().get_command(ctx, cmd_name)
|
|
32
|
+
|
|
33
|
+
def _load_lazy(self, cmd_name: str) -> click.BaseCommand:
|
|
34
|
+
import_path = self._lazy_subcommands[cmd_name]
|
|
35
|
+
module_path, attr_name = import_path.rsplit(".", 1)
|
|
36
|
+
module = importlib.import_module(module_path)
|
|
37
|
+
return getattr(module, attr_name) # type: ignore[no-any-return]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.group(
|
|
41
|
+
cls=LazyGroup,
|
|
42
|
+
lazy_subcommands={
|
|
43
|
+
"hook": "dev10x.commands.hook.hook",
|
|
44
|
+
"init": "dev10x.commands.init.init",
|
|
45
|
+
"permission": "dev10x.commands.permission.permission",
|
|
46
|
+
"platform": "dev10x.commands.platform.platform",
|
|
47
|
+
"validate": "dev10x.commands.validate.validate",
|
|
48
|
+
"skill": "dev10x.commands.skill.skill",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
@click.version_option(package_name="Dev10x")
|
|
52
|
+
def cli() -> None:
|
|
53
|
+
pass
|
|
File without changes
|
dev10x/commands/hook.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
_DEBUG = os.environ.get("HOOK_DEBUG", "") != ""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def hook() -> None:
|
|
16
|
+
"""Hook entry points (validate-bash, validate-edit, plan-sync, session)."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@hook.command(name="validate-bash")
|
|
20
|
+
def validate_bash() -> None:
|
|
21
|
+
"""Validate Bash commands via the unified validator registry.
|
|
22
|
+
|
|
23
|
+
Reads JSON from stdin, dispatches to registered validators.
|
|
24
|
+
Exit codes: 0=allow, 2=block.
|
|
25
|
+
"""
|
|
26
|
+
from dev10x.domain import HookInput
|
|
27
|
+
from dev10x.validators import get_validators
|
|
28
|
+
|
|
29
|
+
inp = HookInput.from_stdin()
|
|
30
|
+
if inp.tool_name != "Bash":
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
if not inp.command:
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
for validator in get_validators():
|
|
36
|
+
try:
|
|
37
|
+
if validator.should_run(inp=inp):
|
|
38
|
+
result = validator.validate(inp=inp)
|
|
39
|
+
if result is not None:
|
|
40
|
+
result.emit()
|
|
41
|
+
except Exception:
|
|
42
|
+
if _DEBUG:
|
|
43
|
+
print(
|
|
44
|
+
f"[HOOK_DEBUG] {validator.name} raised:",
|
|
45
|
+
file=sys.stderr,
|
|
46
|
+
)
|
|
47
|
+
traceback.print_exc(file=sys.stderr)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@hook.group(name="plan")
|
|
54
|
+
def plan() -> None:
|
|
55
|
+
"""Plan synchronization commands."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@plan.command(name="sync")
|
|
59
|
+
def plan_sync() -> None:
|
|
60
|
+
"""Sync task state from stdin (PostToolUse hook mode)."""
|
|
61
|
+
from dev10x.hooks.task_plan_sync import cmd_hook
|
|
62
|
+
|
|
63
|
+
cmd_hook()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@plan.command(name="summary")
|
|
67
|
+
def plan_summary() -> None:
|
|
68
|
+
"""Read plan YAML, output JSON summary."""
|
|
69
|
+
from dev10x.hooks.task_plan_sync import cmd_json_summary
|
|
70
|
+
|
|
71
|
+
cmd_json_summary()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@plan.command(name="set-context")
|
|
75
|
+
@click.argument("pairs", nargs=-1, required=True)
|
|
76
|
+
def plan_set_context(pairs: tuple[str, ...]) -> None:
|
|
77
|
+
"""Store plan-level context metadata (K=V pairs, dot-paths supported)."""
|
|
78
|
+
from dev10x.hooks.task_plan_sync import cmd_set_context
|
|
79
|
+
|
|
80
|
+
cmd_set_context(args=list(pairs))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@plan.command(name="archive")
|
|
84
|
+
def plan_archive() -> None:
|
|
85
|
+
"""Archive completed plan to .claude/session/archive/."""
|
|
86
|
+
from dev10x.hooks.task_plan_sync import cmd_archive
|
|
87
|
+
|
|
88
|
+
cmd_archive()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@hook.command(name="permission-denied")
|
|
92
|
+
def permission_denied() -> None:
|
|
93
|
+
"""Handle PermissionDenied events via the validator registry.
|
|
94
|
+
|
|
95
|
+
Reads JSON from stdin, dispatches to validators that implement
|
|
96
|
+
correct(). Returns retry=true with corrective guidance when a
|
|
97
|
+
validator recognizes the denied command.
|
|
98
|
+
|
|
99
|
+
Also runs permission diagnostics to explain *why* a pre-approved
|
|
100
|
+
tool was prompted (settings override semantics, missing rules).
|
|
101
|
+
Exit codes: 0 always (retry decision is in JSON output).
|
|
102
|
+
"""
|
|
103
|
+
from dev10x.domain import HookInput
|
|
104
|
+
from dev10x.validators import get_validators
|
|
105
|
+
from dev10x.validators.base import Corrector
|
|
106
|
+
|
|
107
|
+
inp = HookInput.from_stdin()
|
|
108
|
+
|
|
109
|
+
if inp.command:
|
|
110
|
+
for validator in get_validators():
|
|
111
|
+
try:
|
|
112
|
+
if not validator.should_run(inp=inp):
|
|
113
|
+
continue
|
|
114
|
+
if not isinstance(validator, Corrector):
|
|
115
|
+
continue
|
|
116
|
+
result = validator.correct(inp=inp)
|
|
117
|
+
if result is not None:
|
|
118
|
+
result.emit()
|
|
119
|
+
except Exception:
|
|
120
|
+
if _DEBUG:
|
|
121
|
+
print(
|
|
122
|
+
f"[HOOK_DEBUG] {validator.name} correct() raised:",
|
|
123
|
+
file=sys.stderr,
|
|
124
|
+
)
|
|
125
|
+
traceback.print_exc(file=sys.stderr)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
_run_permission_diagnostics(raw=inp.raw, cwd=inp.cwd)
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _run_permission_diagnostics(*, raw: dict, cwd: str) -> None:
|
|
133
|
+
try:
|
|
134
|
+
from dev10x.hooks.permission_diagnostics import diagnose, format_diagnostic
|
|
135
|
+
|
|
136
|
+
result = diagnose(raw=raw, cwd=cwd)
|
|
137
|
+
if result is None:
|
|
138
|
+
return
|
|
139
|
+
message = format_diagnostic(result=result)
|
|
140
|
+
if message:
|
|
141
|
+
print(
|
|
142
|
+
json.dumps({"systemMessage": message}),
|
|
143
|
+
file=sys.stderr,
|
|
144
|
+
)
|
|
145
|
+
except Exception:
|
|
146
|
+
if _DEBUG:
|
|
147
|
+
print("[HOOK_DEBUG] permission_diagnostics raised:", file=sys.stderr)
|
|
148
|
+
traceback.print_exc(file=sys.stderr)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@hook.command(name="validate-edit")
|
|
152
|
+
@click.option(
|
|
153
|
+
"--config",
|
|
154
|
+
"config_path",
|
|
155
|
+
type=click.Path(exists=True, path_type=Path),
|
|
156
|
+
default=None,
|
|
157
|
+
)
|
|
158
|
+
@click.option("--debug", is_flag=True)
|
|
159
|
+
def validate_edit(config_path: Path | None, debug: bool) -> None:
|
|
160
|
+
"""Validate Edit/Write tool calls against sensitive file rules.
|
|
161
|
+
|
|
162
|
+
Reads JSON from stdin, checks file paths against rules.
|
|
163
|
+
Exit codes: 0=allow, 2=block.
|
|
164
|
+
"""
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
from dev10x.hooks.edit_validator import validate_edit_write
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
data = json.load(sys.stdin)
|
|
171
|
+
except (json.JSONDecodeError, EOFError):
|
|
172
|
+
sys.exit(0)
|
|
173
|
+
|
|
174
|
+
validate_edit_write(
|
|
175
|
+
data=data,
|
|
176
|
+
yaml_path=config_path,
|
|
177
|
+
debug=debug,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@hook.group(name="session")
|
|
182
|
+
def session() -> None:
|
|
183
|
+
"""Session lifecycle commands."""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@session.command(name="reload")
|
|
187
|
+
def session_reload_cmd() -> None:
|
|
188
|
+
"""Reload prior session state (SessionStart hook)."""
|
|
189
|
+
from dev10x.hooks.session import session_reload
|
|
190
|
+
|
|
191
|
+
session_reload()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@session.command(name="compact")
|
|
195
|
+
def session_compact_cmd() -> None:
|
|
196
|
+
"""Inject context summary before compaction (PreCompact hook)."""
|
|
197
|
+
from dev10x.hooks.session import context_compact
|
|
198
|
+
|
|
199
|
+
context_compact()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@session.command(name="tmpdir")
|
|
203
|
+
def session_tmpdir_cmd() -> None:
|
|
204
|
+
"""Create session scratch directory and install mktmp.sh (SessionStart hook)."""
|
|
205
|
+
from dev10x.hooks.session import session_tmpdir
|
|
206
|
+
|
|
207
|
+
session_tmpdir()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@session.command(name="guidance")
|
|
211
|
+
def session_guidance_cmd() -> None:
|
|
212
|
+
"""Output session-guidance.md as additionalContext (SessionStart hook)."""
|
|
213
|
+
from dev10x.hooks.session import session_guidance
|
|
214
|
+
|
|
215
|
+
session_guidance()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@session.command(name="git-aliases")
|
|
219
|
+
def session_git_aliases_cmd() -> None:
|
|
220
|
+
"""Check git branch-comparison aliases and report status (SessionStart hook)."""
|
|
221
|
+
from dev10x.hooks.session import session_git_aliases
|
|
222
|
+
|
|
223
|
+
session_git_aliases()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@session.command(name="migrate-permissions")
|
|
227
|
+
def session_migrate_permissions_cmd() -> None:
|
|
228
|
+
"""Migrate stale plugin permission rules to current version (SessionStart hook)."""
|
|
229
|
+
from dev10x.hooks.session import session_migrate_permissions
|
|
230
|
+
|
|
231
|
+
session_migrate_permissions()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@session.command(name="persist")
|
|
235
|
+
def session_persist_cmd() -> None:
|
|
236
|
+
"""Persist session state to disk for next-session reload (SessionStop hook)."""
|
|
237
|
+
from dev10x.hooks.session import session_persist
|
|
238
|
+
|
|
239
|
+
session_persist()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@session.command(name="goodbye")
|
|
243
|
+
def session_goodbye_cmd() -> None:
|
|
244
|
+
"""Output goodbye message with community link and resume hint (SessionStop hook)."""
|
|
245
|
+
from dev10x.hooks.session import session_goodbye
|
|
246
|
+
|
|
247
|
+
session_goodbye()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@hook.group(name="skill")
|
|
251
|
+
def skill() -> None:
|
|
252
|
+
"""Skill lifecycle commands."""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@skill.command(name="tmpdir")
|
|
256
|
+
def skill_tmpdir_cmd() -> None:
|
|
257
|
+
"""Create scratch directory for skill (PreToolUse hook)."""
|
|
258
|
+
from dev10x.hooks.skill import skill_tmpdir
|
|
259
|
+
|
|
260
|
+
skill_tmpdir()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@skill.command(name="metrics")
|
|
264
|
+
def skill_metrics_cmd() -> None:
|
|
265
|
+
"""Append skill invocation metric to JSONL file (PostToolUse hook)."""
|
|
266
|
+
from dev10x.hooks.skill import skill_metrics
|
|
267
|
+
|
|
268
|
+
skill_metrics()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@hook.command(name="ruff-format")
|
|
272
|
+
def ruff_format_cmd() -> None:
|
|
273
|
+
"""Auto-format Python files with ruff after Edit/Write (PostToolUse hook)."""
|
|
274
|
+
from dev10x.hooks.skill import ruff_format
|
|
275
|
+
|
|
276
|
+
ruff_format()
|
dev10x/commands/init.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""`dev10x init` — guided onboarding for new users.
|
|
2
|
+
|
|
3
|
+
Creates a starter `.claude/Dev10x/` config tree in the current project
|
|
4
|
+
and prints a quick-start card covering the top 5 workflows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
QUICK_START_WORKFLOWS = [
|
|
15
|
+
(
|
|
16
|
+
"git-commit",
|
|
17
|
+
"/Dev10x:git-commit",
|
|
18
|
+
"JTBD-style commit messages with gitmoji + ticket ID",
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
"pr-create",
|
|
22
|
+
"/Dev10x:gh-pr-create",
|
|
23
|
+
"Draft PR with Job Story body and Fixes: link",
|
|
24
|
+
),
|
|
25
|
+
(
|
|
26
|
+
"review",
|
|
27
|
+
"/Dev10x:review",
|
|
28
|
+
"Self-review your branch before requesting a human reviewer",
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
"testing",
|
|
32
|
+
"/test",
|
|
33
|
+
"Run pytest with coverage enforcement",
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
"architecture",
|
|
37
|
+
"/Dev10x:adr",
|
|
38
|
+
"Author Architecture Decision Records with diagrams",
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
STARTER_WORK_ON_PLAYBOOK = """# Starter work-on playbook for this project.
|
|
43
|
+
#
|
|
44
|
+
# Customize with /Dev10x:playbook edit work-on <play>
|
|
45
|
+
# See skills/playbook/references/playbook.yaml for the full schema.
|
|
46
|
+
|
|
47
|
+
# active_modes controls per-step behavior adaptations. Uncomment any
|
|
48
|
+
# that apply to this project:
|
|
49
|
+
#
|
|
50
|
+
# active_modes:
|
|
51
|
+
# - solo-maintainer # skip reviewer assignment + Slack notifications
|
|
52
|
+
# - open-source # prefer issue templates and public-safe language
|
|
53
|
+
|
|
54
|
+
overrides: []
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
STARTER_GITMOJI_YAML = """# Gitmoji strategy override for this project.
|
|
58
|
+
#
|
|
59
|
+
# Leave default-strategy empty to use plugin defaults.
|
|
60
|
+
# See skills/git-commit/references/gitmoji-defaults.yaml for the base map.
|
|
61
|
+
|
|
62
|
+
default-strategy: null
|
|
63
|
+
projects: []
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _session_yaml_template() -> str:
|
|
68
|
+
return (
|
|
69
|
+
"# Dev10x session config — consumed by work-on, verify-acc-dod, and\n"
|
|
70
|
+
"# the PreCompact recovery hook.\n"
|
|
71
|
+
"friction_level: guided # strict | guided | adaptive\n"
|
|
72
|
+
"active_modes: []\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_if_missing(path: Path, content: str) -> bool:
|
|
77
|
+
if path.exists():
|
|
78
|
+
return False
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
path.write_text(content)
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _seed_project(project_root: Path) -> list[Path]:
|
|
85
|
+
"""Create starter config files. Returns list of paths written."""
|
|
86
|
+
written: list[Path] = []
|
|
87
|
+
|
|
88
|
+
targets = [
|
|
89
|
+
(project_root / ".claude" / "Dev10x" / "session.yaml", _session_yaml_template()),
|
|
90
|
+
(
|
|
91
|
+
project_root / ".claude" / "Dev10x" / "playbooks" / "work-on.yaml",
|
|
92
|
+
STARTER_WORK_ON_PLAYBOOK,
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
for path, content in targets:
|
|
96
|
+
if _write_if_missing(path, content):
|
|
97
|
+
written.append(path)
|
|
98
|
+
|
|
99
|
+
return written
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _print_card(*, project_root: Path) -> None:
|
|
103
|
+
click.echo("")
|
|
104
|
+
click.echo("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
105
|
+
click.echo(" Dev10x — Next 5 commands")
|
|
106
|
+
click.echo("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
107
|
+
for i, (workflow, command, description) in enumerate(QUICK_START_WORKFLOWS, start=1):
|
|
108
|
+
click.echo(f" {i}. {command:<28} {workflow}")
|
|
109
|
+
click.echo(f" {description}")
|
|
110
|
+
click.echo("")
|
|
111
|
+
click.echo(f" Config: {project_root}/.claude/Dev10x/")
|
|
112
|
+
click.echo(" Customize: /Dev10x:playbook edit work-on <play>")
|
|
113
|
+
click.echo(" Discovery: /Dev10x:onboarding")
|
|
114
|
+
click.echo("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
115
|
+
click.echo("")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@click.command()
|
|
119
|
+
@click.option(
|
|
120
|
+
"--setup",
|
|
121
|
+
is_flag=True,
|
|
122
|
+
help="Force interactive setup even when config already exists.",
|
|
123
|
+
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--non-interactive",
|
|
126
|
+
is_flag=True,
|
|
127
|
+
help="Skip interactive prompts; write starter config and print card only.",
|
|
128
|
+
)
|
|
129
|
+
@click.option(
|
|
130
|
+
"--path",
|
|
131
|
+
"project_path",
|
|
132
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
133
|
+
default=None,
|
|
134
|
+
help="Project root (defaults to current directory).",
|
|
135
|
+
)
|
|
136
|
+
def init(*, setup: bool, non_interactive: bool, project_path: Path | None) -> None:
|
|
137
|
+
"""Create a starter .claude/Dev10x/ config and print the quick-start card."""
|
|
138
|
+
project_root = (project_path or Path.cwd()).resolve()
|
|
139
|
+
|
|
140
|
+
if not project_root.is_dir():
|
|
141
|
+
click.echo(f"Project path does not exist: {project_root}", err=True)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
existing = (project_root / ".claude" / "Dev10x" / "session.yaml").exists()
|
|
145
|
+
if existing and not setup:
|
|
146
|
+
click.echo(f"Dev10x config already present at {project_root}/.claude/Dev10x/")
|
|
147
|
+
_print_card(project_root=project_root)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if non_interactive:
|
|
151
|
+
written = _seed_project(project_root)
|
|
152
|
+
for path in written:
|
|
153
|
+
click.echo(f" + {path.relative_to(project_root)}")
|
|
154
|
+
_print_card(project_root=project_root)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
click.echo("")
|
|
158
|
+
click.echo(f"Setting up Dev10x in {project_root}")
|
|
159
|
+
click.echo("")
|
|
160
|
+
|
|
161
|
+
friction_level = click.prompt(
|
|
162
|
+
"Friction level",
|
|
163
|
+
type=click.Choice(["strict", "guided", "adaptive"], case_sensitive=False),
|
|
164
|
+
default="guided",
|
|
165
|
+
)
|
|
166
|
+
solo = click.confirm(
|
|
167
|
+
"Solo maintainer mode? (skips reviewer assignment and Slack notifications)",
|
|
168
|
+
default=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
session_yaml_path = project_root / ".claude" / "Dev10x" / "session.yaml"
|
|
172
|
+
modes = ["solo-maintainer"] if solo else []
|
|
173
|
+
session_yaml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
session_yaml_path.write_text(
|
|
175
|
+
"# Dev10x session config\n"
|
|
176
|
+
f"friction_level: {friction_level.lower()}\n"
|
|
177
|
+
f"active_modes: {modes!r}\n"
|
|
178
|
+
)
|
|
179
|
+
click.echo(f" + {session_yaml_path.relative_to(project_root)}")
|
|
180
|
+
|
|
181
|
+
playbook_path = project_root / ".claude" / "Dev10x" / "playbooks" / "work-on.yaml"
|
|
182
|
+
if not playbook_path.exists():
|
|
183
|
+
playbook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
playbook_path.write_text(STARTER_WORK_ON_PLAYBOOK)
|
|
185
|
+
click.echo(f" + {playbook_path.relative_to(project_root)}")
|
|
186
|
+
|
|
187
|
+
_print_card(project_root=project_root)
|