devtorch-cli 3.0.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.
- devtorch_cli/__init__.py +1 -0
- devtorch_cli/__main__.py +6 -0
- devtorch_cli/audit_cmds.py +221 -0
- devtorch_cli/capability_role_cmds.py +94 -0
- devtorch_cli/main.py +4090 -0
- devtorch_cli/pr_reporter_cmd.py +41 -0
- devtorch_cli/pre_commit_cmds.py +40 -0
- devtorch_cli/session_cmds.py +282 -0
- devtorch_cli-3.0.1.dist-info/METADATA +60 -0
- devtorch_cli-3.0.1.dist-info/RECORD +13 -0
- devtorch_cli-3.0.1.dist-info/WHEEL +5 -0
- devtorch_cli-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_cli-3.0.1.dist-info/top_level.txt +1 -0
devtorch_cli/main.py
ADDED
|
@@ -0,0 +1,4090 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import types
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from devtorch_core import (
|
|
12
|
+
GCCRepository,
|
|
13
|
+
SensitivityEvent,
|
|
14
|
+
OmegaCapability,
|
|
15
|
+
L_MAX_DEFAULT,
|
|
16
|
+
)
|
|
17
|
+
from devtorch_core.serve import cmd_serve as _cmd_serve
|
|
18
|
+
|
|
19
|
+
from devtorch_cli.session_cmds import (
|
|
20
|
+
cmd_session_start,
|
|
21
|
+
cmd_session_list,
|
|
22
|
+
cmd_session_show,
|
|
23
|
+
cmd_session_plan,
|
|
24
|
+
cmd_session_next,
|
|
25
|
+
cmd_session_mark,
|
|
26
|
+
cmd_session_close,
|
|
27
|
+
cmd_session_disagree,
|
|
28
|
+
cmd_session_resolve,
|
|
29
|
+
cmd_session_disagreements,
|
|
30
|
+
cmd_simulate,
|
|
31
|
+
)
|
|
32
|
+
from devtorch_cli.capability_role_cmds import (
|
|
33
|
+
cmd_capability_role_list,
|
|
34
|
+
cmd_capability_role_show,
|
|
35
|
+
cmd_capability_role_set,
|
|
36
|
+
cmd_capability_role_policy,
|
|
37
|
+
cmd_capability_role_apply_default_policy,
|
|
38
|
+
)
|
|
39
|
+
from devtorch_cli.pre_commit_cmds import (
|
|
40
|
+
cmd_pre_commit_install,
|
|
41
|
+
cmd_pre_commit_run,
|
|
42
|
+
)
|
|
43
|
+
from devtorch_cli.pr_reporter_cmd import cmd_pr_report
|
|
44
|
+
from devtorch_cli.audit_cmds import build_audit_parser
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
48
|
+
project_root = Path(args.path).resolve()
|
|
49
|
+
template_name = getattr(args, "template", None)
|
|
50
|
+
if template_name:
|
|
51
|
+
from devtorch_core.templates.engine import apply_template
|
|
52
|
+
try:
|
|
53
|
+
apply_template(project_root, template_name)
|
|
54
|
+
except ValueError as exc:
|
|
55
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
print(f"Initialized DevTorch GCC store at {project_root / '.GCC'} with template '{template_name}'")
|
|
58
|
+
else:
|
|
59
|
+
repo = GCCRepository.at(project_root)
|
|
60
|
+
repo.init()
|
|
61
|
+
print(f"Initialized DevTorch GCC store at {repo.gcc_dir}")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# S27: reasoning template library
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_templates_list(args: argparse.Namespace) -> int:
|
|
69
|
+
"""List built-in and custom reasoning templates."""
|
|
70
|
+
from devtorch_core.templates.library import (
|
|
71
|
+
list_builtin_templates,
|
|
72
|
+
discover_custom_templates,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
project_root = Path(args.path).resolve()
|
|
76
|
+
builtins = list_builtin_templates()
|
|
77
|
+
custom = discover_custom_templates(project_root)
|
|
78
|
+
|
|
79
|
+
if not builtins and not custom:
|
|
80
|
+
print("No templates found.")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
print("Built-in templates:")
|
|
84
|
+
for t in builtins:
|
|
85
|
+
tags = ", ".join(t.get("tags", []))
|
|
86
|
+
print(f" {t['name']:<22} {t['description']}")
|
|
87
|
+
if tags:
|
|
88
|
+
print(f" tags: {tags}")
|
|
89
|
+
|
|
90
|
+
if custom:
|
|
91
|
+
print("\nCustom templates:")
|
|
92
|
+
for t in custom:
|
|
93
|
+
tags = ", ".join(t.get("tags", []))
|
|
94
|
+
print(f" {t['name']:<22} {t['description']} [{t['origin']}]")
|
|
95
|
+
if tags:
|
|
96
|
+
print(f" tags: {tags}")
|
|
97
|
+
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_templates_apply(args: argparse.Namespace) -> int:
|
|
102
|
+
"""Render a reasoning template with variables."""
|
|
103
|
+
from devtorch_core.templates.library import apply_template
|
|
104
|
+
|
|
105
|
+
variables: dict[str, str] = {}
|
|
106
|
+
for var in getattr(args, "variables", []) or []:
|
|
107
|
+
if "=" not in var:
|
|
108
|
+
print(f"error: --var must be KEY=VALUE, got: {var}", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
key, value = var.split("=", 1)
|
|
111
|
+
variables[key.strip()] = value.strip()
|
|
112
|
+
|
|
113
|
+
project_root = Path(args.path).resolve()
|
|
114
|
+
output_path = Path(args.output) if args.output else None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
out = apply_template(
|
|
118
|
+
args.name,
|
|
119
|
+
variables,
|
|
120
|
+
output_path=output_path,
|
|
121
|
+
project_root=project_root,
|
|
122
|
+
)
|
|
123
|
+
except ValueError as exc:
|
|
124
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
print(f"Rendered template to {out}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_templates_init(args: argparse.Namespace) -> int:
|
|
132
|
+
"""Create the custom template extension directory and README."""
|
|
133
|
+
project_root = Path(args.path).resolve()
|
|
134
|
+
templates_dir = project_root / ".devtorch" / "templates"
|
|
135
|
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
readme = templates_dir / "README.md"
|
|
138
|
+
content = """# Custom Reasoning Templates
|
|
139
|
+
|
|
140
|
+
Add Markdown templates to this directory to make them available via `devtorch templates`.
|
|
141
|
+
|
|
142
|
+
Each template should start with a YAML front-matter block:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
---
|
|
146
|
+
name: my_template
|
|
147
|
+
description: Short description shown in `devtorch templates list`
|
|
148
|
+
tags: [reasoning, custom]
|
|
149
|
+
---
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The body can contain `{{variable}}` placeholders. Render with:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
devtorch templates apply my_template --var key=value --output path/to/file.md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Templates are also discovered from the project-level `templates/` directory.
|
|
159
|
+
"""
|
|
160
|
+
readme.write_text(content, encoding="utf-8")
|
|
161
|
+
print(f"Created custom templates directory at {templates_dir}")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_concepts(raw: str | None) -> list[str] | None:
|
|
166
|
+
"""Parse comma-separated concepts, returning None if empty."""
|
|
167
|
+
if not raw:
|
|
168
|
+
return None
|
|
169
|
+
parts = [c.strip() for c in raw.split(",") if c.strip()]
|
|
170
|
+
return parts if parts else None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_commit(args: argparse.Namespace) -> int:
|
|
174
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
175
|
+
concepts = _parse_concepts(getattr(args, "concepts", None))
|
|
176
|
+
try:
|
|
177
|
+
repo.commit(message=args.message or "", concepts=concepts)
|
|
178
|
+
except RuntimeError as e:
|
|
179
|
+
print(f"error: {e}", file=sys.stderr)
|
|
180
|
+
return 1
|
|
181
|
+
if concepts:
|
|
182
|
+
print(f"Recorded commit in {repo.gcc_dir} [concepts: {', '.join(concepts)}]")
|
|
183
|
+
else:
|
|
184
|
+
print(f"Recorded commit in {repo.gcc_dir}")
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cmd_chain(args: argparse.Namespace) -> int:
|
|
189
|
+
from devtorch_core.reasoning_plus.learning.chain import build_chain, format_chains
|
|
190
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
191
|
+
if not repo.is_initialized():
|
|
192
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
193
|
+
return 1
|
|
194
|
+
chains = build_chain(
|
|
195
|
+
repo.gcc_dir,
|
|
196
|
+
concept=getattr(args, "concept", None),
|
|
197
|
+
max_chains=getattr(args, "max_chains", 5),
|
|
198
|
+
)
|
|
199
|
+
print(format_chains(chains))
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_welcome(args: argparse.Namespace) -> int:
|
|
204
|
+
print("Welcome to DevTorch — governance for AI-assisted codebases.")
|
|
205
|
+
print()
|
|
206
|
+
print("Quick start:")
|
|
207
|
+
print(" 1. devtorch init start tracking this project")
|
|
208
|
+
print(" 2. devtorch status see current state")
|
|
209
|
+
print(" 3. devtorch today daily dashboard")
|
|
210
|
+
print()
|
|
211
|
+
print("Common commands:")
|
|
212
|
+
print(" devtorch setup one-command IDE setup")
|
|
213
|
+
print(" devtorch doctor run health checks")
|
|
214
|
+
print(" devtorch log --tail 20 recent reasoning log")
|
|
215
|
+
print(" devtorch metrics --period 1d recent activity metrics")
|
|
216
|
+
print(" devtorch ask <query> search .GCC/ and project files")
|
|
217
|
+
print()
|
|
218
|
+
print("Need help? Run any command with --help, e.g. devtorch init --help")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_ask(args: argparse.Namespace) -> int:
|
|
223
|
+
"""Hybrid grep + semantic search over .GCC/ and project files."""
|
|
224
|
+
from devtorch_core.query import hybrid_search
|
|
225
|
+
|
|
226
|
+
project_root = Path(args.path).resolve()
|
|
227
|
+
if not (project_root / ".GCC").exists():
|
|
228
|
+
print(f"error: {project_root / '.GCC'} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
229
|
+
return 1
|
|
230
|
+
|
|
231
|
+
force_semantic = getattr(args, "semantic", False)
|
|
232
|
+
no_semantic = getattr(args, "no_semantic", False)
|
|
233
|
+
if force_semantic and no_semantic:
|
|
234
|
+
print("error: --semantic and --no-semantic are mutually exclusive.", file=sys.stderr)
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
if force_semantic:
|
|
238
|
+
mode = "semantic"
|
|
239
|
+
elif no_semantic:
|
|
240
|
+
mode = "grep"
|
|
241
|
+
else:
|
|
242
|
+
mode = "hybrid"
|
|
243
|
+
|
|
244
|
+
results = hybrid_search(
|
|
245
|
+
query=args.query,
|
|
246
|
+
project_root=project_root,
|
|
247
|
+
top_n=args.top_n,
|
|
248
|
+
mode=mode,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not results:
|
|
252
|
+
print("No results found.")
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
print(f"Results for: {args.query}\n")
|
|
256
|
+
for r in results:
|
|
257
|
+
rel_path = r.path
|
|
258
|
+
try:
|
|
259
|
+
rel_path = r.path.relative_to(project_root)
|
|
260
|
+
except ValueError:
|
|
261
|
+
pass
|
|
262
|
+
print(f"{rel_path}:{r.line} [{r.source}] score={r.score:.3f}")
|
|
263
|
+
print(f" {r.snippet}")
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
268
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
269
|
+
issues = repo.doctor()
|
|
270
|
+
if getattr(args, "ci", False):
|
|
271
|
+
# In CI the .GCC/ directory is often gitignored and not present; the
|
|
272
|
+
# reusable check is meant to validate code quality, not runtime state.
|
|
273
|
+
issues = [i for i in issues if not (i.kind == "layout" and ".GCC" in i.message and "Missing" in i.message)]
|
|
274
|
+
return _doctor_ci_mode(repo, issues, args)
|
|
275
|
+
if not issues:
|
|
276
|
+
print("devtorch doctor: OK")
|
|
277
|
+
return 0
|
|
278
|
+
print("devtorch doctor: found issues:")
|
|
279
|
+
for issue in issues:
|
|
280
|
+
print(f"- [{issue.kind}] {issue.message}")
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def cmd_alert(args: argparse.Namespace) -> int:
|
|
285
|
+
"""Send a test governance alert to configured channels."""
|
|
286
|
+
from devtorch_core.alerts import (
|
|
287
|
+
AlertEnvelope,
|
|
288
|
+
AlertSeverity,
|
|
289
|
+
dispatch_alert,
|
|
290
|
+
SlackChannel,
|
|
291
|
+
MicrosoftTeamsChannel,
|
|
292
|
+
JiraChannel,
|
|
293
|
+
LinearChannel,
|
|
294
|
+
PagerDutyChannel,
|
|
295
|
+
load_config,
|
|
296
|
+
AlertConfig,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
severity_map = {
|
|
300
|
+
"info": AlertSeverity.INFO,
|
|
301
|
+
"warning": AlertSeverity.WARNING,
|
|
302
|
+
"critical": AlertSeverity.CRITICAL,
|
|
303
|
+
}
|
|
304
|
+
severity = severity_map[args.severity]
|
|
305
|
+
envelope = AlertEnvelope(
|
|
306
|
+
event_type=args.event_type,
|
|
307
|
+
severity=severity,
|
|
308
|
+
title=args.title,
|
|
309
|
+
body=args.body,
|
|
310
|
+
branch="main",
|
|
311
|
+
commit_id="test",
|
|
312
|
+
metadata={"source": "devtorch-cli"},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
config = load_config()
|
|
316
|
+
if args.channel:
|
|
317
|
+
# Dispatch only to selected channel
|
|
318
|
+
channel = args.channel
|
|
319
|
+
if not config.is_channel_configured(channel):
|
|
320
|
+
print(f"error: channel '{channel}' is not configured. Set the required environment variables.", file=sys.stderr)
|
|
321
|
+
return 1
|
|
322
|
+
channels = []
|
|
323
|
+
if channel == "slack":
|
|
324
|
+
channels.append(SlackChannel(config.slack_webhook_url))
|
|
325
|
+
elif channel == "teams":
|
|
326
|
+
channels.append(MicrosoftTeamsChannel(config.teams_webhook_url))
|
|
327
|
+
elif channel == "jira":
|
|
328
|
+
channels.append(JiraChannel(config.jira_url, config.jira_project_key, config.jira_api_token, config.jira_email))
|
|
329
|
+
elif channel == "linear":
|
|
330
|
+
channels.append(LinearChannel(config.linear_api_token, config.linear_team_id))
|
|
331
|
+
elif channel == "pagerduty":
|
|
332
|
+
channels.append(PagerDutyChannel(config.pagerduty_routing_key))
|
|
333
|
+
results = [c.deliver(envelope) for c in channels]
|
|
334
|
+
else:
|
|
335
|
+
results = dispatch_alert(envelope, config=config)
|
|
336
|
+
|
|
337
|
+
if not results:
|
|
338
|
+
print("No alert channels configured. Set e.g. DEVTORCH_SLACK_WEBHOOK_URL and try again.")
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
exit_code = 0
|
|
342
|
+
for result in results:
|
|
343
|
+
status = "OK" if result.success else "FAIL"
|
|
344
|
+
detail = f" ({result.error})" if result.error else ""
|
|
345
|
+
print(f"{status}: {result.channel}{detail}")
|
|
346
|
+
if not result.success:
|
|
347
|
+
exit_code = 1
|
|
348
|
+
return exit_code
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def cmd_cloud_client_config(args: argparse.Namespace) -> int:
|
|
352
|
+
"""Install MCP client config for an IDE and update AGENTS.md."""
|
|
353
|
+
from devtorch_core.cloud.client_config import install_client_config
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
result = install_client_config(
|
|
357
|
+
ide=args.ide,
|
|
358
|
+
org=args.org,
|
|
359
|
+
repo=args.repo,
|
|
360
|
+
api_key=args.api_key,
|
|
361
|
+
domain=args.domain,
|
|
362
|
+
project_root=args.path,
|
|
363
|
+
use_env_key=getattr(args, "use_env_key", False),
|
|
364
|
+
)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
print(f"error: {e}", file=sys.stderr)
|
|
367
|
+
return 1
|
|
368
|
+
|
|
369
|
+
print(f"Installed {result['installed_file']}")
|
|
370
|
+
print(f"DevTorch MCP URL: {result['url']}")
|
|
371
|
+
if "warning" in result:
|
|
372
|
+
print(f"WARNING: {result['warning']}", file=sys.stderr)
|
|
373
|
+
if "warning_use_env_key" in result:
|
|
374
|
+
print(f"WARNING: {result['warning_use_env_key']}", file=sys.stderr)
|
|
375
|
+
if "secret_warning" in result:
|
|
376
|
+
print(f"WARNING: {result['secret_warning']}", file=sys.stderr)
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _doctor_ci_mode(repo: Any, issues: List[Any], args: argparse.Namespace) -> int:
|
|
381
|
+
"""CI mode: enforce DHS and I3 thresholds in addition to doctor checks."""
|
|
382
|
+
from devtorch_core.metrics.dhs import compute_dhs, DHSComponents, dhs_to_label
|
|
383
|
+
from devtorch_core.metrics.shadow_ai import detect_shadow_ai
|
|
384
|
+
|
|
385
|
+
min_dhs = float(getattr(args, "min_dhs_score", 0.0))
|
|
386
|
+
critical_concepts = [c.strip() for c in getattr(args, "critical_concepts", "").split(",") if c.strip()]
|
|
387
|
+
|
|
388
|
+
exit_code = 0
|
|
389
|
+
messages: List[str] = []
|
|
390
|
+
|
|
391
|
+
if issues:
|
|
392
|
+
exit_code = 1
|
|
393
|
+
messages.append(f"Doctor found {len(issues)} issue(s)")
|
|
394
|
+
for issue in issues:
|
|
395
|
+
messages.append(f" - [{issue.kind}] {issue.message}")
|
|
396
|
+
|
|
397
|
+
# DHS check
|
|
398
|
+
shadow = detect_shadow_ai(repo.root)
|
|
399
|
+
components = DHSComponents(
|
|
400
|
+
commit_health=1.0 if shadow.get("gcc_commits", 0) > 0 else 0.0,
|
|
401
|
+
sensitivity_health=1.0,
|
|
402
|
+
capability_health=0.0,
|
|
403
|
+
variance_health=1.0,
|
|
404
|
+
)
|
|
405
|
+
dhs_score = compute_dhs(components)
|
|
406
|
+
dhs_label = dhs_to_label(dhs_score)
|
|
407
|
+
if min_dhs > 0 and dhs_score < min_dhs:
|
|
408
|
+
exit_code = 1
|
|
409
|
+
messages.append(f"DHS {dhs_score:.2f} below threshold {min_dhs:.2f} ({dhs_label})")
|
|
410
|
+
else:
|
|
411
|
+
messages.append(f"DHS {dhs_score:.2f} ({dhs_label})")
|
|
412
|
+
|
|
413
|
+
# I3 critical-concepts check
|
|
414
|
+
if critical_concepts:
|
|
415
|
+
theta = repo.get_theta().get("coordination_vector", {})
|
|
416
|
+
missing = [c for c in critical_concepts if c not in theta]
|
|
417
|
+
if missing:
|
|
418
|
+
exit_code = 1
|
|
419
|
+
messages.append(f"Critical concepts missing from Θ: {', '.join(missing)}")
|
|
420
|
+
else:
|
|
421
|
+
messages.append(f"Critical concepts present in Θ: {', '.join(critical_concepts)}")
|
|
422
|
+
|
|
423
|
+
prefix = "PASS" if exit_code == 0 else "FAIL"
|
|
424
|
+
print(f"devtorch doctor --ci: {prefix}")
|
|
425
|
+
for msg in messages:
|
|
426
|
+
print(f" {msg}")
|
|
427
|
+
return exit_code
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def cmd_branch(args: argparse.Namespace) -> int:
|
|
431
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
432
|
+
try:
|
|
433
|
+
repo.create_branch(args.name, from_ref=args.from_ref)
|
|
434
|
+
except ValueError as e:
|
|
435
|
+
print(f"error: {e}", file=sys.stderr)
|
|
436
|
+
return 1
|
|
437
|
+
print(f"Created branch `{args.name}`.")
|
|
438
|
+
return 0
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def cmd_history(args: argparse.Namespace) -> int:
|
|
442
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
443
|
+
history = repo.history(branch=args.branch)
|
|
444
|
+
if not history:
|
|
445
|
+
print("No commits.")
|
|
446
|
+
return 0
|
|
447
|
+
for commit in history:
|
|
448
|
+
cid = commit.get("id")
|
|
449
|
+
msg = commit.get("message", "")
|
|
450
|
+
br = commit.get("branch")
|
|
451
|
+
parent = commit.get("parent")
|
|
452
|
+
ts = commit.get("timestamp")
|
|
453
|
+
print(f"{cid} [{br}] parent={parent} {ts} :: {msg}")
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def cmd_merge(args: argparse.Namespace) -> int:
|
|
458
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
459
|
+
try:
|
|
460
|
+
repo.merge_fast_forward(source=args.source, target=args.target)
|
|
461
|
+
except RuntimeError as e:
|
|
462
|
+
print(f"error: {e}", file=sys.stderr)
|
|
463
|
+
return 1
|
|
464
|
+
print(f"Merged `{args.source}` into `{args.target or repo._current_branch()}` (fast-forward).")
|
|
465
|
+
return 0
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def cmd_context(args: argparse.Namespace) -> int:
|
|
469
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
470
|
+
try:
|
|
471
|
+
bundle = repo.context_bundle(
|
|
472
|
+
k_tokens=args.k,
|
|
473
|
+
policy={"sensitivity_policy": args.policy},
|
|
474
|
+
branch=args.branch,
|
|
475
|
+
explain=args.explain,
|
|
476
|
+
)
|
|
477
|
+
except RuntimeError as e:
|
|
478
|
+
print(f"error: {e}", file=sys.stderr)
|
|
479
|
+
return 1
|
|
480
|
+
|
|
481
|
+
print(f"Context bundle for branch `{bundle['branch']}` "
|
|
482
|
+
f"(approx {bundle['approx_tokens_used']}/{bundle['k_tokens']} tokens):")
|
|
483
|
+
for art in bundle["artifacts"]:
|
|
484
|
+
print(f"- {art['path']} :: {art['reason']} "
|
|
485
|
+
f"(~{art['approx_tokens']} tokens)")
|
|
486
|
+
if args.explain and "bundle_path" in bundle:
|
|
487
|
+
print(f"Bundle persisted as `.GCC/{bundle['bundle_path']}`")
|
|
488
|
+
return 0
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def cmd_sensitivity_add(args: argparse.Namespace) -> int:
|
|
492
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
493
|
+
try:
|
|
494
|
+
event = SensitivityEvent(
|
|
495
|
+
source_node=args.source,
|
|
496
|
+
target_concept=args.concept,
|
|
497
|
+
counterfactuals=args.counterfactuals or "",
|
|
498
|
+
confidence=args.confidence,
|
|
499
|
+
disclosure_level=args.disclosure.upper(),
|
|
500
|
+
numerical_value=args.numerical,
|
|
501
|
+
uncertainty=args.uncertainty,
|
|
502
|
+
message=args.message or "",
|
|
503
|
+
)
|
|
504
|
+
event_id = repo.add_sensitivity(event)
|
|
505
|
+
except (ValueError, RuntimeError) as e:
|
|
506
|
+
print(f"error: {e}", file=sys.stderr)
|
|
507
|
+
return 1
|
|
508
|
+
print(f"Sensitivity recorded (id={event_id}).")
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def cmd_sensitivity_list(args: argparse.Namespace) -> int:
|
|
513
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
514
|
+
try:
|
|
515
|
+
events = repo.list_sensitivities(
|
|
516
|
+
concept=args.concept,
|
|
517
|
+
disclosure_level=args.disclosure.upper() if args.disclosure else None,
|
|
518
|
+
source_node=args.source_node,
|
|
519
|
+
)
|
|
520
|
+
except RuntimeError as e:
|
|
521
|
+
print(f"error: {e}", file=sys.stderr)
|
|
522
|
+
return 1
|
|
523
|
+
if not events:
|
|
524
|
+
print("No sensitivity events found.")
|
|
525
|
+
return 0
|
|
526
|
+
for ev in events:
|
|
527
|
+
num = f" num={ev['numerical_value']}" if ev.get("numerical_value") is not None else ""
|
|
528
|
+
print(
|
|
529
|
+
f"[{ev['disclosure_level']}] {ev['source_node']} → {ev['target_concept']} "
|
|
530
|
+
f"conf={ev['confidence']}{num} id={ev['event_id']}"
|
|
531
|
+
)
|
|
532
|
+
return 0
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def cmd_sensitivity_filter(args: argparse.Namespace) -> int:
|
|
536
|
+
"""Alias for `sensitivity list` with a mandatory --disclosure filter."""
|
|
537
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
538
|
+
try:
|
|
539
|
+
events = repo.list_sensitivities(
|
|
540
|
+
concept=args.concept,
|
|
541
|
+
disclosure_level=args.disclosure.upper() if args.disclosure else None,
|
|
542
|
+
source_node=args.source_node,
|
|
543
|
+
)
|
|
544
|
+
except RuntimeError as e:
|
|
545
|
+
print(f"error: {e}", file=sys.stderr)
|
|
546
|
+
return 1
|
|
547
|
+
theta = repo.get_theta()
|
|
548
|
+
cv = theta.get("coordination_vector", {})
|
|
549
|
+
|
|
550
|
+
print(f"Filtered sensitivities (disclosure={args.disclosure or 'any'}, "
|
|
551
|
+
f"concept={args.concept or 'any'}):")
|
|
552
|
+
for ev in events:
|
|
553
|
+
num = f" num={ev['numerical_value']}" if ev.get("numerical_value") is not None else ""
|
|
554
|
+
print(
|
|
555
|
+
f" [{ev['disclosure_level']}] {ev['source_node']} → {ev['target_concept']} "
|
|
556
|
+
f"conf={ev['confidence']}{num}"
|
|
557
|
+
)
|
|
558
|
+
print(f"\nΘ coordination vector (all concepts):")
|
|
559
|
+
for concept, entry in cv.items():
|
|
560
|
+
print(
|
|
561
|
+
f" {concept}: mean_conf={entry['mean_confidence']} "
|
|
562
|
+
f"level={entry['disclosure_level']} n={entry['event_count']}"
|
|
563
|
+
)
|
|
564
|
+
return 0
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def cmd_capability_set(args: argparse.Namespace) -> int:
|
|
568
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
569
|
+
try:
|
|
570
|
+
cap = OmegaCapability(
|
|
571
|
+
agent_id=args.agent_id,
|
|
572
|
+
lipschitz_bound=args.lipschitz_bound,
|
|
573
|
+
calibration_dataset_hash=args.calibration_hash,
|
|
574
|
+
embedding_model=args.embedding_model,
|
|
575
|
+
calibration_date=args.calibration_date,
|
|
576
|
+
temperature_used=args.temperature,
|
|
577
|
+
lipschitz_p95=args.lipschitz_p95,
|
|
578
|
+
expiry_days=args.expiry_days,
|
|
579
|
+
)
|
|
580
|
+
repo.set_capability(cap)
|
|
581
|
+
except (ValueError, RuntimeError) as e:
|
|
582
|
+
print(f"error: {e}", file=sys.stderr)
|
|
583
|
+
return 1
|
|
584
|
+
print(f"Capability declaration set for agent '{cap.agent_id}'.")
|
|
585
|
+
return 0
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def cmd_capability_show(args: argparse.Namespace) -> int:
|
|
589
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
590
|
+
try:
|
|
591
|
+
cap = repo.get_capability()
|
|
592
|
+
except RuntimeError as e:
|
|
593
|
+
print(f"error: {e}", file=sys.stderr)
|
|
594
|
+
return 1
|
|
595
|
+
if cap is None:
|
|
596
|
+
print("No capability declaration set. Run `devtorch capability set` first.")
|
|
597
|
+
return 1
|
|
598
|
+
print(f"Ωᵢ capability for agent '{cap['agent_id']}':")
|
|
599
|
+
for key in (
|
|
600
|
+
"schema_version", "lipschitz_bound", "lipschitz_p95",
|
|
601
|
+
"embedding_model", "calibration_date", "temperature_used",
|
|
602
|
+
"expiry_days", "pst_status", "pst_score",
|
|
603
|
+
"pst_n_prompts", "pst_n_repetitions", "calibration_dataset_hash",
|
|
604
|
+
"created_at", "updated_at",
|
|
605
|
+
):
|
|
606
|
+
val = cap.get(key)
|
|
607
|
+
if val is not None:
|
|
608
|
+
print(f" {key}: {val}")
|
|
609
|
+
return 0
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def cmd_capability_validate(args: argparse.Namespace) -> int:
|
|
613
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
614
|
+
try:
|
|
615
|
+
result = repo.validate_capability(l_max=args.l_max)
|
|
616
|
+
except RuntimeError as e:
|
|
617
|
+
print(f"error: {e}", file=sys.stderr)
|
|
618
|
+
return 1
|
|
619
|
+
if result["textual_mode_allowed"]:
|
|
620
|
+
print(f"Capability valid. Textual Mode: ALLOWED (agent={result['agent_id']}).")
|
|
621
|
+
return 0
|
|
622
|
+
print(f"Capability DEGRADED. Textual Mode: BLOCKED (agent={result['agent_id']}).")
|
|
623
|
+
for reason in result["reasons"]:
|
|
624
|
+
print(f" - {reason}")
|
|
625
|
+
return 1
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# Sprint 4: lock, unlock, invariant, concept, variance
|
|
629
|
+
|
|
630
|
+
def cmd_lock(args: argparse.Namespace) -> int:
|
|
631
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
632
|
+
try:
|
|
633
|
+
repo.lock(reason=args.reason or "", required_peer_count=args.peers)
|
|
634
|
+
except RuntimeError as e:
|
|
635
|
+
print(f"error: {e}", file=sys.stderr)
|
|
636
|
+
return 1
|
|
637
|
+
print("Consensus lock acquired.")
|
|
638
|
+
return 0
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def cmd_unlock(args: argparse.Namespace) -> int:
|
|
642
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
643
|
+
try:
|
|
644
|
+
repo.unlock()
|
|
645
|
+
except RuntimeError as e:
|
|
646
|
+
print(f"error: {e}", file=sys.stderr)
|
|
647
|
+
return 1
|
|
648
|
+
print("Consensus lock released.")
|
|
649
|
+
return 0
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def cmd_invariant_check(args: argparse.Namespace) -> int:
|
|
653
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
654
|
+
try:
|
|
655
|
+
concepts_raw = (args.concepts or "").strip()
|
|
656
|
+
concepts_used = [c.strip() for c in concepts_raw.split(",") if c.strip()] or None
|
|
657
|
+
failures = repo.invariant_check(
|
|
658
|
+
operation=args.operation or "merge",
|
|
659
|
+
branch_tips=None,
|
|
660
|
+
concepts_used=concepts_used,
|
|
661
|
+
)
|
|
662
|
+
except RuntimeError as e:
|
|
663
|
+
print(f"error: {e}", file=sys.stderr)
|
|
664
|
+
return 1
|
|
665
|
+
if not failures:
|
|
666
|
+
print("All invariant checks passed.")
|
|
667
|
+
return 0
|
|
668
|
+
print("Invariant check(s) failed:")
|
|
669
|
+
for f in failures:
|
|
670
|
+
print(f" [{f.invariant_id}] {f.message}")
|
|
671
|
+
print(f" Fix: {f.actionable_fix}")
|
|
672
|
+
return 1
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def cmd_concept_add(args: argparse.Namespace) -> int:
|
|
676
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
677
|
+
try:
|
|
678
|
+
repo.concept_add(args.name, args.definition)
|
|
679
|
+
except RuntimeError as e:
|
|
680
|
+
print(f"error: {e}", file=sys.stderr)
|
|
681
|
+
return 1
|
|
682
|
+
print(f"Concept '{args.name}' registered.")
|
|
683
|
+
return 0
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def cmd_concept_list(args: argparse.Namespace) -> int:
|
|
687
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
688
|
+
try:
|
|
689
|
+
names = repo.concept_list()
|
|
690
|
+
except RuntimeError as e:
|
|
691
|
+
print(f"error: {e}", file=sys.stderr)
|
|
692
|
+
return 1
|
|
693
|
+
if not names:
|
|
694
|
+
print("No concepts registered.")
|
|
695
|
+
return 0
|
|
696
|
+
for n in names:
|
|
697
|
+
print(n)
|
|
698
|
+
return 0
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def cmd_concept_danger(args: argparse.Namespace) -> int:
|
|
702
|
+
from devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
703
|
+
from devtorch_core.reasoning_plus.learning.analytics import danger_zones
|
|
704
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
705
|
+
if not repo.is_initialized():
|
|
706
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
707
|
+
return 1
|
|
708
|
+
store = LearningStore(repo.gcc_dir)
|
|
709
|
+
zones = danger_zones(store, threshold=args.threshold, min_samples=args.min_samples)
|
|
710
|
+
if not zones:
|
|
711
|
+
print(f"No danger zones found (threshold={args.threshold}, min_samples={args.min_samples}).")
|
|
712
|
+
return 0
|
|
713
|
+
print(f"Danger zones (success rate < {args.threshold:.0%}):")
|
|
714
|
+
for z in zones:
|
|
715
|
+
print(
|
|
716
|
+
f" {z['concept']}: success {z['success_rate']:.0%}, "
|
|
717
|
+
f"{z['total_learnings']} learning(s), "
|
|
718
|
+
f"avg confidence {z['avg_confidence']:.2f}"
|
|
719
|
+
)
|
|
720
|
+
return 0
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _try_remote_or_local(
|
|
724
|
+
args: argparse.Namespace,
|
|
725
|
+
tool_name: str,
|
|
726
|
+
tool_args: dict,
|
|
727
|
+
local_fn: Any,
|
|
728
|
+
) -> int | None:
|
|
729
|
+
"""Try remote MCP first, fall back to local on failure.
|
|
730
|
+
|
|
731
|
+
Returns an int return code if remote succeeded, or None if the caller
|
|
732
|
+
should proceed with the local fallback.
|
|
733
|
+
"""
|
|
734
|
+
from devtorch_core.cloud.mcp_client import detect_mcp_config, McpClient
|
|
735
|
+
|
|
736
|
+
root = Path(args.path).resolve()
|
|
737
|
+
config = detect_mcp_config(root)
|
|
738
|
+
|
|
739
|
+
if config:
|
|
740
|
+
url, api_key = config
|
|
741
|
+
try:
|
|
742
|
+
client = McpClient(url, api_key, timeout=10.0)
|
|
743
|
+
result = client.call_tool(tool_name, tool_args)
|
|
744
|
+
print(result)
|
|
745
|
+
return 0
|
|
746
|
+
except Exception as e:
|
|
747
|
+
print(f"Remote MCP unavailable ({e}), falling back to local .GCC/...")
|
|
748
|
+
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def cmd_concept_list_all(args: argparse.Namespace) -> int:
|
|
753
|
+
rc = _try_remote_or_local(args, "devtorch_concepts_list", {"learned_only": False}, None)
|
|
754
|
+
if rc is not None:
|
|
755
|
+
return rc
|
|
756
|
+
|
|
757
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
758
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
759
|
+
if not repo.is_initialized():
|
|
760
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
761
|
+
return 1
|
|
762
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
763
|
+
all_concepts = catalog.enumerate_all()
|
|
764
|
+
if not all_concepts:
|
|
765
|
+
print("No concepts found.")
|
|
766
|
+
return 0
|
|
767
|
+
print(f"All concepts ({len(all_concepts)} from multiple sources):\n")
|
|
768
|
+
for cname, sources in all_concepts.items():
|
|
769
|
+
print(f" {cname:<20} {', '.join(sources)}")
|
|
770
|
+
return 0
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def cmd_concept_learned(args: argparse.Namespace) -> int:
|
|
774
|
+
rc = _try_remote_or_local(args, "devtorch_concepts_list", {"learned_only": True}, None)
|
|
775
|
+
if rc is not None:
|
|
776
|
+
return rc
|
|
777
|
+
|
|
778
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
779
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
780
|
+
if not repo.is_initialized():
|
|
781
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
782
|
+
return 1
|
|
783
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
784
|
+
learned = catalog.enumerate_learned()
|
|
785
|
+
if not learned:
|
|
786
|
+
print("No concepts with learning data found.")
|
|
787
|
+
return 0
|
|
788
|
+
print("Concepts learned from interactions:\n")
|
|
789
|
+
print(f" {'Concept':<16} {'Learnings':>9} {'Active':>7} {'Success':>8} {'Confidence':>10}")
|
|
790
|
+
print(f" {'─'*16} {'─'*9} {'─'*7} {'─'*8} {'─'*10}")
|
|
791
|
+
for cname, stats in sorted(learned.items()):
|
|
792
|
+
danger = " DANGER" if stats["success_rate"] < 0.4 else ""
|
|
793
|
+
print(
|
|
794
|
+
f" {cname:<16} {stats['learning_count']:>9} {stats['active']:>7} "
|
|
795
|
+
f"{stats['success_rate']:>7.0%} {stats['avg_confidence']:>10.2f}{danger}"
|
|
796
|
+
)
|
|
797
|
+
print(f"\n{len(learned)} concepts with learning data.")
|
|
798
|
+
return 0
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def cmd_concept_related(args: argparse.Namespace) -> int:
|
|
802
|
+
rc = _try_remote_or_local(args, "devtorch_concepts_related", {"topic": args.topic}, None)
|
|
803
|
+
if rc is not None:
|
|
804
|
+
return rc
|
|
805
|
+
|
|
806
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
807
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
808
|
+
if not repo.is_initialized():
|
|
809
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
810
|
+
return 1
|
|
811
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
812
|
+
result = catalog.concepts_for_topic(args.topic, hide_empty=True)
|
|
813
|
+
concepts = result.get("concepts", [])
|
|
814
|
+
taxonomy_only = result.get("taxonomy_only", [])
|
|
815
|
+
if not concepts and not taxonomy_only:
|
|
816
|
+
print(f"Topic '{args.topic}' not found or has no concepts.")
|
|
817
|
+
return 0
|
|
818
|
+
print(f"Concepts related to topic '{args.topic}':\n")
|
|
819
|
+
for c in concepts:
|
|
820
|
+
parts = [f" {c['name']:<16}"]
|
|
821
|
+
parts.append(f"sources: {', '.join(c['sources'])}")
|
|
822
|
+
if "learning_count" in c:
|
|
823
|
+
parts.append(f"{c['learning_count']} learnings, {c['success_rate']:.0%} success")
|
|
824
|
+
print(" — ".join(parts))
|
|
825
|
+
if taxonomy_only:
|
|
826
|
+
print(f"\n Also in taxonomy (no data): {', '.join(taxonomy_only)}")
|
|
827
|
+
print(f"\n{len(concepts)} concept(s) with data. Use 'devtorch reasoning history --topic {args.topic}' for full history.")
|
|
828
|
+
return 0
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def cmd_topics_list(args: argparse.Namespace) -> int:
|
|
832
|
+
show_all = getattr(args, "all", False)
|
|
833
|
+
rc = _try_remote_or_local(args, "devtorch_topics_list", {"all": show_all}, None)
|
|
834
|
+
if rc is not None:
|
|
835
|
+
return rc
|
|
836
|
+
|
|
837
|
+
from devtorch_core.topics import TopicStore
|
|
838
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
839
|
+
if not repo.is_initialized():
|
|
840
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
841
|
+
return 1
|
|
842
|
+
store = TopicStore(repo.gcc_dir)
|
|
843
|
+
topics = store.list_topics(with_data_only=not show_all)
|
|
844
|
+
if not topics:
|
|
845
|
+
print("No topics found." if show_all else "No topics with captured reasoning. Use --all to see full taxonomy.")
|
|
846
|
+
return 0
|
|
847
|
+
header = "Topics" + (" (full taxonomy):\n" if show_all else " with captured reasoning:\n")
|
|
848
|
+
print(header)
|
|
849
|
+
for t in topics:
|
|
850
|
+
tag = f" [{t['source']}]" if t["source"] != "default" else ""
|
|
851
|
+
print(f" {t['name']:<20} ({len(t['concepts'])} concepts: {', '.join(t['concepts'])}){tag}")
|
|
852
|
+
total = len(topics)
|
|
853
|
+
defaults = sum(1 for t in topics if t["source"] == "default")
|
|
854
|
+
customs = sum(1 for t in topics if t["source"] == "custom")
|
|
855
|
+
derived = sum(1 for t in topics if t["source"] == "derived")
|
|
856
|
+
print(f"\n{total} topics ({defaults} default, {customs} custom, {derived} auto-derived)")
|
|
857
|
+
return 0
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def cmd_topics_show(args: argparse.Namespace) -> int:
|
|
861
|
+
from devtorch_core.topics import TopicStore
|
|
862
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
863
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
864
|
+
if not repo.is_initialized():
|
|
865
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
866
|
+
return 1
|
|
867
|
+
store = TopicStore(repo.gcc_dir)
|
|
868
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
869
|
+
concepts = store.get_concepts(args.name)
|
|
870
|
+
if not concepts:
|
|
871
|
+
print(f"Topic '{args.name}' not found.")
|
|
872
|
+
return 1
|
|
873
|
+
topics = store.list_topics(with_data_only=False)
|
|
874
|
+
topic_meta = next((t for t in topics if t["name"] == args.name), None)
|
|
875
|
+
source_tag = f" [{topic_meta['source']}]" if topic_meta and topic_meta["source"] != "default" else ""
|
|
876
|
+
print(f"Topic: {args.name}{source_tag}\n")
|
|
877
|
+
|
|
878
|
+
all_concepts = catalog.enumerate_all()
|
|
879
|
+
learned = catalog.enumerate_learned()
|
|
880
|
+
with_data = []
|
|
881
|
+
without_data = []
|
|
882
|
+
for c in concepts:
|
|
883
|
+
sources = all_concepts.get(c.lower(), [])
|
|
884
|
+
if sources:
|
|
885
|
+
entry = f" {c:<16} [sources: {', '.join(sources)}]"
|
|
886
|
+
if c.lower() in learned:
|
|
887
|
+
stats = learned[c.lower()]
|
|
888
|
+
entry += f" — {stats['learning_count']} learnings, {stats['success_rate']:.0%} success"
|
|
889
|
+
with_data.append(entry)
|
|
890
|
+
else:
|
|
891
|
+
without_data.append(c)
|
|
892
|
+
|
|
893
|
+
if with_data:
|
|
894
|
+
print("Concepts with data:")
|
|
895
|
+
for line in with_data:
|
|
896
|
+
print(line)
|
|
897
|
+
if without_data:
|
|
898
|
+
print(f"\nAlso in taxonomy (no data yet):\n {', '.join(without_data)}")
|
|
899
|
+
if not with_data and not without_data:
|
|
900
|
+
print("No concepts found for this topic.")
|
|
901
|
+
return 0
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def cmd_topics_add(args: argparse.Namespace) -> int:
|
|
905
|
+
from devtorch_core.topics import TopicStore
|
|
906
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
907
|
+
if not repo.is_initialized():
|
|
908
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
909
|
+
return 1
|
|
910
|
+
concepts = _parse_concepts(args.concepts) or []
|
|
911
|
+
store = TopicStore(repo.gcc_dir)
|
|
912
|
+
store.add_topic(args.name, concepts)
|
|
913
|
+
print(f"Topic '{args.name}' registered with {len(concepts)} concept(s).")
|
|
914
|
+
return 0
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def cmd_topics_remove(args: argparse.Namespace) -> int:
|
|
918
|
+
from devtorch_core.topics import TopicStore
|
|
919
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
920
|
+
if not repo.is_initialized():
|
|
921
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
922
|
+
return 1
|
|
923
|
+
store = TopicStore(repo.gcc_dir)
|
|
924
|
+
removed = store.remove_topic(args.name)
|
|
925
|
+
if removed:
|
|
926
|
+
print(f"Topic '{args.name}' removed.")
|
|
927
|
+
else:
|
|
928
|
+
print(f"Topic '{args.name}' not found.")
|
|
929
|
+
return 1
|
|
930
|
+
return 0
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def cmd_topics_derive(args: argparse.Namespace) -> int:
|
|
934
|
+
from devtorch_core.topics import TopicStore
|
|
935
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
936
|
+
if not repo.is_initialized():
|
|
937
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
938
|
+
return 1
|
|
939
|
+
store = TopicStore(repo.gcc_dir)
|
|
940
|
+
derived = store.derive_topics(force=getattr(args, "force", False))
|
|
941
|
+
if not derived:
|
|
942
|
+
print("No topics could be derived from concept co-occurrence (need >= 3 co-occurring concepts).")
|
|
943
|
+
return 0
|
|
944
|
+
print(f"Derived {len(derived)} topic(s) from concept co-occurrence:")
|
|
945
|
+
for label, concepts in derived.items():
|
|
946
|
+
print(f" {label:<20} ({len(concepts)} concepts: {', '.join(concepts)})")
|
|
947
|
+
print("\nRe-derivation skipped on next call unless commits or learnings change.")
|
|
948
|
+
return 0
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def cmd_reasoning_history(args: argparse.Namespace) -> int:
|
|
952
|
+
topic = getattr(args, "topic", None)
|
|
953
|
+
concept = getattr(args, "concept", None)
|
|
954
|
+
max_entries = getattr(args, "max_entries", 50)
|
|
955
|
+
agent_id = getattr(args, "agent_id", None)
|
|
956
|
+
|
|
957
|
+
if not topic and not concept:
|
|
958
|
+
print("Provide either --topic or --concept.", file=sys.stderr)
|
|
959
|
+
return 1
|
|
960
|
+
|
|
961
|
+
tool_args = {"max_entries": max_entries}
|
|
962
|
+
if topic:
|
|
963
|
+
tool_args["topic"] = topic
|
|
964
|
+
if concept:
|
|
965
|
+
tool_args["concept"] = concept
|
|
966
|
+
if agent_id:
|
|
967
|
+
tool_args["agent_id"] = agent_id
|
|
968
|
+
|
|
969
|
+
rc = _try_remote_or_local(args, "devtorch_reasoning_history", tool_args, None)
|
|
970
|
+
if rc is not None:
|
|
971
|
+
return rc
|
|
972
|
+
|
|
973
|
+
from devtorch_core.reasoning import ReasoningStore
|
|
974
|
+
from devtorch_core.topics import TopicStore
|
|
975
|
+
|
|
976
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
977
|
+
if not repo.is_initialized():
|
|
978
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
979
|
+
return 1
|
|
980
|
+
store = ReasoningStore(repo)
|
|
981
|
+
|
|
982
|
+
if concept:
|
|
983
|
+
entries = store.query(concept=concept, agent_id=agent_id, k_tokens=args.tokens)
|
|
984
|
+
if not entries:
|
|
985
|
+
print(f"No reasoning entries found for concept '{concept}'.")
|
|
986
|
+
return 0
|
|
987
|
+
print(f"Reasoning for '{concept}' ({len(entries)} entries):\n")
|
|
988
|
+
for e in entries[:max_entries]:
|
|
989
|
+
print(f" [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]")
|
|
990
|
+
print(f" Reasoning: {e.reasoning_text}")
|
|
991
|
+
if e.decision_text:
|
|
992
|
+
print(f" Decision: {e.decision_text}")
|
|
993
|
+
print()
|
|
994
|
+
return 0
|
|
995
|
+
|
|
996
|
+
if topic:
|
|
997
|
+
topic_store = TopicStore(repo.gcc_dir)
|
|
998
|
+
concepts = topic_store.get_concepts(topic)
|
|
999
|
+
if not concepts:
|
|
1000
|
+
print(f"Topic '{topic}' not found.")
|
|
1001
|
+
return 1
|
|
1002
|
+
print(f"Reasoning history for topic '{topic}' ({len(concepts)} concepts):\n")
|
|
1003
|
+
total = 0
|
|
1004
|
+
for c in concepts:
|
|
1005
|
+
entries = store.query(concept=c, agent_id=agent_id, k_tokens=args.tokens)
|
|
1006
|
+
if entries:
|
|
1007
|
+
shown = entries[:max(0, max_entries - total)]
|
|
1008
|
+
if not shown:
|
|
1009
|
+
break
|
|
1010
|
+
print(f"═══ {c} ({len(entries)} entries, showing {len(shown)}) ═══")
|
|
1011
|
+
for e in shown:
|
|
1012
|
+
print(f" [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]")
|
|
1013
|
+
print(f" Reasoning: {e.reasoning_text}")
|
|
1014
|
+
if e.decision_text:
|
|
1015
|
+
print(f" Decision: {e.decision_text}")
|
|
1016
|
+
print()
|
|
1017
|
+
total += len(shown)
|
|
1018
|
+
if total >= max_entries:
|
|
1019
|
+
break
|
|
1020
|
+
print(f"Total: {total} reasoning entries (capped at --max-entries {max_entries}) across {len(concepts)} concepts.")
|
|
1021
|
+
return 0
|
|
1022
|
+
|
|
1023
|
+
return 0
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def cmd_learning_stats(args: argparse.Namespace) -> int:
|
|
1027
|
+
from devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
1028
|
+
from devtorch_core.reasoning_plus.learning.analytics import concept_stats
|
|
1029
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1030
|
+
if not repo.is_initialized():
|
|
1031
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
1032
|
+
return 1
|
|
1033
|
+
store = LearningStore(repo.gcc_dir)
|
|
1034
|
+
stats = concept_stats(store)
|
|
1035
|
+
if not stats:
|
|
1036
|
+
print("No learnings found.")
|
|
1037
|
+
return 0
|
|
1038
|
+
|
|
1039
|
+
if args.by_concept:
|
|
1040
|
+
for cname, data in stats.items():
|
|
1041
|
+
print(f"\n [{cname}]")
|
|
1042
|
+
print(f" Total: {data['total']}")
|
|
1043
|
+
print(f" Active: {data['active']} Stale: {data['stale']} Deprecated: {data['deprecated']}")
|
|
1044
|
+
print(f" Avg confidence: {data['avg_confidence']:.2f}")
|
|
1045
|
+
print(f" Success rate: {data['success_rate']:.0%}")
|
|
1046
|
+
if data["by_type"]:
|
|
1047
|
+
types = ", ".join(f"{t}: {c}" for t, c in data["by_type"].most_common())
|
|
1048
|
+
print(f" By type: {types}")
|
|
1049
|
+
else:
|
|
1050
|
+
total_all = sum(d["total"] for d in stats.values())
|
|
1051
|
+
active_all = sum(d["active"] for d in stats.values())
|
|
1052
|
+
avg_conf_all = sum(d["avg_confidence"] * d["total"] for d in stats.values()) / total_all if total_all else 0
|
|
1053
|
+
concepts_with_data = [c for c in stats if c != "__untyped__"]
|
|
1054
|
+
print(f"Total learnings: {total_all}")
|
|
1055
|
+
print(f"Active: {active_all}")
|
|
1056
|
+
print(f"Average confidence: {avg_conf_all:.2f}")
|
|
1057
|
+
print(f"Concepts tracked: {len(concepts_with_data)}")
|
|
1058
|
+
print(f"Danger zones: {sum(1 for d in stats.values() if d['success_rate'] < 0.4)}")
|
|
1059
|
+
return 0
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def cmd_variance_calibrate(args: argparse.Namespace) -> int:
|
|
1063
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1064
|
+
# Parse agent variances: key=value[,key=value...]
|
|
1065
|
+
agent_variances = {}
|
|
1066
|
+
if getattr(args, "variances", None):
|
|
1067
|
+
for part in args.variances.split(","):
|
|
1068
|
+
part = part.strip()
|
|
1069
|
+
if "=" in part:
|
|
1070
|
+
k, v = part.split("=", 1)
|
|
1071
|
+
agent_variances[k.strip()] = float(v.strip())
|
|
1072
|
+
try:
|
|
1073
|
+
report = repo.variance_calibrate(agent_variances)
|
|
1074
|
+
except RuntimeError as e:
|
|
1075
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1076
|
+
return 1
|
|
1077
|
+
print(f"Variance calibration: n={report.n}, σ²_obs={report.sigma_sq_obs:.4f}, f_max={report.f_max}")
|
|
1078
|
+
return 0
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def cmd_variance_monitor(args: argparse.Namespace) -> int:
|
|
1082
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1083
|
+
agent_variances = None
|
|
1084
|
+
if getattr(args, "variances", None):
|
|
1085
|
+
agent_variances = {}
|
|
1086
|
+
for part in args.variances.split(","):
|
|
1087
|
+
part = part.strip()
|
|
1088
|
+
if "=" in part:
|
|
1089
|
+
k, v = part.split("=", 1)
|
|
1090
|
+
agent_variances[k.strip()] = float(v.strip())
|
|
1091
|
+
try:
|
|
1092
|
+
result = repo.variance_monitor_run(agent_variances)
|
|
1093
|
+
except RuntimeError as e:
|
|
1094
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1095
|
+
return 1
|
|
1096
|
+
print(f"Variance monitor: f_max_live={result['f_max_live']}, previous_f_max={result.get('previous_f_max')}, "
|
|
1097
|
+
f"σ²_obs={result.get('sigma_sq_obs', 0):.4f}, alert_emitted={result.get('alert_emitted')}, "
|
|
1098
|
+
f"deterministic_mode_forced={result.get('deterministic_mode_forced')}")
|
|
1099
|
+
return 0
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
# Sprint 5: REP, SIS-TC, prompt, deltaf, RDP, disclosure
|
|
1103
|
+
|
|
1104
|
+
def cmd_rep_append(args: argparse.Namespace) -> int:
|
|
1105
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1106
|
+
try:
|
|
1107
|
+
payload = json.loads(args.payload) if args.payload else {}
|
|
1108
|
+
seq = repo.rep_append(args.agent_id, args.round_id, payload, trust=args.trust or 0.5)
|
|
1109
|
+
except RuntimeError as e:
|
|
1110
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1111
|
+
return 1
|
|
1112
|
+
print(f"REP envelope appended (sequence_id={seq}).")
|
|
1113
|
+
return 0
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def cmd_rep_checksum(args: argparse.Namespace) -> int:
|
|
1117
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1118
|
+
cs = repo.rep_replay_checksum()
|
|
1119
|
+
if not cs:
|
|
1120
|
+
print("No REP ledger or repo not initialized.")
|
|
1121
|
+
return 0
|
|
1122
|
+
print(cs)
|
|
1123
|
+
return 0
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def cmd_sis_tc_run(args: argparse.Namespace) -> int:
|
|
1127
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1128
|
+
try:
|
|
1129
|
+
report = repo.sis_tc_run(corpus_path=Path(args.corpus) if getattr(args, "corpus", None) else None)
|
|
1130
|
+
except RuntimeError as e:
|
|
1131
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1132
|
+
return 1
|
|
1133
|
+
print(f"SIS-TC report: n={report.n_total}, FPR={report.fpr:.4f}, FNR={report.fnr:.4f}")
|
|
1134
|
+
return 0
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def cmd_prompt_set(args: argparse.Namespace) -> int:
|
|
1138
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1139
|
+
content = args.content if hasattr(args, "content") and args.content else ""
|
|
1140
|
+
if not content and hasattr(args, "file") and args.file:
|
|
1141
|
+
content = Path(args.file).read_text(encoding="utf-8")
|
|
1142
|
+
try:
|
|
1143
|
+
repo.prompt_set(content, version=getattr(args, "version", "v2.1") or "v2.1")
|
|
1144
|
+
except RuntimeError as e:
|
|
1145
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1146
|
+
return 1
|
|
1147
|
+
print("RACP_SYSTEM_PROMPT_v2.1 stored.")
|
|
1148
|
+
return 0
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def cmd_prompt_show(args: argparse.Namespace) -> int:
|
|
1152
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1153
|
+
content = repo.prompt_get()
|
|
1154
|
+
if content is None:
|
|
1155
|
+
print("No system prompt set.")
|
|
1156
|
+
return 0
|
|
1157
|
+
print(content)
|
|
1158
|
+
return 0
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def cmd_deltaf_run(args: argparse.Namespace) -> int:
|
|
1162
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1163
|
+
try:
|
|
1164
|
+
report = repo.deltaf_run(expiry_days=getattr(args, "expiry_days", None) or 90)
|
|
1165
|
+
except RuntimeError as e:
|
|
1166
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1167
|
+
return 1
|
|
1168
|
+
print(f"Δf report: delta_f={report.delta_f}, expiry={report.expiry_date}")
|
|
1169
|
+
return 0
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def cmd_rdp_spend(args: argparse.Namespace) -> int:
|
|
1173
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1174
|
+
try:
|
|
1175
|
+
allowed, event = repo.rdp_spend(args.round_id, args.epsilon)
|
|
1176
|
+
except RuntimeError as e:
|
|
1177
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1178
|
+
return 1
|
|
1179
|
+
if not allowed:
|
|
1180
|
+
print(f"Privacy budget exhausted: {event}")
|
|
1181
|
+
return 1
|
|
1182
|
+
print("Epsilon spend recorded.")
|
|
1183
|
+
return 0
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def cmd_rdp_status(args: argparse.Namespace) -> int:
|
|
1187
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1188
|
+
ro = repo.rdp_read_only()
|
|
1189
|
+
print("read_only=True" if ro else "read_only=False")
|
|
1190
|
+
return 0
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def cmd_disclosure_redact(args: argparse.Namespace) -> int:
|
|
1194
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1195
|
+
text = args.text if getattr(args, "text", None) else sys.stdin.read()
|
|
1196
|
+
try:
|
|
1197
|
+
out = repo.disclosure_redact(text, padding_length=getattr(args, "padding", None))
|
|
1198
|
+
except RuntimeError as e:
|
|
1199
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1200
|
+
return 1
|
|
1201
|
+
print(out, end="")
|
|
1202
|
+
return 0
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
# Sprint 19: reasoning subcommand
|
|
1206
|
+
|
|
1207
|
+
def cmd_reasoning_show(args: argparse.Namespace) -> int:
|
|
1208
|
+
rc = _try_remote_or_local(
|
|
1209
|
+
args, "devtorch_reasoning_query",
|
|
1210
|
+
{"concept": args.concept, "agent_id": args.agent_id, "k": args.tokens},
|
|
1211
|
+
None,
|
|
1212
|
+
)
|
|
1213
|
+
if rc is not None:
|
|
1214
|
+
return rc
|
|
1215
|
+
|
|
1216
|
+
from devtorch_core.mcp.server import _tool_devtorch_reasoning_query
|
|
1217
|
+
gcc = GCCRepository.at(Path(args.path).resolve())
|
|
1218
|
+
result = _tool_devtorch_reasoning_query(gcc, {
|
|
1219
|
+
"concept": args.concept,
|
|
1220
|
+
"agent_id": args.agent_id,
|
|
1221
|
+
"k": args.tokens,
|
|
1222
|
+
})
|
|
1223
|
+
print(result)
|
|
1224
|
+
return 0
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
# Sprint 9: status command
|
|
1228
|
+
|
|
1229
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
1230
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1231
|
+
|
|
1232
|
+
print("─── DevTorch status ───")
|
|
1233
|
+
|
|
1234
|
+
# 1. Current branch
|
|
1235
|
+
branch = repo._current_branch()
|
|
1236
|
+
|
|
1237
|
+
# 2. Last commit
|
|
1238
|
+
history = repo.history(branch=None)
|
|
1239
|
+
if history:
|
|
1240
|
+
last = history[0]
|
|
1241
|
+
commit_id = (last.get("id") or "")[:7]
|
|
1242
|
+
commit_msg = last.get("message", "")
|
|
1243
|
+
commit_str = f'{commit_id} "{commit_msg}"'
|
|
1244
|
+
else:
|
|
1245
|
+
commit_str = "(no commits)"
|
|
1246
|
+
|
|
1247
|
+
print(f'Branch: {branch} | Last commit: {commit_str}')
|
|
1248
|
+
|
|
1249
|
+
# 3. Top 5 Θ hot zones by mean_confidence
|
|
1250
|
+
theta = repo.get_theta()
|
|
1251
|
+
cv = theta.get("coordination_vector", {})
|
|
1252
|
+
sorted_cv = sorted(cv.items(), key=lambda kv: kv[1].get("mean_confidence", 0), reverse=True)
|
|
1253
|
+
hot_zones = " ".join(
|
|
1254
|
+
f"{concept} ({entry['mean_confidence']:.2f})"
|
|
1255
|
+
for concept, entry in sorted_cv[:5]
|
|
1256
|
+
)
|
|
1257
|
+
print(f"Θ hot zones: {hot_zones or '(none)'}")
|
|
1258
|
+
|
|
1259
|
+
# 4. Node state
|
|
1260
|
+
node_state_path = repo.gcc_dir / "NODE_STATE"
|
|
1261
|
+
if node_state_path.exists():
|
|
1262
|
+
node_state = node_state_path.read_text(encoding="utf-8").strip()
|
|
1263
|
+
else:
|
|
1264
|
+
node_state = "UNKNOWN"
|
|
1265
|
+
print(f"Node state: {node_state}")
|
|
1266
|
+
|
|
1267
|
+
# 5. Invariant summary
|
|
1268
|
+
try:
|
|
1269
|
+
failures = repo.invariant_check(operation="status")
|
|
1270
|
+
if not failures:
|
|
1271
|
+
print("Invariants: \u2713 clear")
|
|
1272
|
+
else:
|
|
1273
|
+
print(f"Invariants: {len(failures)} failure(s):")
|
|
1274
|
+
for f in failures:
|
|
1275
|
+
print(f" [{f.invariant_id}] {f.message}")
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
print(f"Invariants: ERROR ({e})")
|
|
1278
|
+
|
|
1279
|
+
# 6. Consensus lock status
|
|
1280
|
+
locked = repo.is_locked()
|
|
1281
|
+
print(f"Lock: {'LOCKED' if locked else 'UNLOCKED'}")
|
|
1282
|
+
|
|
1283
|
+
print("\nTip: run `devtorch metrics --period 1d` for recent activity.")
|
|
1284
|
+
return 0
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
# Sprint 9: today command
|
|
1288
|
+
|
|
1289
|
+
def cmd_today(args: argparse.Namespace) -> int:
|
|
1290
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1291
|
+
if not repo.is_initialized():
|
|
1292
|
+
print("This directory is not tracking DevTorch yet.")
|
|
1293
|
+
print("Run `devtorch init` to start, or `devtorch welcome` for a quick guide.")
|
|
1294
|
+
return 0
|
|
1295
|
+
|
|
1296
|
+
print("═══ DevTorch daily dashboard ═══\n")
|
|
1297
|
+
rc = 0
|
|
1298
|
+
|
|
1299
|
+
status_args = types.SimpleNamespace(path=args.path)
|
|
1300
|
+
try:
|
|
1301
|
+
rc = max(rc, cmd_status(status_args))
|
|
1302
|
+
except Exception as e:
|
|
1303
|
+
print(f"Status: unavailable ({e})")
|
|
1304
|
+
rc = 1
|
|
1305
|
+
print()
|
|
1306
|
+
|
|
1307
|
+
doctor_args = types.SimpleNamespace(path=args.path, ci=False, skip_checks=[])
|
|
1308
|
+
try:
|
|
1309
|
+
rc = max(rc, cmd_doctor(doctor_args))
|
|
1310
|
+
except Exception as e:
|
|
1311
|
+
print(f"Doctor: unavailable ({e})")
|
|
1312
|
+
rc = 1
|
|
1313
|
+
print()
|
|
1314
|
+
|
|
1315
|
+
log_args = types.SimpleNamespace(
|
|
1316
|
+
path=args.path, tail=getattr(args, "tail", 20), branch=None
|
|
1317
|
+
)
|
|
1318
|
+
try:
|
|
1319
|
+
rc = max(rc, cmd_log(log_args))
|
|
1320
|
+
except Exception as e:
|
|
1321
|
+
print(f"Log: unavailable ({e})")
|
|
1322
|
+
rc = 1
|
|
1323
|
+
print()
|
|
1324
|
+
|
|
1325
|
+
metrics_args = types.SimpleNamespace(
|
|
1326
|
+
path=args.path,
|
|
1327
|
+
period="1d",
|
|
1328
|
+
as_json=False,
|
|
1329
|
+
tokens=False,
|
|
1330
|
+
speedup=False,
|
|
1331
|
+
all_projects=False,
|
|
1332
|
+
delivery_time=False,
|
|
1333
|
+
)
|
|
1334
|
+
try:
|
|
1335
|
+
rc = max(rc, cmd_metrics(metrics_args))
|
|
1336
|
+
except Exception as e:
|
|
1337
|
+
print(f"Metrics: unavailable ({e})")
|
|
1338
|
+
rc = 1
|
|
1339
|
+
|
|
1340
|
+
try:
|
|
1341
|
+
from devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
1342
|
+
from devtorch_core.reasoning_plus.learning.analytics import concept_health_section
|
|
1343
|
+
health = concept_health_section(LearningStore(repo.gcc_dir), threshold=0.4)
|
|
1344
|
+
if health:
|
|
1345
|
+
print()
|
|
1346
|
+
print(health)
|
|
1347
|
+
except Exception as e:
|
|
1348
|
+
print(f"Concept health: unavailable ({e})")
|
|
1349
|
+
rc = 1
|
|
1350
|
+
|
|
1351
|
+
try:
|
|
1352
|
+
from devtorch_core.reasoning_plus.learning.chain import build_chain, format_chain
|
|
1353
|
+
recent = build_chain(repo.gcc_dir, concept=None, max_chains=2)
|
|
1354
|
+
if recent:
|
|
1355
|
+
print()
|
|
1356
|
+
print("─── Recent reasoning chains ───")
|
|
1357
|
+
for i, chain in enumerate(recent, 1):
|
|
1358
|
+
print()
|
|
1359
|
+
print(format_chain(chain, indent=" "))
|
|
1360
|
+
except Exception as e:
|
|
1361
|
+
print(f"Reasoning chains: unavailable ({e})")
|
|
1362
|
+
rc = 1
|
|
1363
|
+
|
|
1364
|
+
try:
|
|
1365
|
+
from devtorch_core.cloud.sync_conflicts import ConflictLog
|
|
1366
|
+
summary = ConflictLog(repo.gcc_dir).summary()
|
|
1367
|
+
if summary:
|
|
1368
|
+
print()
|
|
1369
|
+
print(summary)
|
|
1370
|
+
except Exception as e:
|
|
1371
|
+
print(f"Sync conflicts: unavailable ({e})")
|
|
1372
|
+
rc = 1
|
|
1373
|
+
|
|
1374
|
+
print("\n═══ End of dashboard ═══")
|
|
1375
|
+
return rc
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
# Sprint 9: log command
|
|
1379
|
+
|
|
1380
|
+
def cmd_log(args: argparse.Namespace) -> int:
|
|
1381
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1382
|
+
log_path = repo.gcc_dir / "log.md"
|
|
1383
|
+
|
|
1384
|
+
if not log_path.exists():
|
|
1385
|
+
print("No log found. Run `devtorch init` first.")
|
|
1386
|
+
return 0
|
|
1387
|
+
|
|
1388
|
+
if getattr(args, "branch", None):
|
|
1389
|
+
print("(branch filter not yet supported — log.md is global)")
|
|
1390
|
+
|
|
1391
|
+
content = log_path.read_text(encoding="utf-8")
|
|
1392
|
+
lines = content.splitlines()
|
|
1393
|
+
|
|
1394
|
+
tail = getattr(args, "tail", None)
|
|
1395
|
+
if tail is not None:
|
|
1396
|
+
lines = lines[-tail:]
|
|
1397
|
+
|
|
1398
|
+
print("\n".join(lines))
|
|
1399
|
+
return 0
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
# S7: proxy subcommand (server in devtorch_core/proxy/)
|
|
1403
|
+
|
|
1404
|
+
def cmd_proxy_start(args: argparse.Namespace) -> int:
|
|
1405
|
+
port = getattr(args, "port", 8765)
|
|
1406
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
1407
|
+
try:
|
|
1408
|
+
from devtorch_core.proxy.server import run_proxy
|
|
1409
|
+
run_proxy(port=port, host=host)
|
|
1410
|
+
except ImportError:
|
|
1411
|
+
print("Install proxy deps: pip install devtorch[proxy]")
|
|
1412
|
+
return 1
|
|
1413
|
+
return 0
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def cmd_proxy_stop(args: argparse.Namespace) -> int:
|
|
1417
|
+
print("The proxy runs in the foreground. To stop it:")
|
|
1418
|
+
print(" - Press Ctrl+C in the terminal where it is running, or")
|
|
1419
|
+
print(" - Find the PID with `lsof -i :8765` and run `kill <PID>`.")
|
|
1420
|
+
return 0
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def cmd_proxy_install(args: argparse.Namespace) -> int:
|
|
1424
|
+
try:
|
|
1425
|
+
from devtorch_core.proxy.server import install_service
|
|
1426
|
+
install_service()
|
|
1427
|
+
except ImportError:
|
|
1428
|
+
print("Install proxy deps: pip install devtorch[proxy]")
|
|
1429
|
+
return 1
|
|
1430
|
+
return 0
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def cmd_proxy_status(args: argparse.Namespace) -> int:
|
|
1434
|
+
import socket
|
|
1435
|
+
port = 8765
|
|
1436
|
+
try:
|
|
1437
|
+
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
1438
|
+
print(f"Proxy running on :{port}")
|
|
1439
|
+
except (ConnectionRefusedError, OSError):
|
|
1440
|
+
print("Proxy not running")
|
|
1441
|
+
return 0
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
# S26: daemon subcommand
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def cmd_daemon_start(args: argparse.Namespace) -> int:
|
|
1448
|
+
from devtorch_core.daemon import start_daemon
|
|
1449
|
+
|
|
1450
|
+
project_root = Path(args.path).resolve()
|
|
1451
|
+
ok, message = start_daemon(
|
|
1452
|
+
project_root,
|
|
1453
|
+
run_server=getattr(args, "run_server", False),
|
|
1454
|
+
host=getattr(args, "host", "127.0.0.1"),
|
|
1455
|
+
port=int(getattr(args, "port", 8765)),
|
|
1456
|
+
)
|
|
1457
|
+
print(message)
|
|
1458
|
+
return 0 if ok else 1
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def cmd_daemon_stop(args: argparse.Namespace) -> int:
|
|
1462
|
+
from devtorch_core.daemon import stop_daemon
|
|
1463
|
+
|
|
1464
|
+
project_root = Path(args.path).resolve()
|
|
1465
|
+
ok, message = stop_daemon(project_root)
|
|
1466
|
+
print(message)
|
|
1467
|
+
return 0 if ok else 1
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def cmd_daemon_status(args: argparse.Namespace) -> int:
|
|
1471
|
+
from devtorch_core.daemon import daemon_status
|
|
1472
|
+
|
|
1473
|
+
project_root = Path(args.path).resolve()
|
|
1474
|
+
status = daemon_status(project_root)
|
|
1475
|
+
if status.get("error"):
|
|
1476
|
+
print(f"error: {status['error']}", file=sys.stderr)
|
|
1477
|
+
return 1
|
|
1478
|
+
if status["running"]:
|
|
1479
|
+
print(f"DevTorch daemon running (PID {status['pid']})")
|
|
1480
|
+
else:
|
|
1481
|
+
print("DevTorch daemon stopped")
|
|
1482
|
+
print(f"Log: {status['log_path']}")
|
|
1483
|
+
return 0
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def cmd_daemon_logs(args: argparse.Namespace) -> int:
|
|
1487
|
+
from devtorch_core.daemon import daemon_logs
|
|
1488
|
+
|
|
1489
|
+
project_root = Path(args.path).resolve()
|
|
1490
|
+
lines = daemon_logs(project_root, lines=getattr(args, "lines", 50))
|
|
1491
|
+
if not lines:
|
|
1492
|
+
print("(no daemon log entries)")
|
|
1493
|
+
return 0
|
|
1494
|
+
for line in lines:
|
|
1495
|
+
print(line)
|
|
1496
|
+
return 0
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
# Sprint 9: event log signing
|
|
1500
|
+
|
|
1501
|
+
def cmd_signing_enable(args: argparse.Namespace) -> int:
|
|
1502
|
+
from devtorch_core.signing import enable_signing
|
|
1503
|
+
gcc_dir = Path(args.path).resolve() / ".GCC"
|
|
1504
|
+
if not gcc_dir.exists():
|
|
1505
|
+
print(f"error: {gcc_dir} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
1506
|
+
return 1
|
|
1507
|
+
enable_signing(gcc_dir)
|
|
1508
|
+
print(f"Event log signing enabled for {gcc_dir}.")
|
|
1509
|
+
print(" Key: .GCC/signing.key")
|
|
1510
|
+
print(" Config: .GCC/signing.json")
|
|
1511
|
+
print(" New events will carry HMAC-SHA256 signatures.")
|
|
1512
|
+
return 0
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def cmd_signing_disable(args: argparse.Namespace) -> int:
|
|
1516
|
+
from devtorch_core.signing import disable_signing
|
|
1517
|
+
gcc_dir = Path(args.path).resolve() / ".GCC"
|
|
1518
|
+
if not gcc_dir.exists():
|
|
1519
|
+
print(f"error: {gcc_dir} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
1520
|
+
return 1
|
|
1521
|
+
disable_signing(gcc_dir)
|
|
1522
|
+
print(f"Event log signing disabled for {gcc_dir}. Existing signatures remain intact.")
|
|
1523
|
+
return 0
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def cmd_signing_verify(args: argparse.Namespace) -> int:
|
|
1527
|
+
from devtorch_core.signing import verify_log
|
|
1528
|
+
gcc_dir = Path(args.path).resolve() / ".GCC"
|
|
1529
|
+
if not gcc_dir.exists():
|
|
1530
|
+
print(f"error: {gcc_dir} not found. Run `devtorch init` first.", file=sys.stderr)
|
|
1531
|
+
return 1
|
|
1532
|
+
failures = verify_log(gcc_dir)
|
|
1533
|
+
if not failures:
|
|
1534
|
+
print("Event log signature verification: OK")
|
|
1535
|
+
return 0
|
|
1536
|
+
print(f"Event log signature verification: {len(failures)} failure(s)")
|
|
1537
|
+
for f in failures:
|
|
1538
|
+
print(f" line {f['line_no']}: {f['reason']}")
|
|
1539
|
+
return 1
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
# Sprint 19: project registry
|
|
1543
|
+
|
|
1544
|
+
def cmd_projects_add(args: argparse.Namespace) -> int:
|
|
1545
|
+
from devtorch_core.projects import ProjectRegistry
|
|
1546
|
+
try:
|
|
1547
|
+
registry = ProjectRegistry()
|
|
1548
|
+
entry = registry.add(Path(args.path), name=args.name, developer_id=args.developer_id)
|
|
1549
|
+
except ValueError as e:
|
|
1550
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1551
|
+
return 1
|
|
1552
|
+
print(f"Added project `{entry.name}` at {entry.path}")
|
|
1553
|
+
print(f" developer_id: {entry.developer_id}")
|
|
1554
|
+
|
|
1555
|
+
if getattr(args, "configure_mcp", False):
|
|
1556
|
+
from devtorch_core.hooks.installer import install_all_hooks, install_cursor_mcp
|
|
1557
|
+
hooks_result = install_all_hooks(str(entry.path))
|
|
1558
|
+
if hooks_result.claude_hooks_written:
|
|
1559
|
+
print(" MCP: .claude/hooks.json written")
|
|
1560
|
+
cursor_ok, cursor_msg = install_cursor_mcp(str(entry.path))
|
|
1561
|
+
if cursor_ok:
|
|
1562
|
+
print(" MCP: .cursor/mcp.json written")
|
|
1563
|
+
else:
|
|
1564
|
+
print(f" MCP: .cursor/mcp.json skipped ({cursor_msg})")
|
|
1565
|
+
return 0
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def cmd_projects_list(args: argparse.Namespace) -> int:
|
|
1569
|
+
from devtorch_core.projects import ProjectRegistry
|
|
1570
|
+
registry = ProjectRegistry()
|
|
1571
|
+
entries = registry.list()
|
|
1572
|
+
if not entries:
|
|
1573
|
+
print("No projects registered. Run `devtorch projects add <path>`.")
|
|
1574
|
+
return 0
|
|
1575
|
+
print(f"{'Name':<20} {'Path':<40} {'Developer':<25} {'Last Synced'}")
|
|
1576
|
+
print("-" * 100)
|
|
1577
|
+
for e in entries:
|
|
1578
|
+
print(f"{e.name:<20} {e.path:<40} {e.developer_id:<25} {e.last_synced[:19]}")
|
|
1579
|
+
return 0
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def cmd_projects_remove(args: argparse.Namespace) -> int:
|
|
1583
|
+
from devtorch_core.projects import ProjectRegistry
|
|
1584
|
+
registry = ProjectRegistry()
|
|
1585
|
+
removed = registry.remove(Path(args.path))
|
|
1586
|
+
if removed:
|
|
1587
|
+
print(f"Removed project at {args.path}")
|
|
1588
|
+
return 0
|
|
1589
|
+
print(f"No project registered at {args.path}", file=sys.stderr)
|
|
1590
|
+
return 1
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def cmd_projects_sync(args: argparse.Namespace) -> int:
|
|
1594
|
+
from devtorch_core.projects import ProjectRegistry
|
|
1595
|
+
registry = ProjectRegistry()
|
|
1596
|
+
entries = registry.sync()
|
|
1597
|
+
print(f"Synced {len(entries)} project(s).")
|
|
1598
|
+
return 0
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
# Sprint 6: debug views
|
|
1602
|
+
|
|
1603
|
+
def cmd_debug_timeline(args: argparse.Namespace) -> int:
|
|
1604
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1605
|
+
filter_types = None
|
|
1606
|
+
if getattr(args, "filter", None):
|
|
1607
|
+
filter_types = [t.strip() for t in args.filter.split(",") if t.strip()]
|
|
1608
|
+
events = repo.debug_timeline(filter_types=filter_types)
|
|
1609
|
+
for e in events:
|
|
1610
|
+
ts = e.get("timestamp", "")
|
|
1611
|
+
et = e.get("event_type", "")
|
|
1612
|
+
payload = e.get("payload", {})
|
|
1613
|
+
print(f"{ts} {et} {payload}")
|
|
1614
|
+
return 0
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def cmd_debug_branch_compare(args: argparse.Namespace) -> int:
|
|
1618
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1619
|
+
b1 = args.branch1
|
|
1620
|
+
b2 = args.branch2
|
|
1621
|
+
try:
|
|
1622
|
+
data = repo.debug_branch_compare(b1, b2)
|
|
1623
|
+
except RuntimeError as e:
|
|
1624
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1625
|
+
return 1
|
|
1626
|
+
print(f"Branch: {b1}")
|
|
1627
|
+
for c in data["branch1"]:
|
|
1628
|
+
print(f" {c.get('id')} {c.get('timestamp')} {c.get('message')}")
|
|
1629
|
+
print(f"\nBranch: {b2}")
|
|
1630
|
+
for c in data["branch2"]:
|
|
1631
|
+
print(f" {c.get('id')} {c.get('timestamp')} {c.get('message')}")
|
|
1632
|
+
print("\nΘ (coordination vector):")
|
|
1633
|
+
for k, v in (data.get("theta") or {}).items():
|
|
1634
|
+
print(f" {k}: {v}")
|
|
1635
|
+
return 0
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def cmd_debug_invariant_trace(args: argparse.Namespace) -> int:
|
|
1639
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
1640
|
+
try:
|
|
1641
|
+
traces = repo.debug_invariant_trace()
|
|
1642
|
+
except RuntimeError as e:
|
|
1643
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1644
|
+
return 1
|
|
1645
|
+
if not traces:
|
|
1646
|
+
print("No invariant failures (trace clear).")
|
|
1647
|
+
return 0
|
|
1648
|
+
print("Invariant trace (failures):")
|
|
1649
|
+
for t in traces:
|
|
1650
|
+
print(f" [{t['invariant_id']}] {t['message']}")
|
|
1651
|
+
print(f" Fix: {t['actionable_fix']}")
|
|
1652
|
+
return 1
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
_CLAUDE_MD_BLOCK = """
|
|
1656
|
+
## DevTorch — Reasoning Capture
|
|
1657
|
+
|
|
1658
|
+
This project uses DevTorch for AI governance. Follow these rules on every task:
|
|
1659
|
+
|
|
1660
|
+
1. **Before writing code**: call `devtorch_commit` (MCP tool) with your reasoning.
|
|
1661
|
+
Include what you decided and why, not just what you did.
|
|
1662
|
+
|
|
1663
|
+
2. **When touching sensitive areas** (auth, payments, database schema, secrets,
|
|
1664
|
+
PII, external APIs): call `devtorch_sensitivity_add` with the concept name,
|
|
1665
|
+
your confidence (0–1), and disclosure level (PUBLIC / PROTECTED / PRIVATE).
|
|
1666
|
+
|
|
1667
|
+
3. **At the start of a new task**: call `devtorch_context` to retrieve prior
|
|
1668
|
+
decisions before making new ones.
|
|
1669
|
+
"""
|
|
1670
|
+
|
|
1671
|
+
def cmd_setup(args: argparse.Namespace) -> int:
|
|
1672
|
+
"""
|
|
1673
|
+
One-command setup: init + hooks + MCP server config + optional CLAUDE.md.
|
|
1674
|
+
Idempotent — safe to run on an already-configured project.
|
|
1675
|
+
"""
|
|
1676
|
+
import shutil
|
|
1677
|
+
|
|
1678
|
+
project_root = Path(args.path).resolve()
|
|
1679
|
+
ok_mark = "✓"
|
|
1680
|
+
skip_mark = "–"
|
|
1681
|
+
fail_mark = "✗"
|
|
1682
|
+
|
|
1683
|
+
print("DevTorch setup\n")
|
|
1684
|
+
|
|
1685
|
+
# ------------------------------------------------------------------ #
|
|
1686
|
+
# Detect IDE(s) and project type
|
|
1687
|
+
# ------------------------------------------------------------------ #
|
|
1688
|
+
from devtorch_core.hooks.installer import (
|
|
1689
|
+
_detect_ides,
|
|
1690
|
+
_detect_project_type,
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
detected_ides = _detect_ides(str(project_root))
|
|
1694
|
+
project_type = _detect_project_type(str(project_root))
|
|
1695
|
+
print(f"Detected project type: {project_type}")
|
|
1696
|
+
print(f"Detected IDE(s): {', '.join(detected_ides) if detected_ides else 'none'}")
|
|
1697
|
+
print()
|
|
1698
|
+
|
|
1699
|
+
# ------------------------------------------------------------------ #
|
|
1700
|
+
# Step 1: devtorch init
|
|
1701
|
+
# ------------------------------------------------------------------ #
|
|
1702
|
+
repo = GCCRepository.at(project_root)
|
|
1703
|
+
if repo.is_initialized():
|
|
1704
|
+
print(f" {skip_mark} .GCC/ already initialized — skipping")
|
|
1705
|
+
else:
|
|
1706
|
+
try:
|
|
1707
|
+
repo.init()
|
|
1708
|
+
print(f" {ok_mark} .GCC/ initialized at {repo.gcc_dir}")
|
|
1709
|
+
except Exception as exc:
|
|
1710
|
+
print(f" {fail_mark} .GCC/ init failed: {exc}", file=sys.stderr)
|
|
1711
|
+
return 1
|
|
1712
|
+
|
|
1713
|
+
# ------------------------------------------------------------------ #
|
|
1714
|
+
# Step 2: Claude Code hooks (hooks key in .claude/settings.json)
|
|
1715
|
+
# ------------------------------------------------------------------ #
|
|
1716
|
+
from devtorch_core.hooks.installer import install_all_hooks, get_hooks_status
|
|
1717
|
+
|
|
1718
|
+
hooks_status = get_hooks_status(str(project_root))
|
|
1719
|
+
if hooks_status["claude_hooks"]:
|
|
1720
|
+
print(f" {skip_mark} .claude/settings.json hooks already installed — skipping")
|
|
1721
|
+
else:
|
|
1722
|
+
result = install_all_hooks(str(project_root))
|
|
1723
|
+
if result.claude_hooks_written:
|
|
1724
|
+
print(f" {ok_mark} .claude/settings.json hooks installed")
|
|
1725
|
+
else:
|
|
1726
|
+
err = result.errors[0] if result.errors else "unknown error"
|
|
1727
|
+
print(f" {fail_mark} .claude/settings.json hooks failed: {err}")
|
|
1728
|
+
|
|
1729
|
+
if hooks_status["git_hook"]:
|
|
1730
|
+
print(f" {skip_mark} .git/hooks/commit-msg already installed — skipping")
|
|
1731
|
+
elif not hooks_status["claude_hooks"]:
|
|
1732
|
+
# result was assigned in the else-branch above
|
|
1733
|
+
if result.git_hook_written:
|
|
1734
|
+
print(f" {ok_mark} .git/hooks/commit-msg written")
|
|
1735
|
+
else:
|
|
1736
|
+
print(f" {skip_mark} .git/hooks/commit-msg no .git repo found (skipped)")
|
|
1737
|
+
else:
|
|
1738
|
+
print(f" {skip_mark} .git/hooks/commit-msg no .git repo found (skipped)")
|
|
1739
|
+
|
|
1740
|
+
# ------------------------------------------------------------------ #
|
|
1741
|
+
# Step 3: Claude Code MCP server (<project-root>/.mcp.json)
|
|
1742
|
+
# ------------------------------------------------------------------ #
|
|
1743
|
+
# Claude Code only reads project-scope MCP servers from .mcp.json at the
|
|
1744
|
+
# project root; a mcpServers key in .claude/settings.json is ignored.
|
|
1745
|
+
from devtorch_core.hooks.installer import install_claude_mcp
|
|
1746
|
+
|
|
1747
|
+
mcp_path = project_root / ".mcp.json"
|
|
1748
|
+
try:
|
|
1749
|
+
pre_existing = json.loads(mcp_path.read_text(encoding="utf-8")).get("mcpServers", {}) \
|
|
1750
|
+
if mcp_path.exists() else {}
|
|
1751
|
+
except Exception:
|
|
1752
|
+
pre_existing = {}
|
|
1753
|
+
|
|
1754
|
+
ok, path_or_err = install_claude_mcp(str(project_root))
|
|
1755
|
+
if not ok:
|
|
1756
|
+
print(f" {fail_mark} .mcp.json failed: {path_or_err}", file=sys.stderr)
|
|
1757
|
+
elif "devtorch" in pre_existing:
|
|
1758
|
+
print(f" {skip_mark} .mcp.json MCP entry already present — skipping")
|
|
1759
|
+
else:
|
|
1760
|
+
print(f" {ok_mark} .mcp.json MCP server entry added")
|
|
1761
|
+
|
|
1762
|
+
# ------------------------------------------------------------------ #
|
|
1763
|
+
# Step 4: CLAUDE.md DevTorch instructions
|
|
1764
|
+
# ------------------------------------------------------------------ #
|
|
1765
|
+
if not getattr(args, "skip_claude_md", False):
|
|
1766
|
+
claude_md_path = project_root / "CLAUDE.md"
|
|
1767
|
+
marker = "## DevTorch"
|
|
1768
|
+
if claude_md_path.exists() and marker in claude_md_path.read_text(encoding="utf-8"):
|
|
1769
|
+
print(f" {skip_mark} CLAUDE.md DevTorch section already present — skipping")
|
|
1770
|
+
else:
|
|
1771
|
+
try:
|
|
1772
|
+
with claude_md_path.open("a", encoding="utf-8") as f:
|
|
1773
|
+
f.write(_CLAUDE_MD_BLOCK)
|
|
1774
|
+
action = "appended to" if claude_md_path.stat().st_size > len(_CLAUDE_MD_BLOCK) else "created"
|
|
1775
|
+
print(f" {ok_mark} CLAUDE.md DevTorch instructions {action}")
|
|
1776
|
+
except Exception as exc:
|
|
1777
|
+
print(f" {fail_mark} CLAUDE.md failed: {exc}", file=sys.stderr)
|
|
1778
|
+
|
|
1779
|
+
# ------------------------------------------------------------------ #
|
|
1780
|
+
# Step 5: AGENTS.md DevTorch instructions (Google Antigravity)
|
|
1781
|
+
# ------------------------------------------------------------------ #
|
|
1782
|
+
if not getattr(args, "skip_agents_md", False):
|
|
1783
|
+
from devtorch_core.hooks.installer import install_agents_md
|
|
1784
|
+
agents_md_path = project_root / "AGENTS.md"
|
|
1785
|
+
marker = "## DevTorch"
|
|
1786
|
+
if agents_md_path.exists() and marker in agents_md_path.read_text(encoding="utf-8"):
|
|
1787
|
+
print(f" {skip_mark} AGENTS.md DevTorch section already present — skipping")
|
|
1788
|
+
else:
|
|
1789
|
+
ok, path_or_err = install_agents_md(str(project_root))
|
|
1790
|
+
if ok:
|
|
1791
|
+
action = "appended to" if agents_md_path.exists() and agents_md_path.stat().st_size > len(
|
|
1792
|
+
"\n## DevTorch"
|
|
1793
|
+
) else "created"
|
|
1794
|
+
print(f" {ok_mark} AGENTS.md DevTorch instructions {action}")
|
|
1795
|
+
else:
|
|
1796
|
+
print(f" {fail_mark} AGENTS.md failed: {path_or_err}", file=sys.stderr)
|
|
1797
|
+
|
|
1798
|
+
# ------------------------------------------------------------------ #
|
|
1799
|
+
# Step 6/7: IDE-specific MCP config
|
|
1800
|
+
# ------------------------------------------------------------------ #
|
|
1801
|
+
target = getattr(args, "target", None)
|
|
1802
|
+
if target is None or target == "auto":
|
|
1803
|
+
targets = detected_ides if detected_ides else ["claude-code"]
|
|
1804
|
+
else:
|
|
1805
|
+
targets = [target]
|
|
1806
|
+
|
|
1807
|
+
from devtorch_core.hooks.installer import (
|
|
1808
|
+
install_cursor_mcp,
|
|
1809
|
+
install_antigravity_mcp,
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
for target in targets:
|
|
1813
|
+
if target == "cursor":
|
|
1814
|
+
cursor_mcp = project_root / ".cursor" / "mcp.json"
|
|
1815
|
+
already = (
|
|
1816
|
+
cursor_mcp.exists()
|
|
1817
|
+
and "devtorch" in json.loads(cursor_mcp.read_text()).get("mcpServers", {})
|
|
1818
|
+
)
|
|
1819
|
+
if already:
|
|
1820
|
+
print(f" {skip_mark} .cursor/mcp.json MCP entry already present — skipping")
|
|
1821
|
+
else:
|
|
1822
|
+
ok, path_or_err = install_cursor_mcp(str(project_root))
|
|
1823
|
+
if ok:
|
|
1824
|
+
print(f" {ok_mark} .cursor/mcp.json MCP server entry added")
|
|
1825
|
+
else:
|
|
1826
|
+
print(f" {fail_mark} .cursor/mcp.json failed: {path_or_err}", file=sys.stderr)
|
|
1827
|
+
print()
|
|
1828
|
+
print(" Next: install the VS Code extension in Cursor:")
|
|
1829
|
+
print(" Extensions: Install from VSIX... →")
|
|
1830
|
+
print(" extensions/vscode/devtorch-0.1.0.vsix")
|
|
1831
|
+
|
|
1832
|
+
elif target == "antigravity":
|
|
1833
|
+
ag_mcp = project_root / "mcp_config.json"
|
|
1834
|
+
already = (
|
|
1835
|
+
ag_mcp.exists()
|
|
1836
|
+
and "devtorch" in json.loads(ag_mcp.read_text()).get("mcpServers", {})
|
|
1837
|
+
)
|
|
1838
|
+
if already:
|
|
1839
|
+
print(f" {skip_mark} mcp_config.json MCP entry already present — skipping")
|
|
1840
|
+
else:
|
|
1841
|
+
ok, path_or_err = install_antigravity_mcp(str(project_root))
|
|
1842
|
+
if ok:
|
|
1843
|
+
print(f" {ok_mark} mcp_config.json MCP server entry added")
|
|
1844
|
+
else:
|
|
1845
|
+
print(f" {fail_mark} mcp_config.json failed: {path_or_err}", file=sys.stderr)
|
|
1846
|
+
|
|
1847
|
+
# ------------------------------------------------------------------ #
|
|
1848
|
+
# Step 5.5: OpenCode hooks (PreUserPrompt for context bundle)
|
|
1849
|
+
# ------------------------------------------------------------------ #
|
|
1850
|
+
if "opencode" in detected_ides:
|
|
1851
|
+
from devtorch_core.hooks.installer import install_opencode_hooks
|
|
1852
|
+
|
|
1853
|
+
ok, path_or_err = install_opencode_hooks(str(project_root))
|
|
1854
|
+
if ok:
|
|
1855
|
+
print(f" {ok_mark} opencode.json hooks installed")
|
|
1856
|
+
else:
|
|
1857
|
+
print(f" {fail_mark} opencode.json hooks failed: {path_or_err}", file=sys.stderr)
|
|
1858
|
+
|
|
1859
|
+
# ------------------------------------------------------------------ #
|
|
1860
|
+
# Done
|
|
1861
|
+
# ------------------------------------------------------------------ #
|
|
1862
|
+
print()
|
|
1863
|
+
print("Setup complete. Start Claude Code and run: devtorch status")
|
|
1864
|
+
if not shutil.which("devtorch"):
|
|
1865
|
+
print()
|
|
1866
|
+
print(" NOTE: 'devtorch' not found in PATH.")
|
|
1867
|
+
print(" Activate your venv first: source .venv/bin/activate")
|
|
1868
|
+
return 0
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def cmd_calibrate(args: argparse.Namespace) -> int:
|
|
1872
|
+
from devtorch_core.metrics.calibrate import CalibrationStore, run_calibrate_wizard
|
|
1873
|
+
|
|
1874
|
+
gcc_dir = Path(args.path).resolve() / ".GCC"
|
|
1875
|
+
|
|
1876
|
+
if getattr(args, "set_value", None):
|
|
1877
|
+
# Parse KEY=VALUE
|
|
1878
|
+
raw = args.set_value.strip()
|
|
1879
|
+
if "=" not in raw:
|
|
1880
|
+
print(f"error: --set expects KEY=VALUE, got: {raw!r}", file=sys.stderr)
|
|
1881
|
+
return 1
|
|
1882
|
+
key, _, value_str = raw.partition("=")
|
|
1883
|
+
key = key.strip()
|
|
1884
|
+
value_str = value_str.strip()
|
|
1885
|
+
# Try numeric coercion
|
|
1886
|
+
try:
|
|
1887
|
+
value: object = float(value_str)
|
|
1888
|
+
except ValueError:
|
|
1889
|
+
value = value_str
|
|
1890
|
+
try:
|
|
1891
|
+
store = CalibrationStore(gcc_dir)
|
|
1892
|
+
store.set_value(key, value)
|
|
1893
|
+
print(f"Calibration set: {key} = {value}")
|
|
1894
|
+
except Exception as exc:
|
|
1895
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1896
|
+
return 1
|
|
1897
|
+
return 0
|
|
1898
|
+
|
|
1899
|
+
# Interactive wizard
|
|
1900
|
+
try:
|
|
1901
|
+
result = run_calibrate_wizard(gcc_dir)
|
|
1902
|
+
print("Calibration saved:")
|
|
1903
|
+
for k, v in sorted(result.items()):
|
|
1904
|
+
print(f" {k}: {v}")
|
|
1905
|
+
except Exception as exc:
|
|
1906
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1907
|
+
return 1
|
|
1908
|
+
return 0
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def cmd_metrics(args: argparse.Namespace) -> int:
|
|
1912
|
+
import glob as _glob
|
|
1913
|
+
|
|
1914
|
+
from devtorch_core.metrics.calibrate import CalibrationStore
|
|
1915
|
+
from devtorch_core.metrics.delivery_time import compute_delivery_time_summary
|
|
1916
|
+
from devtorch_core.metrics.shadow_ai import detect_shadow_ai
|
|
1917
|
+
from devtorch_core.metrics.dhs import compute_dhs, DHSComponents, dhs_to_label
|
|
1918
|
+
|
|
1919
|
+
repo_root = Path(args.path).resolve()
|
|
1920
|
+
gcc_dir = repo_root / ".GCC"
|
|
1921
|
+
|
|
1922
|
+
# Parse period
|
|
1923
|
+
period_str = getattr(args, "period", "30d")
|
|
1924
|
+
try:
|
|
1925
|
+
period_days = int(period_str.rstrip("d"))
|
|
1926
|
+
except (ValueError, AttributeError):
|
|
1927
|
+
period_days = 30
|
|
1928
|
+
|
|
1929
|
+
# Load calibration
|
|
1930
|
+
try:
|
|
1931
|
+
store = CalibrationStore(gcc_dir)
|
|
1932
|
+
calibration = store.load()
|
|
1933
|
+
except Exception:
|
|
1934
|
+
calibration = {}
|
|
1935
|
+
|
|
1936
|
+
# Sprint 20: token/speedup breakdowns
|
|
1937
|
+
from devtorch_core.metrics.aggregate import (
|
|
1938
|
+
aggregate_tokens,
|
|
1939
|
+
aggregate_speedup,
|
|
1940
|
+
aggregate_by_developer,
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
if getattr(args, "tokens", False):
|
|
1944
|
+
if getattr(args, "all_projects", False):
|
|
1945
|
+
from devtorch_core.metrics.aggregate import aggregate_all_projects
|
|
1946
|
+
all_projects = aggregate_all_projects()
|
|
1947
|
+
if getattr(args, "as_json", False):
|
|
1948
|
+
print(json.dumps(all_projects, indent=2))
|
|
1949
|
+
return 0
|
|
1950
|
+
print("DevTorch Token Metrics — All Projects\n")
|
|
1951
|
+
print(f" {'Project':<20} {'Sessions':>10} {'Used':>12} {'Saved':>12}")
|
|
1952
|
+
print(" " + "-" * 60)
|
|
1953
|
+
for p in all_projects["projects"]:
|
|
1954
|
+
print(f" {p['name']:<20} {p['sessions_count']:>10} {p['tokens_used']:>12,} {p['tokens_saved']:>12,}")
|
|
1955
|
+
agg = all_projects["aggregate"]
|
|
1956
|
+
print(" " + "-" * 60)
|
|
1957
|
+
print(f" {'AGGREGATE':<20} {agg['sessions_count']:>10} {agg['tokens_used']:>12,} {agg['tokens_saved']:>12,}")
|
|
1958
|
+
return 0
|
|
1959
|
+
|
|
1960
|
+
agg = aggregate_tokens(gcc_dir)
|
|
1961
|
+
by_dev = aggregate_by_developer(gcc_dir)
|
|
1962
|
+
token_cost = calibration.get("token_cost_usd_per_million")
|
|
1963
|
+
if token_cost:
|
|
1964
|
+
cost_saved_usd = (agg['total_tokens_saved'] / 1_000_000) * float(token_cost)
|
|
1965
|
+
else:
|
|
1966
|
+
cost_saved_usd = None
|
|
1967
|
+
if getattr(args, "as_json", False):
|
|
1968
|
+
output = {
|
|
1969
|
+
"project": agg,
|
|
1970
|
+
"per_developer": by_dev,
|
|
1971
|
+
"cost_saved_usd": cost_saved_usd,
|
|
1972
|
+
"cost_calibrated": bool(token_cost),
|
|
1973
|
+
}
|
|
1974
|
+
print(json.dumps(output, indent=2))
|
|
1975
|
+
return 0
|
|
1976
|
+
print("DevTorch Token Metrics\n")
|
|
1977
|
+
print(f" Sessions: {agg['sessions_count']}")
|
|
1978
|
+
print(f" Tokens used: {agg['total_tokens_used']:,} [MEASURED]")
|
|
1979
|
+
print(f" Tokens saved: {agg['total_tokens_saved']:,} [CALIBRATED]")
|
|
1980
|
+
print(f" Bundle tokens: {agg['total_bundle_tokens']:,} [MEASURED]")
|
|
1981
|
+
print(f" Full history tokens: {agg['total_full_history_tokens']:,} [MEASURED]")
|
|
1982
|
+
if cost_saved_usd is not None:
|
|
1983
|
+
print(f" Cost saved (USD): ${cost_saved_usd:,.2f} [CALIBRATED]")
|
|
1984
|
+
else:
|
|
1985
|
+
print(" Cost saved (USD): Set token_cost_usd_per_million to unlock [UNAVAILABLE]")
|
|
1986
|
+
if by_dev:
|
|
1987
|
+
print("\n Per developer:")
|
|
1988
|
+
for dev_id, dev in sorted(by_dev.items()):
|
|
1989
|
+
print(f" {dev_id}: used={dev['tokens_used']:,} saved={dev['tokens_saved']:,} sessions={dev['sessions_count']}")
|
|
1990
|
+
return 0
|
|
1991
|
+
|
|
1992
|
+
if getattr(args, "speedup", False):
|
|
1993
|
+
if getattr(args, "all_projects", False):
|
|
1994
|
+
from devtorch_core.metrics.aggregate import aggregate_all_projects
|
|
1995
|
+
all_projects = aggregate_all_projects()
|
|
1996
|
+
if getattr(args, "as_json", False):
|
|
1997
|
+
print(json.dumps(all_projects, indent=2))
|
|
1998
|
+
return 0
|
|
1999
|
+
print("DevTorch Speedup Metrics — All Projects\n")
|
|
2000
|
+
print(f" {'Project':<20} {'Sessions':>10} {'Speedup':>10}")
|
|
2001
|
+
print(" " + "-" * 45)
|
|
2002
|
+
for p in all_projects["projects"]:
|
|
2003
|
+
print(f" {p['name']:<20} {p['sessions_count']:>10} {p['latency_speedup_factor']:>9.2f}x")
|
|
2004
|
+
agg = all_projects["aggregate"]
|
|
2005
|
+
print(" " + "-" * 45)
|
|
2006
|
+
print(f" {'AGGREGATE':<20} {agg['sessions_count']:>10} {agg['latency_speedup_factor']:>9.2f}x")
|
|
2007
|
+
return 0
|
|
2008
|
+
|
|
2009
|
+
agg = aggregate_speedup(gcc_dir)
|
|
2010
|
+
by_dev = aggregate_by_developer(gcc_dir)
|
|
2011
|
+
if getattr(args, "as_json", False):
|
|
2012
|
+
print(json.dumps({"project": agg, "per_developer": by_dev}, indent=2))
|
|
2013
|
+
return 0
|
|
2014
|
+
print("DevTorch Speedup Metrics\n")
|
|
2015
|
+
print(f" Sessions: {agg['sessions_count']}")
|
|
2016
|
+
print(f" Latency speedup factor: {agg['latency_speedup_factor']:.2f}x [MEASURED]")
|
|
2017
|
+
print(f" Full history tokens: {agg['total_full_history_tokens']:,}")
|
|
2018
|
+
print(f" Bundle tokens: {agg['total_bundle_tokens']:,}")
|
|
2019
|
+
if by_dev:
|
|
2020
|
+
print("\n Per developer:")
|
|
2021
|
+
for dev_id, dev in sorted(by_dev.items()):
|
|
2022
|
+
print(f" {dev_id}: speedup={dev['latency_speedup_factor']:.2f}x sessions={dev['sessions_count']}")
|
|
2023
|
+
return 0
|
|
2024
|
+
|
|
2025
|
+
# Read session metrics
|
|
2026
|
+
session_dir = gcc_dir / "metrics" / "session"
|
|
2027
|
+
session_files: list[dict] = []
|
|
2028
|
+
if session_dir.exists():
|
|
2029
|
+
for p in session_dir.glob("*.json"):
|
|
2030
|
+
try:
|
|
2031
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
2032
|
+
if isinstance(data, dict):
|
|
2033
|
+
session_files.append(data)
|
|
2034
|
+
except Exception:
|
|
2035
|
+
pass
|
|
2036
|
+
|
|
2037
|
+
# Read sprint metrics
|
|
2038
|
+
sprint_dir = gcc_dir / "metrics" / "sprint"
|
|
2039
|
+
sprint_files: list[dict] = []
|
|
2040
|
+
if sprint_dir.exists():
|
|
2041
|
+
for p in sprint_dir.glob("*.json"):
|
|
2042
|
+
try:
|
|
2043
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
2044
|
+
if isinstance(data, dict):
|
|
2045
|
+
sprint_files.append(data)
|
|
2046
|
+
except Exception:
|
|
2047
|
+
pass
|
|
2048
|
+
|
|
2049
|
+
# Shadow AI detection
|
|
2050
|
+
try:
|
|
2051
|
+
shadow = detect_shadow_ai(repo_root)
|
|
2052
|
+
except Exception:
|
|
2053
|
+
shadow = {"git_commits": 0, "gcc_commits": 0, "unregistered": 0, "shadow_ratio": 0.0, "flagged": False}
|
|
2054
|
+
|
|
2055
|
+
# DHS computation (best-effort from available data)
|
|
2056
|
+
try:
|
|
2057
|
+
components = DHSComponents(
|
|
2058
|
+
commit_health=1.0 if (shadow.get("gcc_commits", 0) > 0) else 0.0,
|
|
2059
|
+
sensitivity_health=1.0,
|
|
2060
|
+
capability_health=0.0,
|
|
2061
|
+
variance_health=1.0,
|
|
2062
|
+
)
|
|
2063
|
+
dhs_score = compute_dhs(components)
|
|
2064
|
+
dhs_label = dhs_to_label(dhs_score)
|
|
2065
|
+
except Exception:
|
|
2066
|
+
dhs_score = 0.0
|
|
2067
|
+
dhs_label = "UNAVAILABLE"
|
|
2068
|
+
|
|
2069
|
+
# Sprint 25: complex-problem delivery-time baseline
|
|
2070
|
+
metrics_dir = gcc_dir / "metrics"
|
|
2071
|
+
show_delivery_time = (
|
|
2072
|
+
getattr(args, "delivery_time", False)
|
|
2073
|
+
or "complex_problem_delivery_time_baseline_min" in calibration
|
|
2074
|
+
)
|
|
2075
|
+
delivery_summary = None
|
|
2076
|
+
if show_delivery_time:
|
|
2077
|
+
delivery_summary = compute_delivery_time_summary(metrics_dir, calibration)
|
|
2078
|
+
|
|
2079
|
+
# Aggregate session data
|
|
2080
|
+
total_tokens_used = sum(s.get("tokens_used", 0) for s in session_files)
|
|
2081
|
+
total_tokens_saved = sum(s.get("tokens_saved", 0) for s in session_files)
|
|
2082
|
+
avg_coverage = (
|
|
2083
|
+
sum(s.get("coverage", 0.0) for s in session_files) / len(session_files)
|
|
2084
|
+
if session_files else 0.0
|
|
2085
|
+
)
|
|
2086
|
+
# Aggregate confidence distribution
|
|
2087
|
+
conf_dist: dict[str, int] = {}
|
|
2088
|
+
for s in session_files:
|
|
2089
|
+
for tier, cnt in (s.get("confidence_distribution") or {}).items():
|
|
2090
|
+
conf_dist[tier] = conf_dist.get(tier, 0) + int(cnt)
|
|
2091
|
+
|
|
2092
|
+
# Aggregate sprint data
|
|
2093
|
+
avg_mcs = (
|
|
2094
|
+
sum(sp.get("mcs_score", 0.0) for sp in sprint_files) / len(sprint_files)
|
|
2095
|
+
if sprint_files else 0.0
|
|
2096
|
+
)
|
|
2097
|
+
total_i3_violations = sum(sp.get("i3_violations", 0) for sp in sprint_files)
|
|
2098
|
+
total_collisions_prevented = sum(sp.get("collisions_prevented", 0) for sp in sprint_files)
|
|
2099
|
+
|
|
2100
|
+
# Auditability: fraction of git commits that are GCC-registered
|
|
2101
|
+
git_commits = shadow.get("git_commits", 0)
|
|
2102
|
+
gcc_commits = shadow.get("gcc_commits", 0)
|
|
2103
|
+
auditability = gcc_commits / git_commits if git_commits > 0 else 1.0
|
|
2104
|
+
|
|
2105
|
+
disclosure = (
|
|
2106
|
+
"\nNOTE: MCS/DHS weights are DevTorch design choices pending empirical calibration."
|
|
2107
|
+
)
|
|
2108
|
+
|
|
2109
|
+
if getattr(args, "as_json", False):
|
|
2110
|
+
output = {
|
|
2111
|
+
"period_days": period_days,
|
|
2112
|
+
"calibration": calibration,
|
|
2113
|
+
"developer": {
|
|
2114
|
+
"tokens_used": {"value": total_tokens_used, "tier": "MEASURED"},
|
|
2115
|
+
"tokens_saved": {"value": total_tokens_saved, "tier": "CALIBRATED"},
|
|
2116
|
+
"coverage_pct": {"value": round(avg_coverage * 100, 1), "tier": "MEASURED"},
|
|
2117
|
+
"confidence_distribution": {"value": conf_dist, "tier": "MEASURED"},
|
|
2118
|
+
},
|
|
2119
|
+
"tech_lead": {
|
|
2120
|
+
"mcs_trend": {"value": round(avg_mcs, 4), "tier": "CALIBRATED"},
|
|
2121
|
+
"i3_violations": {"value": total_i3_violations, "tier": "MEASURED"},
|
|
2122
|
+
"collisions_prevented": {"value": total_collisions_prevented, "tier": "CALIBRATED"},
|
|
2123
|
+
},
|
|
2124
|
+
"ciso": {
|
|
2125
|
+
"dhs_score": {"value": round(dhs_score, 4), "label": dhs_label, "tier": "CALIBRATED"},
|
|
2126
|
+
"shadow_ai_ratio": {"value": round(shadow.get("shadow_ratio", 0.0), 4), "flagged": shadow.get("flagged", False), "tier": "MEASURED"},
|
|
2127
|
+
"auditability_pct": {"value": round(auditability * 100, 1), "tier": "MEASURED"},
|
|
2128
|
+
},
|
|
2129
|
+
"shadow_ai": shadow,
|
|
2130
|
+
"disclosure": disclosure.strip(),
|
|
2131
|
+
}
|
|
2132
|
+
if delivery_summary:
|
|
2133
|
+
output["tech_lead"]["complex_problem_delivery_time"] = {
|
|
2134
|
+
"current_min": delivery_summary["complex_problem_delivery_time_current_min"],
|
|
2135
|
+
"baseline_min": delivery_summary["complex_problem_delivery_time_baseline_min"],
|
|
2136
|
+
"delta_vs_baseline_min": delivery_summary["delta_vs_baseline_min"],
|
|
2137
|
+
"tier": delivery_summary["tier"],
|
|
2138
|
+
}
|
|
2139
|
+
print(json.dumps(output, indent=2))
|
|
2140
|
+
return 0
|
|
2141
|
+
|
|
2142
|
+
# Formatted output
|
|
2143
|
+
print(f"DevTorch Metrics Summary (period: {period_days}d)\n")
|
|
2144
|
+
print("=== Developer ===")
|
|
2145
|
+
print(f" Tokens used: {total_tokens_used:,} [MEASURED]")
|
|
2146
|
+
print(f" Tokens saved: {total_tokens_saved:,} [CALIBRATED]")
|
|
2147
|
+
print(f" Coverage: {avg_coverage * 100:.1f}% [MEASURED]")
|
|
2148
|
+
if conf_dist:
|
|
2149
|
+
conf_str = " ".join(f"{t}:{c}" for t, c in sorted(conf_dist.items()))
|
|
2150
|
+
print(f" Confidence: {conf_str} [MEASURED]")
|
|
2151
|
+
else:
|
|
2152
|
+
print(" Confidence: (no session data) [UNAVAILABLE]")
|
|
2153
|
+
|
|
2154
|
+
print("\n=== Tech Lead ===")
|
|
2155
|
+
print(f" MCS trend: {avg_mcs:.4f} [CALIBRATED]")
|
|
2156
|
+
print(f" I3 violations: {total_i3_violations} [MEASURED]")
|
|
2157
|
+
print(f" Collisions prevented: {total_collisions_prevented} [CALIBRATED]")
|
|
2158
|
+
if delivery_summary:
|
|
2159
|
+
current = delivery_summary["complex_problem_delivery_time_current_min"]
|
|
2160
|
+
baseline = delivery_summary["complex_problem_delivery_time_baseline_min"]
|
|
2161
|
+
delta = delivery_summary["delta_vs_baseline_min"]
|
|
2162
|
+
print(
|
|
2163
|
+
f" Complex problem delivery time: {current:.1f} min "
|
|
2164
|
+
f"(baseline {baseline:.1f}, delta {delta:+.1f}) {delivery_summary['tier']}"
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
print("\n=== CISO ===")
|
|
2168
|
+
print(f" DHS score: {dhs_score:.4f} ({dhs_label}) [CALIBRATED]")
|
|
2169
|
+
shadow_ratio = shadow.get("shadow_ratio", 0.0)
|
|
2170
|
+
flagged_str = " *** FLAGGED ***" if shadow.get("flagged") else ""
|
|
2171
|
+
print(f" Shadow AI ratio: {shadow_ratio:.4f}{flagged_str} [MEASURED]")
|
|
2172
|
+
print(f" Auditability: {auditability * 100:.1f}% [MEASURED]")
|
|
2173
|
+
|
|
2174
|
+
print(disclosure)
|
|
2175
|
+
return 0
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
2179
|
+
epilog = (
|
|
2180
|
+
"Common commands:\n"
|
|
2181
|
+
" devtorch init start tracking this project\n"
|
|
2182
|
+
" devtorch status show the current repository state\n"
|
|
2183
|
+
" devtorch doctor run health checks\n"
|
|
2184
|
+
" devtorch today daily status dashboard\n"
|
|
2185
|
+
" devtorch log --tail 20 recent commit history\n"
|
|
2186
|
+
" devtorch metrics --period 1d recent activity metrics\n"
|
|
2187
|
+
" devtorch topics list list topics with captured reasoning\n"
|
|
2188
|
+
" devtorch concept list-all list all concepts from all sources\n"
|
|
2189
|
+
" devtorch concept learned list concepts from learning interactions\n"
|
|
2190
|
+
" devtorch reasoning history --topic <topic> reasoning history by topic\n"
|
|
2191
|
+
" devtorch sync all [--interval N] bidirectional sync with MCP server\n"
|
|
2192
|
+
"\n"
|
|
2193
|
+
"Run 'devtorch <command> --help' for more information on a command."
|
|
2194
|
+
)
|
|
2195
|
+
parser = argparse.ArgumentParser(
|
|
2196
|
+
prog="devtorch",
|
|
2197
|
+
description="DevTorch — governance for AI-assisted codebases.",
|
|
2198
|
+
epilog=epilog,
|
|
2199
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
2200
|
+
)
|
|
2201
|
+
parser.add_argument(
|
|
2202
|
+
"-C",
|
|
2203
|
+
"--path",
|
|
2204
|
+
default=".",
|
|
2205
|
+
help="Path to the repository root (default: current directory).",
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
2209
|
+
|
|
2210
|
+
# One-command setup: init + hooks + MCP + CLAUDE.md
|
|
2211
|
+
p_setup = subparsers.add_parser(
|
|
2212
|
+
"setup",
|
|
2213
|
+
help="One-command setup: init + MCP server config + IDE-specific wiring.",
|
|
2214
|
+
)
|
|
2215
|
+
p_setup.add_argument(
|
|
2216
|
+
"--target",
|
|
2217
|
+
choices=["claude-code", "cursor", "antigravity", "auto"],
|
|
2218
|
+
default=None,
|
|
2219
|
+
help="IDE to configure (default: auto-detect).",
|
|
2220
|
+
)
|
|
2221
|
+
p_setup.add_argument(
|
|
2222
|
+
"--skip-claude-md",
|
|
2223
|
+
dest="skip_claude_md",
|
|
2224
|
+
action="store_true",
|
|
2225
|
+
help="Skip writing the DevTorch section to CLAUDE.md.",
|
|
2226
|
+
)
|
|
2227
|
+
p_setup.add_argument(
|
|
2228
|
+
"--skip-agents-md",
|
|
2229
|
+
dest="skip_agents_md",
|
|
2230
|
+
action="store_true",
|
|
2231
|
+
help="Skip writing the DevTorch section to AGENTS.md (Google Antigravity).",
|
|
2232
|
+
)
|
|
2233
|
+
p_setup.set_defaults(func=cmd_setup)
|
|
2234
|
+
|
|
2235
|
+
p_init = subparsers.add_parser("init", help="Initialize the .GCC layout.")
|
|
2236
|
+
p_init.add_argument(
|
|
2237
|
+
"--template",
|
|
2238
|
+
choices=["python", "typescript", "react", "go", "infra"],
|
|
2239
|
+
default=None,
|
|
2240
|
+
help="Apply a project template with default concepts and governance policy.",
|
|
2241
|
+
)
|
|
2242
|
+
p_init.set_defaults(func=cmd_init)
|
|
2243
|
+
|
|
2244
|
+
p_serve = subparsers.add_parser(
|
|
2245
|
+
"serve",
|
|
2246
|
+
help="Start the unified local dev server (proxy + dashboard + broadcast + MCP).",
|
|
2247
|
+
)
|
|
2248
|
+
p_serve.add_argument(
|
|
2249
|
+
"--host", default="127.0.0.1", help="Host interface to bind (default: 127.0.0.1)."
|
|
2250
|
+
)
|
|
2251
|
+
p_serve.add_argument(
|
|
2252
|
+
"--port", type=int, default=8765, help="Port to bind (default: 8765)."
|
|
2253
|
+
)
|
|
2254
|
+
p_serve.add_argument(
|
|
2255
|
+
"--gcc-path", default=None, help="Path to project root containing .GCC (default: auto-detect)."
|
|
2256
|
+
)
|
|
2257
|
+
p_serve.set_defaults(func=_cmd_serve)
|
|
2258
|
+
|
|
2259
|
+
p_welcome = subparsers.add_parser(
|
|
2260
|
+
"welcome", help="Show a quick getting-started guide."
|
|
2261
|
+
)
|
|
2262
|
+
p_welcome.set_defaults(func=cmd_welcome)
|
|
2263
|
+
|
|
2264
|
+
p_ask = subparsers.add_parser(
|
|
2265
|
+
"ask", help="Hybrid grep + semantic search over .GCC/ and project files (S27)."
|
|
2266
|
+
)
|
|
2267
|
+
p_ask.add_argument("query", help="Search query.")
|
|
2268
|
+
p_ask.add_argument(
|
|
2269
|
+
"--top-n", type=int, default=5, dest="top_n",
|
|
2270
|
+
help="Maximum number of results to show (default: 5)."
|
|
2271
|
+
)
|
|
2272
|
+
p_ask.add_argument(
|
|
2273
|
+
"--semantic", action="store_true", dest="semantic",
|
|
2274
|
+
help="Force semantic search (falls back to keyword search if sentence-transformers is unavailable)."
|
|
2275
|
+
)
|
|
2276
|
+
p_ask.add_argument(
|
|
2277
|
+
"--no-semantic", action="store_true", dest="no_semantic",
|
|
2278
|
+
help="Force grep-only search."
|
|
2279
|
+
)
|
|
2280
|
+
p_ask.set_defaults(func=cmd_ask)
|
|
2281
|
+
|
|
2282
|
+
p_today = subparsers.add_parser(
|
|
2283
|
+
"today", help="Daily status dashboard (status + doctor + log + metrics)."
|
|
2284
|
+
)
|
|
2285
|
+
p_today.add_argument(
|
|
2286
|
+
"--tail", type=int, default=20, help="Recent log lines to show (default: 20)."
|
|
2287
|
+
)
|
|
2288
|
+
p_today.set_defaults(func=cmd_today)
|
|
2289
|
+
|
|
2290
|
+
p_commit = subparsers.add_parser("commit", help="Record a reasoning commit (Sprint 0 happy path).")
|
|
2291
|
+
p_commit.add_argument("-m", "--message", help="Commit message.", default="")
|
|
2292
|
+
p_commit.add_argument(
|
|
2293
|
+
"--concepts",
|
|
2294
|
+
help="Comma-separated concept tags (e.g. auth,security). Phase 5: developer-agent handoff.",
|
|
2295
|
+
default="",
|
|
2296
|
+
)
|
|
2297
|
+
p_commit.set_defaults(func=cmd_commit)
|
|
2298
|
+
|
|
2299
|
+
# Phase 5: reasoning chain
|
|
2300
|
+
p_chain = subparsers.add_parser(
|
|
2301
|
+
"chain", help="Show the full reasoning chain: human commit -> agent session -> calls -> learnings -> outcomes (Phase 5)."
|
|
2302
|
+
)
|
|
2303
|
+
p_chain.add_argument(
|
|
2304
|
+
"--concept",
|
|
2305
|
+
default=None,
|
|
2306
|
+
help="Filter chains by concept name (matches commit concepts and message).",
|
|
2307
|
+
)
|
|
2308
|
+
p_chain.add_argument(
|
|
2309
|
+
"--max", type=int, default=5, dest="max_chains",
|
|
2310
|
+
help="Maximum number of chains to show (default: 5).",
|
|
2311
|
+
)
|
|
2312
|
+
p_chain.set_defaults(func=cmd_chain)
|
|
2313
|
+
|
|
2314
|
+
p_doctor = subparsers.add_parser("doctor", help="Validate .GCC layout and event log integrity.")
|
|
2315
|
+
p_doctor.add_argument(
|
|
2316
|
+
"--ci",
|
|
2317
|
+
action="store_true",
|
|
2318
|
+
help="CI mode: enforce --min-dhs-score and --critical-concepts thresholds.",
|
|
2319
|
+
)
|
|
2320
|
+
p_doctor.add_argument(
|
|
2321
|
+
"--min-dhs-score",
|
|
2322
|
+
type=float,
|
|
2323
|
+
default=0.0,
|
|
2324
|
+
help="Minimum Deployment Health Score (0-1) for CI mode to pass.",
|
|
2325
|
+
)
|
|
2326
|
+
p_doctor.add_argument(
|
|
2327
|
+
"--critical-concepts",
|
|
2328
|
+
type=str,
|
|
2329
|
+
default="",
|
|
2330
|
+
help="Comma-separated list of concepts that must exist in Θ for CI mode to pass.",
|
|
2331
|
+
)
|
|
2332
|
+
p_doctor.set_defaults(func=cmd_doctor)
|
|
2333
|
+
|
|
2334
|
+
p_branch = subparsers.add_parser("branch", help="Create a new branch.")
|
|
2335
|
+
p_branch.add_argument("name", help="New branch name.")
|
|
2336
|
+
p_branch.add_argument("--from-ref", help="Commit ID to start from (default: current tip).", default=None)
|
|
2337
|
+
p_branch.set_defaults(func=cmd_branch)
|
|
2338
|
+
|
|
2339
|
+
p_history = subparsers.add_parser("history", help="Show commit history.")
|
|
2340
|
+
p_history.add_argument("--branch", help="Branch name (default: current branch).", default=None)
|
|
2341
|
+
p_history.set_defaults(func=cmd_history)
|
|
2342
|
+
|
|
2343
|
+
p_merge = subparsers.add_parser("merge", help="Fast-forward merge source into target branch.")
|
|
2344
|
+
p_merge.add_argument("source", help="Source branch name.")
|
|
2345
|
+
p_merge.add_argument(
|
|
2346
|
+
"--target",
|
|
2347
|
+
help="Target branch name (default: current branch).",
|
|
2348
|
+
default=None,
|
|
2349
|
+
)
|
|
2350
|
+
p_merge.set_defaults(func=cmd_merge)
|
|
2351
|
+
|
|
2352
|
+
p_context = subparsers.add_parser("context", help="Build a MECW-aware context bundle.")
|
|
2353
|
+
p_context.add_argument(
|
|
2354
|
+
"-k",
|
|
2355
|
+
type=int,
|
|
2356
|
+
required=True,
|
|
2357
|
+
help="Approximate token budget for the bundle.",
|
|
2358
|
+
)
|
|
2359
|
+
p_context.add_argument(
|
|
2360
|
+
"--branch",
|
|
2361
|
+
help="Branch name to build context from (default: current branch).",
|
|
2362
|
+
default=None,
|
|
2363
|
+
)
|
|
2364
|
+
p_context.add_argument(
|
|
2365
|
+
"--policy",
|
|
2366
|
+
help="Optional sensitivity policy name (e.g. 'default', 'no-private'); "
|
|
2367
|
+
"recorded in the bundle for future enforcement.",
|
|
2368
|
+
default="default",
|
|
2369
|
+
)
|
|
2370
|
+
p_context.add_argument(
|
|
2371
|
+
"--explain",
|
|
2372
|
+
action="store_true",
|
|
2373
|
+
help="Include bundle file path in the output for debugging.",
|
|
2374
|
+
)
|
|
2375
|
+
p_context.set_defaults(func=cmd_context)
|
|
2376
|
+
|
|
2377
|
+
# Sprint 3: sensitivity subcommands
|
|
2378
|
+
p_sens = subparsers.add_parser("sensitivity", help="Manage sensitivity events.")
|
|
2379
|
+
sens_sub = p_sens.add_subparsers(dest="sens_command", required=True)
|
|
2380
|
+
|
|
2381
|
+
p_sens_add = sens_sub.add_parser("add", help="Record a sensitivity event.")
|
|
2382
|
+
p_sens_add.add_argument("--source", required=True, help="Source node identifier.")
|
|
2383
|
+
p_sens_add.add_argument("--concept", required=True, help="Target concept name.")
|
|
2384
|
+
p_sens_add.add_argument("--confidence", type=float, required=True, help="Confidence [0, 1].")
|
|
2385
|
+
p_sens_add.add_argument(
|
|
2386
|
+
"--disclosure",
|
|
2387
|
+
default="PUBLIC",
|
|
2388
|
+
choices=["PUBLIC", "PROTECTED", "PRIVATE"],
|
|
2389
|
+
help="Disclosure level (default: PUBLIC).",
|
|
2390
|
+
)
|
|
2391
|
+
p_sens_add.add_argument("--counterfactuals", default="", help="Counterfactual description.")
|
|
2392
|
+
p_sens_add.add_argument("--numerical", type=float, default=None, help="Optional numerical sensitivity value.")
|
|
2393
|
+
p_sens_add.add_argument("--uncertainty", type=float, default=None, help="Optional ±uncertainty for numerical value.")
|
|
2394
|
+
p_sens_add.add_argument("-m", "--message", default="", help="Human-readable annotation.")
|
|
2395
|
+
p_sens_add.set_defaults(func=cmd_sensitivity_add)
|
|
2396
|
+
|
|
2397
|
+
p_sens_list = sens_sub.add_parser("list", help="List sensitivity events.")
|
|
2398
|
+
p_sens_list.add_argument("--concept", default=None, help="Filter by concept name.")
|
|
2399
|
+
p_sens_list.add_argument("--disclosure", default=None, choices=["PUBLIC", "PROTECTED", "PRIVATE"],
|
|
2400
|
+
help="Filter by disclosure level.")
|
|
2401
|
+
p_sens_list.add_argument("--source-node", default=None, dest="source_node",
|
|
2402
|
+
help="Filter by source node identifier.")
|
|
2403
|
+
p_sens_list.set_defaults(func=cmd_sensitivity_list)
|
|
2404
|
+
|
|
2405
|
+
p_sens_filter = sens_sub.add_parser("filter", help="Filter sensitivities and show Θ vector.")
|
|
2406
|
+
p_sens_filter.add_argument("--concept", default=None, help="Filter by concept name.")
|
|
2407
|
+
p_sens_filter.add_argument("--disclosure", default=None, choices=["PUBLIC", "PROTECTED", "PRIVATE"],
|
|
2408
|
+
help="Filter by disclosure level.")
|
|
2409
|
+
p_sens_filter.add_argument("--source-node", default=None, dest="source_node",
|
|
2410
|
+
help="Filter by source node identifier.")
|
|
2411
|
+
p_sens_filter.set_defaults(func=cmd_sensitivity_filter)
|
|
2412
|
+
|
|
2413
|
+
# Sprint 3: capability subcommands
|
|
2414
|
+
p_cap = subparsers.add_parser("capability", help="Manage Ωᵢ capability declarations.")
|
|
2415
|
+
cap_sub = p_cap.add_subparsers(dest="cap_command", required=True)
|
|
2416
|
+
|
|
2417
|
+
p_cap_set = cap_sub.add_parser("set", help="Set the Ωᵢ capability declaration.")
|
|
2418
|
+
p_cap_set.add_argument("--agent-id", required=True, dest="agent_id", help="Agent identifier.")
|
|
2419
|
+
p_cap_set.add_argument("--lipschitz-bound", type=float, required=True, dest="lipschitz_bound",
|
|
2420
|
+
help="Empirical Lipschitz bound L̂.")
|
|
2421
|
+
p_cap_set.add_argument("--calibration-hash", required=True, dest="calibration_hash",
|
|
2422
|
+
help="SHA-256 hex digest of calibration dataset.")
|
|
2423
|
+
p_cap_set.add_argument("--embedding-model", required=True, dest="embedding_model",
|
|
2424
|
+
help="Embedding model name/version.")
|
|
2425
|
+
p_cap_set.add_argument("--calibration-date", required=True, dest="calibration_date",
|
|
2426
|
+
help="ISO date of calibration run (YYYY-MM-DD).")
|
|
2427
|
+
p_cap_set.add_argument("--temperature", type=float, required=True, dest="temperature",
|
|
2428
|
+
help="Sampling temperature used during calibration [0, 2].")
|
|
2429
|
+
p_cap_set.add_argument("--lipschitz-p95", type=float, default=None, dest="lipschitz_p95",
|
|
2430
|
+
help="Optional 95th-percentile Lipschitz estimate.")
|
|
2431
|
+
p_cap_set.add_argument("--expiry-days", type=int, default=90, dest="expiry_days",
|
|
2432
|
+
help="Days until calibration expires (default: 90).")
|
|
2433
|
+
p_cap_set.set_defaults(func=cmd_capability_set)
|
|
2434
|
+
|
|
2435
|
+
p_cap_show = cap_sub.add_parser("show", help="Display the current Ωᵢ declaration.")
|
|
2436
|
+
p_cap_show.set_defaults(func=cmd_capability_show)
|
|
2437
|
+
|
|
2438
|
+
p_cap_val = cap_sub.add_parser("validate", help="Validate Ωᵢ against A1 gate conditions.")
|
|
2439
|
+
p_cap_val.add_argument("--l-max", type=float, default=L_MAX_DEFAULT, dest="l_max",
|
|
2440
|
+
help=f"Lipschitz bound threshold L_max (default: {L_MAX_DEFAULT}).")
|
|
2441
|
+
p_cap_val.set_defaults(func=cmd_capability_validate)
|
|
2442
|
+
|
|
2443
|
+
# S28: capability role subcommands
|
|
2444
|
+
p_cap_role = cap_sub.add_parser("role", help="Manage agent roles and role policy (S28).")
|
|
2445
|
+
role_sub = p_cap_role.add_subparsers(dest="role_command", required=True)
|
|
2446
|
+
|
|
2447
|
+
p_role_list = role_sub.add_parser("list", help="List defined agent roles.")
|
|
2448
|
+
p_role_list.set_defaults(func=cmd_capability_role_list)
|
|
2449
|
+
|
|
2450
|
+
p_role_show = role_sub.add_parser("show", help="Show a role definition.")
|
|
2451
|
+
p_role_show.add_argument("role_id", help="Role identifier.")
|
|
2452
|
+
p_role_show.set_defaults(func=cmd_capability_role_show)
|
|
2453
|
+
|
|
2454
|
+
p_role_set = role_sub.add_parser("set", help="Create or update a role.")
|
|
2455
|
+
p_role_set.add_argument("role_id", help="Role identifier.")
|
|
2456
|
+
p_role_set.add_argument("--name", default=None, help="Human-readable role name.")
|
|
2457
|
+
p_role_set.add_argument("--allowed", default="", help="Comma-separated allowed concepts.")
|
|
2458
|
+
p_role_set.add_argument("--disallowed", default="", help="Comma-separated disallowed concepts.")
|
|
2459
|
+
p_role_set.add_argument("--required", default="", help="Comma-required capability fields.")
|
|
2460
|
+
p_role_set.add_argument("--max-temperature", type=float, default=None, dest="max_temperature",
|
|
2461
|
+
help="Maximum allowed temperature for this role.")
|
|
2462
|
+
p_role_set.set_defaults(func=cmd_capability_role_set)
|
|
2463
|
+
|
|
2464
|
+
p_role_policy = role_sub.add_parser("policy", help="Show the current role policy.")
|
|
2465
|
+
p_role_policy.set_defaults(func=cmd_capability_role_policy)
|
|
2466
|
+
|
|
2467
|
+
p_role_default = role_sub.add_parser("apply-default-policy", help="Apply the built-in default role policy.")
|
|
2468
|
+
p_role_default.set_defaults(func=cmd_capability_role_apply_default_policy)
|
|
2469
|
+
|
|
2470
|
+
# Sprint 4: lock, unlock, invariant, concept, variance
|
|
2471
|
+
p_lock = subparsers.add_parser("lock", help="Acquire consensus lock.")
|
|
2472
|
+
p_lock.add_argument("--reason", default="", help="Reason for locking.")
|
|
2473
|
+
p_lock.add_argument("--peers", type=int, default=None, dest="peers", help="Required peer count (optional).")
|
|
2474
|
+
p_lock.set_defaults(func=cmd_lock)
|
|
2475
|
+
|
|
2476
|
+
p_unlock = subparsers.add_parser("unlock", help="Release consensus lock (I1 checked).")
|
|
2477
|
+
p_unlock.set_defaults(func=cmd_unlock)
|
|
2478
|
+
|
|
2479
|
+
p_inv = subparsers.add_parser("invariant", help="Run invariant checks.")
|
|
2480
|
+
inv_sub = p_inv.add_subparsers(dest="inv_command", required=True)
|
|
2481
|
+
p_inv_check = inv_sub.add_parser("check", help="Run I1/I3 invariant engine.")
|
|
2482
|
+
p_inv_check.add_argument("--operation", default="merge", help="Operation context (default: merge).")
|
|
2483
|
+
p_inv_check.add_argument("--concepts", default="", help="Comma-separated concept names for I3.")
|
|
2484
|
+
p_inv_check.set_defaults(func=cmd_invariant_check)
|
|
2485
|
+
|
|
2486
|
+
# S29: alias `invariants` for the invariant command (plural form used in pre-commit hooks).
|
|
2487
|
+
p_inv_plural = subparsers.add_parser("invariants", help="Alias for `devtorch invariant`.")
|
|
2488
|
+
inv_plural_sub = p_inv_plural.add_subparsers(dest="inv_command", required=True)
|
|
2489
|
+
p_inv_plural_check = inv_plural_sub.add_parser("check", help="Run I1/I3 invariant engine.")
|
|
2490
|
+
p_inv_plural_check.add_argument("--operation", default="merge", help="Operation context (default: merge).")
|
|
2491
|
+
p_inv_plural_check.add_argument("--concepts", default="", help="Comma-separated concept names for I3.")
|
|
2492
|
+
p_inv_plural_check.set_defaults(func=cmd_invariant_check)
|
|
2493
|
+
|
|
2494
|
+
p_concept = subparsers.add_parser("concept", help="Manage concept definitions (I3 semantic grounding).")
|
|
2495
|
+
concept_sub = p_concept.add_subparsers(dest="concept_command", required=True)
|
|
2496
|
+
p_concept_add = concept_sub.add_parser("add", help="Add a concept definition.")
|
|
2497
|
+
p_concept_add.add_argument("name", help="Concept name.")
|
|
2498
|
+
p_concept_add.add_argument("definition", help="Concept definition text.")
|
|
2499
|
+
p_concept_add.set_defaults(func=cmd_concept_add)
|
|
2500
|
+
p_concept_list = concept_sub.add_parser("list", help="List registered concepts.")
|
|
2501
|
+
p_concept_list.set_defaults(func=cmd_concept_list)
|
|
2502
|
+
p_concept_danger = concept_sub.add_parser(
|
|
2503
|
+
"danger", help="Detect concepts with abnormally low learning success rates (Phase 3)."
|
|
2504
|
+
)
|
|
2505
|
+
p_concept_danger.add_argument(
|
|
2506
|
+
"--threshold", type=float, default=0.4,
|
|
2507
|
+
help="Success rate threshold below which a concept is flagged (default: 0.4).",
|
|
2508
|
+
)
|
|
2509
|
+
p_concept_danger.add_argument(
|
|
2510
|
+
"--min-samples", type=int, default=3,
|
|
2511
|
+
help="Minimum learnings required to consider a concept (default: 3).",
|
|
2512
|
+
)
|
|
2513
|
+
p_concept_danger.set_defaults(func=cmd_concept_danger)
|
|
2514
|
+
|
|
2515
|
+
# Phase 5B: concept list-all, learned, related subcommands
|
|
2516
|
+
p_concept_list_all = concept_sub.add_parser(
|
|
2517
|
+
"list-all", help="List all concepts from every source (theta, sensitivity, learnings, commits, calls)."
|
|
2518
|
+
)
|
|
2519
|
+
p_concept_list_all.set_defaults(func=cmd_concept_list_all)
|
|
2520
|
+
|
|
2521
|
+
p_concept_learned = concept_sub.add_parser(
|
|
2522
|
+
"learned", help="List concepts learned from interactions with learning stats."
|
|
2523
|
+
)
|
|
2524
|
+
p_concept_learned.set_defaults(func=cmd_concept_learned)
|
|
2525
|
+
|
|
2526
|
+
p_concept_related = concept_sub.add_parser(
|
|
2527
|
+
"related", help="List concepts related to a given topic."
|
|
2528
|
+
)
|
|
2529
|
+
p_concept_related.add_argument("--topic", required=True, help="Topic name.")
|
|
2530
|
+
p_concept_related.set_defaults(func=cmd_concept_related)
|
|
2531
|
+
|
|
2532
|
+
p_var = subparsers.add_parser("variance", help="Variance calibration and rolling monitor (f_max, A2/I2).")
|
|
2533
|
+
var_sub = p_var.add_subparsers(dest="var_command", required=True)
|
|
2534
|
+
p_var_cal = var_sub.add_parser("calibrate", help="Run calibration and persist report.")
|
|
2535
|
+
p_var_cal.add_argument("--variances", default="", help="Comma-separated agent_id=sigma_sq (e.g. a1=0.1,a2=0.2).")
|
|
2536
|
+
p_var_cal.set_defaults(func=cmd_variance_calibrate)
|
|
2537
|
+
p_var_mon = var_sub.add_parser("monitor", help="Run rolling monitor; emit VARIANCE_ALERT if f_max drops or is 0.")
|
|
2538
|
+
p_var_mon.add_argument("--variances", default="", help="Optional: comma-separated agent_id=sigma_sq for live re-check.")
|
|
2539
|
+
p_var_mon.set_defaults(func=cmd_variance_monitor)
|
|
2540
|
+
|
|
2541
|
+
# Sprint 5: rep, sis-tc, prompt, deltaf, rdp, disclosure
|
|
2542
|
+
p_rep = subparsers.add_parser("rep", help="REP envelope and local ledger (S5).")
|
|
2543
|
+
rep_sub = p_rep.add_subparsers(dest="rep_command", required=True)
|
|
2544
|
+
p_rep_append = rep_sub.add_parser("append", help="Append REP envelope to ledger.")
|
|
2545
|
+
p_rep_append.add_argument("--agent-id", required=True, dest="agent_id", help="Agent ID.")
|
|
2546
|
+
p_rep_append.add_argument("--round", type=int, required=True, dest="round_id", help="Round ID.")
|
|
2547
|
+
p_rep_append.add_argument("--payload", default="{}", help="JSON payload.")
|
|
2548
|
+
p_rep_append.add_argument("--trust", type=float, default=0.5, help="Trust score [0,1].")
|
|
2549
|
+
p_rep_append.set_defaults(func=cmd_rep_append)
|
|
2550
|
+
p_rep_checksum = rep_sub.add_parser("checksum", help="Replay checksum of ledger.")
|
|
2551
|
+
p_rep_checksum.set_defaults(func=cmd_rep_checksum)
|
|
2552
|
+
|
|
2553
|
+
p_sis = subparsers.add_parser("sis-tc", help="SIS-TC evaluation (A3).")
|
|
2554
|
+
sis_sub = p_sis.add_subparsers(dest="sis_command", required=True)
|
|
2555
|
+
p_sis_run = sis_sub.add_parser("run", help="Run SIS-TC eval; persist report.")
|
|
2556
|
+
p_sis_run.add_argument("--corpus", default=None, help="Path to corpus JSONL (default: .GCC/sis/sis_tc_corpus.jsonl).")
|
|
2557
|
+
p_sis_run.set_defaults(func=cmd_sis_tc_run)
|
|
2558
|
+
|
|
2559
|
+
p_prompt = subparsers.add_parser("prompt", help="RACP system prompt artifact (A3).")
|
|
2560
|
+
prompt_sub = p_prompt.add_subparsers(dest="prompt_command", required=True)
|
|
2561
|
+
p_prompt_set = prompt_sub.add_parser("set", help="Store RACP_SYSTEM_PROMPT_v2.1.")
|
|
2562
|
+
p_prompt_set.add_argument("--file", default=None, help="Read content from file.")
|
|
2563
|
+
p_prompt_set.add_argument("content", nargs="?", default="", help="Prompt content (or use --file).")
|
|
2564
|
+
p_prompt_set.add_argument("--version", default="v2.1")
|
|
2565
|
+
p_prompt_set.set_defaults(func=cmd_prompt_set)
|
|
2566
|
+
p_prompt_show = prompt_sub.add_parser("show", help="Show stored prompt.")
|
|
2567
|
+
p_prompt_show.set_defaults(func=cmd_prompt_show)
|
|
2568
|
+
|
|
2569
|
+
p_deltaf = subparsers.add_parser("deltaf", help="Δf estimation (A4).")
|
|
2570
|
+
deltaf_sub = p_deltaf.add_subparsers(dest="deltaf_command", required=True)
|
|
2571
|
+
p_deltaf_run = deltaf_sub.add_parser("run", help="Run Δf estimation; persist report.")
|
|
2572
|
+
p_deltaf_run.add_argument("--expiry-days", type=int, default=90, dest="expiry_days")
|
|
2573
|
+
p_deltaf_run.set_defaults(func=cmd_deltaf_run)
|
|
2574
|
+
|
|
2575
|
+
p_rdp = subparsers.add_parser("rdp", help="RDP accountant (A4).")
|
|
2576
|
+
rdp_sub = p_rdp.add_subparsers(dest="rdp_command", required=True)
|
|
2577
|
+
p_rdp_spend = rdp_sub.add_parser("spend", help="Spend epsilon for round.")
|
|
2578
|
+
p_rdp_spend.add_argument("round_id", type=int, help="Round ID.")
|
|
2579
|
+
p_rdp_spend.add_argument("epsilon", type=float, help="Epsilon cost.")
|
|
2580
|
+
p_rdp_spend.set_defaults(func=cmd_rdp_spend)
|
|
2581
|
+
p_rdp_status = rdp_sub.add_parser("status", help="Show read_only status.")
|
|
2582
|
+
p_rdp_status.set_defaults(func=cmd_rdp_status)
|
|
2583
|
+
|
|
2584
|
+
p_disc = subparsers.add_parser("disclosure", help="[PRIVATE] suppression and padding (A4).")
|
|
2585
|
+
disc_sub = p_disc.add_subparsers(dest="disclosure_command", required=True)
|
|
2586
|
+
p_disc_redact = disc_sub.add_parser("redact", help="Redact [PRIVATE] with padding.")
|
|
2587
|
+
p_disc_redact.add_argument("text", nargs="?", default="", help="Input text (or stdin).")
|
|
2588
|
+
p_disc_redact.add_argument("--padding", type=int, default=32, help="Padding length.")
|
|
2589
|
+
p_disc_redact.set_defaults(func=cmd_disclosure_redact)
|
|
2590
|
+
|
|
2591
|
+
# Sprint 6: debug
|
|
2592
|
+
p_debug = subparsers.add_parser("debug", help="Debug views (timeline, branch comparison, invariant trace).")
|
|
2593
|
+
debug_sub = p_debug.add_subparsers(dest="debug_command", required=True)
|
|
2594
|
+
p_debug_timeline = debug_sub.add_parser("timeline", help="Event timeline, optionally filtered.")
|
|
2595
|
+
p_debug_timeline.add_argument(
|
|
2596
|
+
"--filter",
|
|
2597
|
+
default=None,
|
|
2598
|
+
help="Comma-separated event types (e.g. CAPABILITY_DEGRADED,VARIANCE_ALERT,QUARANTINE,PRIVACY_BUDGET_EXHAUSTED).",
|
|
2599
|
+
)
|
|
2600
|
+
p_debug_timeline.set_defaults(func=cmd_debug_timeline)
|
|
2601
|
+
p_debug_branch = debug_sub.add_parser("branch-compare", help="Compare two branches (history + Θ).")
|
|
2602
|
+
p_debug_branch.add_argument("branch1", help="First branch name.")
|
|
2603
|
+
p_debug_branch.add_argument("branch2", help="Second branch name.")
|
|
2604
|
+
p_debug_branch.set_defaults(func=cmd_debug_branch_compare)
|
|
2605
|
+
p_debug_inv = debug_sub.add_parser("invariant-trace", help="Show invariant failures and actionable fixes.")
|
|
2606
|
+
p_debug_inv.set_defaults(func=cmd_debug_invariant_trace)
|
|
2607
|
+
|
|
2608
|
+
# Sprint 9: status command
|
|
2609
|
+
p_status = subparsers.add_parser("status", help="Show repository status (branch, commits, Θ hot zones, invariants, lock).")
|
|
2610
|
+
p_status.set_defaults(func=cmd_status)
|
|
2611
|
+
|
|
2612
|
+
# Sprint 9: log command
|
|
2613
|
+
p_log = subparsers.add_parser("log", help="Print .GCC/log.md to stdout.")
|
|
2614
|
+
p_log.add_argument("--tail", type=int, default=None, metavar="N",
|
|
2615
|
+
help="Print last N lines only (default: print all).")
|
|
2616
|
+
p_log.add_argument("--branch", default=None, metavar="B",
|
|
2617
|
+
help="Branch filter (note: log.md is global; filter is noted but not applied).")
|
|
2618
|
+
p_log.set_defaults(func=cmd_log)
|
|
2619
|
+
|
|
2620
|
+
# Sprint 9: signing subcommand
|
|
2621
|
+
p_signing = subparsers.add_parser("signing", help="Optional HMAC-SHA256 signing of events.log.jsonl.")
|
|
2622
|
+
signing_sub = p_signing.add_subparsers(dest="signing_command", required=True)
|
|
2623
|
+
|
|
2624
|
+
p_signing_enable = signing_sub.add_parser("enable", help="Enable event log signing (creates .GCC/signing.key if needed).")
|
|
2625
|
+
p_signing_enable.set_defaults(func=cmd_signing_enable)
|
|
2626
|
+
|
|
2627
|
+
p_signing_disable = signing_sub.add_parser("disable", help="Disable event log signing (keeps existing key).")
|
|
2628
|
+
p_signing_disable.set_defaults(func=cmd_signing_disable)
|
|
2629
|
+
|
|
2630
|
+
p_signing_verify = signing_sub.add_parser("verify", help="Verify signatures in events.log.jsonl.")
|
|
2631
|
+
p_signing_verify.set_defaults(func=cmd_signing_verify)
|
|
2632
|
+
|
|
2633
|
+
# Sprint 30: audit subcommand
|
|
2634
|
+
build_audit_parser(subparsers)
|
|
2635
|
+
|
|
2636
|
+
# Sprint 19: projects subcommand
|
|
2637
|
+
p_projects = subparsers.add_parser("projects", help="Manage the machine-level DevTorch project registry.")
|
|
2638
|
+
projects_sub = p_projects.add_subparsers(dest="projects_command", required=True)
|
|
2639
|
+
|
|
2640
|
+
p_projects_add = projects_sub.add_parser("add", help="Add a project to the registry.")
|
|
2641
|
+
p_projects_add.add_argument("path", help="Path to the project root.")
|
|
2642
|
+
p_projects_add.add_argument("--name", default=None, help="Optional project name.")
|
|
2643
|
+
p_projects_add.add_argument("--developer-id", default=None, help="Optional developer ID override.")
|
|
2644
|
+
p_projects_add.add_argument(
|
|
2645
|
+
"--configure-mcp",
|
|
2646
|
+
action="store_true",
|
|
2647
|
+
dest="configure_mcp",
|
|
2648
|
+
help="Also write per-project MCP configs (.claude/hooks.json, .cursor/mcp.json).",
|
|
2649
|
+
)
|
|
2650
|
+
p_projects_add.set_defaults(func=cmd_projects_add)
|
|
2651
|
+
|
|
2652
|
+
p_projects_list = projects_sub.add_parser("list", help="List registered projects.")
|
|
2653
|
+
p_projects_list.set_defaults(func=cmd_projects_list)
|
|
2654
|
+
|
|
2655
|
+
p_projects_remove = projects_sub.add_parser("remove", help="Remove a project from the registry.")
|
|
2656
|
+
p_projects_remove.add_argument("path", help="Path to the project root.")
|
|
2657
|
+
p_projects_remove.set_defaults(func=cmd_projects_remove)
|
|
2658
|
+
|
|
2659
|
+
p_projects_sync = projects_sub.add_parser("sync", help="Refresh last_synced for all registered projects.")
|
|
2660
|
+
p_projects_sync.set_defaults(func=cmd_projects_sync)
|
|
2661
|
+
|
|
2662
|
+
# S7: proxy subcommand (server in devtorch_core/proxy/)
|
|
2663
|
+
p_proxy = subparsers.add_parser("proxy", help="LLM proxy management (S7).")
|
|
2664
|
+
proxy_sub = p_proxy.add_subparsers(dest="proxy_command", required=True)
|
|
2665
|
+
|
|
2666
|
+
p_proxy_start = proxy_sub.add_parser("start", help="Start the LLM proxy server in the foreground.")
|
|
2667
|
+
p_proxy_start.add_argument("--port", type=int, default=8765, help="Port to listen on (default: 8765).")
|
|
2668
|
+
p_proxy_start.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1).")
|
|
2669
|
+
p_proxy_start.set_defaults(func=cmd_proxy_start)
|
|
2670
|
+
|
|
2671
|
+
p_proxy_stop = proxy_sub.add_parser("stop", help="Print instructions to stop the proxy.")
|
|
2672
|
+
p_proxy_stop.set_defaults(func=cmd_proxy_stop)
|
|
2673
|
+
|
|
2674
|
+
p_proxy_install = proxy_sub.add_parser("install", help="Install proxy as a system service.")
|
|
2675
|
+
p_proxy_install.set_defaults(func=cmd_proxy_install)
|
|
2676
|
+
|
|
2677
|
+
p_proxy_status = proxy_sub.add_parser("status", help="Check if the proxy is running on :8765.")
|
|
2678
|
+
p_proxy_status.set_defaults(func=cmd_proxy_status)
|
|
2679
|
+
|
|
2680
|
+
# S26: daemon subcommand
|
|
2681
|
+
p_daemon = subparsers.add_parser(
|
|
2682
|
+
"daemon", help="Background file watcher and server (S26)."
|
|
2683
|
+
)
|
|
2684
|
+
daemon_sub = p_daemon.add_subparsers(dest="daemon_command", required=True)
|
|
2685
|
+
|
|
2686
|
+
p_daemon_start = daemon_sub.add_parser("start", help="Start the background daemon.")
|
|
2687
|
+
p_daemon_start.add_argument(
|
|
2688
|
+
"--run-server", action="store_true", dest="run_server",
|
|
2689
|
+
help="Also start the unified DevTorch server in the daemon process.",
|
|
2690
|
+
)
|
|
2691
|
+
p_daemon_start.add_argument(
|
|
2692
|
+
"--host", default="127.0.0.1", help="Host to bind the unified server to (default: 127.0.0.1)."
|
|
2693
|
+
)
|
|
2694
|
+
p_daemon_start.add_argument(
|
|
2695
|
+
"--port", type=int, default=8765, help="Port to bind the unified server to (default: 8765)."
|
|
2696
|
+
)
|
|
2697
|
+
p_daemon_start.set_defaults(func=cmd_daemon_start)
|
|
2698
|
+
|
|
2699
|
+
p_daemon_stop = daemon_sub.add_parser("stop", help="Stop the background daemon.")
|
|
2700
|
+
p_daemon_stop.set_defaults(func=cmd_daemon_stop)
|
|
2701
|
+
|
|
2702
|
+
p_daemon_status = daemon_sub.add_parser("status", help="Show daemon status.")
|
|
2703
|
+
p_daemon_status.set_defaults(func=cmd_daemon_status)
|
|
2704
|
+
|
|
2705
|
+
p_daemon_logs = daemon_sub.add_parser("logs", help="Print the daemon log.")
|
|
2706
|
+
p_daemon_logs.add_argument(
|
|
2707
|
+
"--lines", type=int, default=50, help="Number of lines to show (default: 50)."
|
|
2708
|
+
)
|
|
2709
|
+
p_daemon_logs.set_defaults(func=cmd_daemon_logs)
|
|
2710
|
+
|
|
2711
|
+
# S28: session subcommand
|
|
2712
|
+
p_session = subparsers.add_parser(
|
|
2713
|
+
"session", help="Multi-agent session orchestration (S28)."
|
|
2714
|
+
)
|
|
2715
|
+
session_sub = p_session.add_subparsers(dest="session_command", required=True)
|
|
2716
|
+
|
|
2717
|
+
p_session_start = session_sub.add_parser("start", help="Start a new multi-agent session.")
|
|
2718
|
+
p_session_start.add_argument("--name", required=True, help="Session name.")
|
|
2719
|
+
p_session_start.add_argument("--problem", required=True, help="Problem statement to solve.")
|
|
2720
|
+
p_session_start.add_argument(
|
|
2721
|
+
"--agent", action="append", dest="agents", default=[],
|
|
2722
|
+
help="Agent assignment AGENT_ID=ROLE (repeatable).",
|
|
2723
|
+
)
|
|
2724
|
+
p_session_start.set_defaults(func=cmd_session_start)
|
|
2725
|
+
|
|
2726
|
+
p_session_list = session_sub.add_parser("list", help="List sessions.")
|
|
2727
|
+
p_session_list.set_defaults(func=cmd_session_list)
|
|
2728
|
+
|
|
2729
|
+
p_session_show = session_sub.add_parser("show", help="Show session details.")
|
|
2730
|
+
p_session_show.add_argument("session_id", help="Session ID.")
|
|
2731
|
+
p_session_show.set_defaults(func=cmd_session_show)
|
|
2732
|
+
|
|
2733
|
+
p_session_plan = session_sub.add_parser("plan", help="Decompose the session into subtasks.")
|
|
2734
|
+
p_session_plan.add_argument("session_id", help="Session ID.")
|
|
2735
|
+
p_session_plan.add_argument("--problem", default=None, help="Override the problem statement.")
|
|
2736
|
+
p_session_plan.set_defaults(func=cmd_session_plan)
|
|
2737
|
+
|
|
2738
|
+
p_session_next = session_sub.add_parser("next", help="Show the next ready subtask.")
|
|
2739
|
+
p_session_next.add_argument("session_id", help="Session ID.")
|
|
2740
|
+
p_session_next.set_defaults(func=cmd_session_next)
|
|
2741
|
+
|
|
2742
|
+
p_session_mark = session_sub.add_parser("mark", help="Update subtask status.")
|
|
2743
|
+
p_session_mark.add_argument("session_id", help="Session ID.")
|
|
2744
|
+
p_session_mark.add_argument("--subtask", required=True, dest="subtask_id", help="Subtask ID.")
|
|
2745
|
+
p_session_mark.add_argument(
|
|
2746
|
+
"--status", required=True,
|
|
2747
|
+
choices=["pending", "in_progress", "completed", "blocked", "failed"],
|
|
2748
|
+
help="New subtask status.",
|
|
2749
|
+
)
|
|
2750
|
+
p_session_mark.set_defaults(func=cmd_session_mark)
|
|
2751
|
+
|
|
2752
|
+
p_session_close = session_sub.add_parser("close", help="Close a session.")
|
|
2753
|
+
p_session_close.add_argument("session_id", help="Session ID.")
|
|
2754
|
+
p_session_close.set_defaults(func=cmd_session_close)
|
|
2755
|
+
|
|
2756
|
+
p_session_disagree = session_sub.add_parser("disagree", help="Record a disagreement between agents.")
|
|
2757
|
+
p_session_disagree.add_argument("session_id", help="Session ID.")
|
|
2758
|
+
p_session_disagree.add_argument("--concept", required=True, help="Concept under disagreement.")
|
|
2759
|
+
p_session_disagree.add_argument(
|
|
2760
|
+
"--position", action="append", dest="positions", default=[],
|
|
2761
|
+
help="Agent position AGENT=POSITION:CONFIDENCE (repeatable).",
|
|
2762
|
+
)
|
|
2763
|
+
p_session_disagree.set_defaults(func=cmd_session_disagree)
|
|
2764
|
+
|
|
2765
|
+
p_session_resolve = session_sub.add_parser("resolve", help="Resolve a disagreement.")
|
|
2766
|
+
p_session_resolve.add_argument("disagreement_id", help="Disagreement ID.")
|
|
2767
|
+
p_session_resolve.add_argument("--decision", default=None, help="Human decision to record.")
|
|
2768
|
+
p_session_resolve.set_defaults(func=cmd_session_resolve)
|
|
2769
|
+
|
|
2770
|
+
p_session_disagreements = session_sub.add_parser("disagreements", help="List disagreements.")
|
|
2771
|
+
p_session_disagreements.add_argument("--session", dest="session_id", default=None, help="Filter by session ID.")
|
|
2772
|
+
p_session_disagreements.set_defaults(func=cmd_session_disagreements)
|
|
2773
|
+
|
|
2774
|
+
# S28: simulate subcommand
|
|
2775
|
+
p_simulate = subparsers.add_parser(
|
|
2776
|
+
"simulate", help="Run a what-if simulation against a session (S28)."
|
|
2777
|
+
)
|
|
2778
|
+
p_simulate.add_argument("--session", required=True, dest="session_id", help="Session ID.")
|
|
2779
|
+
p_simulate.add_argument(
|
|
2780
|
+
"--assume", action="append", dest="assumptions", default=[],
|
|
2781
|
+
help="Alternate assumption KEY=VALUE (repeatable).",
|
|
2782
|
+
)
|
|
2783
|
+
p_simulate.add_argument("--json", action="store_true", help="Also print the raw result as JSON.")
|
|
2784
|
+
p_simulate.set_defaults(func=cmd_simulate)
|
|
2785
|
+
|
|
2786
|
+
# S29: pre-commit subcommand
|
|
2787
|
+
p_pre_commit = subparsers.add_parser(
|
|
2788
|
+
"pre-commit", help="Install and run pre-commit hooks (S29)."
|
|
2789
|
+
)
|
|
2790
|
+
pre_commit_sub = p_pre_commit.add_subparsers(dest="pre_commit_command", required=True)
|
|
2791
|
+
|
|
2792
|
+
p_pre_commit_install = pre_commit_sub.add_parser("install", help="Install the DevTorch pre-commit hook.")
|
|
2793
|
+
p_pre_commit_install.set_defaults(func=cmd_pre_commit_install)
|
|
2794
|
+
|
|
2795
|
+
p_pre_commit_run = pre_commit_sub.add_parser("run", help="Run DevTorch pre-commit checks manually.")
|
|
2796
|
+
p_pre_commit_run.set_defaults(func=cmd_pre_commit_run)
|
|
2797
|
+
|
|
2798
|
+
# S29: pr-report subcommand
|
|
2799
|
+
p_pr_report = subparsers.add_parser(
|
|
2800
|
+
"pr-report", help="Post a reasoning-delta comment on a GitHub PR and check merge gate (S29)."
|
|
2801
|
+
)
|
|
2802
|
+
p_pr_report.add_argument("--base", required=True, help="Base branch name.")
|
|
2803
|
+
p_pr_report.add_argument("--head", required=True, help="Head branch name.")
|
|
2804
|
+
p_pr_report.add_argument("--repo-owner", required=True, help="GitHub repository owner.")
|
|
2805
|
+
p_pr_report.add_argument("--repo-name", required=True, help="GitHub repository name.")
|
|
2806
|
+
p_pr_report.add_argument("--pr-number", required=True, type=int, help="Pull request number.")
|
|
2807
|
+
p_pr_report.add_argument("--token", required=True, help="GitHub API token.")
|
|
2808
|
+
p_pr_report.add_argument("--critical-concepts", default="", help="Comma-separated critical concept names.")
|
|
2809
|
+
p_pr_report.add_argument("--min-mcs", type=float, default=0.0, help="Minimum MCS score for merge gate (default: 0).")
|
|
2810
|
+
p_pr_report.set_defaults(func=cmd_pr_report)
|
|
2811
|
+
|
|
2812
|
+
# S17a: calibrate subcommand
|
|
2813
|
+
p_calibrate = subparsers.add_parser("calibrate", help="Calibrate DevTorch metrics baselines.")
|
|
2814
|
+
p_calibrate.add_argument(
|
|
2815
|
+
"--set",
|
|
2816
|
+
dest="set_value",
|
|
2817
|
+
metavar="KEY=VALUE",
|
|
2818
|
+
help="Set a single calibration value.",
|
|
2819
|
+
)
|
|
2820
|
+
p_calibrate.set_defaults(func=cmd_calibrate)
|
|
2821
|
+
|
|
2822
|
+
# S17a: metrics subcommand
|
|
2823
|
+
p_metrics = subparsers.add_parser("metrics", help="Show DevTorch metrics summary.")
|
|
2824
|
+
p_metrics.add_argument(
|
|
2825
|
+
"--period",
|
|
2826
|
+
default="30d",
|
|
2827
|
+
help="Time period (e.g. 30d, 7d). Default: 30d.",
|
|
2828
|
+
)
|
|
2829
|
+
p_metrics.add_argument(
|
|
2830
|
+
"--json",
|
|
2831
|
+
action="store_true",
|
|
2832
|
+
dest="as_json",
|
|
2833
|
+
help="Output raw JSON.",
|
|
2834
|
+
)
|
|
2835
|
+
p_metrics.add_argument(
|
|
2836
|
+
"--tokens",
|
|
2837
|
+
action="store_true",
|
|
2838
|
+
help="Show token usage/savings breakdown (S20).",
|
|
2839
|
+
)
|
|
2840
|
+
p_metrics.add_argument(
|
|
2841
|
+
"--speedup",
|
|
2842
|
+
action="store_true",
|
|
2843
|
+
help="Show latency speedup breakdown (S20).",
|
|
2844
|
+
)
|
|
2845
|
+
p_metrics.add_argument(
|
|
2846
|
+
"--all-projects",
|
|
2847
|
+
action="store_true",
|
|
2848
|
+
dest="all_projects",
|
|
2849
|
+
help="Aggregate metrics across all projects in the registry (S19).",
|
|
2850
|
+
)
|
|
2851
|
+
p_metrics.add_argument(
|
|
2852
|
+
"--delivery-time",
|
|
2853
|
+
action="store_true",
|
|
2854
|
+
dest="delivery_time",
|
|
2855
|
+
help="Show complex-problem delivery-time baseline and current estimate (S25).",
|
|
2856
|
+
)
|
|
2857
|
+
p_metrics.set_defaults(func=cmd_metrics)
|
|
2858
|
+
|
|
2859
|
+
# Sprint 19: reasoning subcommand
|
|
2860
|
+
reasoning_parser = subparsers.add_parser("reasoning", help="Query agent reasoning")
|
|
2861
|
+
reasoning_sub = reasoning_parser.add_subparsers(dest="reasoning_cmd")
|
|
2862
|
+
rshow = reasoning_sub.add_parser("show", help="Show agent reasoning for a concept")
|
|
2863
|
+
rshow.add_argument("--concept", required=True)
|
|
2864
|
+
rshow.add_argument("--agent", dest="agent_id", default=None)
|
|
2865
|
+
rshow.add_argument("--tokens", type=int, default=4000)
|
|
2866
|
+
rshow.set_defaults(func=cmd_reasoning_show)
|
|
2867
|
+
|
|
2868
|
+
# Phase 5B: reasoning history subcommand
|
|
2869
|
+
rhistory = reasoning_sub.add_parser("history", help="Show reasoning history for a topic or concept.")
|
|
2870
|
+
rhistory.add_argument("--topic", default=None, help="Topic name (expands to multiple concepts).")
|
|
2871
|
+
rhistory.add_argument("--concept", default=None, help="Single concept name (alternative to topic).")
|
|
2872
|
+
rhistory.add_argument("--max-entries", type=int, default=50, dest="max_entries", help="Max total entries (default: 50).")
|
|
2873
|
+
rhistory.add_argument("--agent", dest="agent_id", default=None, help="Filter by agent ID.")
|
|
2874
|
+
rhistory.add_argument("--tokens", type=int, default=4000, help="Per-concept token budget (default: 4000).")
|
|
2875
|
+
rhistory.set_defaults(func=cmd_reasoning_history)
|
|
2876
|
+
|
|
2877
|
+
# Phase 5B: topics subcommand
|
|
2878
|
+
topics_parser = subparsers.add_parser("topics", help="Manage and query topic taxonomy.")
|
|
2879
|
+
topics_sub = topics_parser.add_subparsers(dest="topics_command", required=True)
|
|
2880
|
+
|
|
2881
|
+
p_topics_list = topics_sub.add_parser("list", help="List topics with captured reasoning.")
|
|
2882
|
+
p_topics_list.add_argument("--all", action="store_true", help="Show full taxonomy including topics with no data.")
|
|
2883
|
+
p_topics_list.set_defaults(func=cmd_topics_list)
|
|
2884
|
+
|
|
2885
|
+
p_topics_show = topics_sub.add_parser("show", help="Show details for a specific topic.")
|
|
2886
|
+
p_topics_show.add_argument("name", help="Topic name.")
|
|
2887
|
+
p_topics_show.set_defaults(func=cmd_topics_show)
|
|
2888
|
+
|
|
2889
|
+
p_topics_add = topics_sub.add_parser("add", help="Add a custom topic.")
|
|
2890
|
+
p_topics_add.add_argument("name", help="Topic name.")
|
|
2891
|
+
p_topics_add.add_argument("--concepts", required=True, help="Comma-separated concept names.")
|
|
2892
|
+
p_topics_add.set_defaults(func=cmd_topics_add)
|
|
2893
|
+
|
|
2894
|
+
p_topics_remove = topics_sub.add_parser("remove", help="Remove a custom topic.")
|
|
2895
|
+
p_topics_remove.add_argument("name", help="Topic name.")
|
|
2896
|
+
p_topics_remove.set_defaults(func=cmd_topics_remove)
|
|
2897
|
+
|
|
2898
|
+
p_topics_derive = topics_sub.add_parser("derive", help="Auto-derive topics from concept co-occurrence.")
|
|
2899
|
+
p_topics_derive.add_argument("--force", action="store_true", help="Force re-derivation even if cached.")
|
|
2900
|
+
p_topics_derive.set_defaults(func=cmd_topics_derive)
|
|
2901
|
+
|
|
2902
|
+
# S27: templates subcommand (reasoning template library)
|
|
2903
|
+
p_templates = subparsers.add_parser(
|
|
2904
|
+
"templates",
|
|
2905
|
+
help="Reasoning template library and custom template extension (S27).",
|
|
2906
|
+
)
|
|
2907
|
+
templates_sub = p_templates.add_subparsers(dest="templates_command", required=True)
|
|
2908
|
+
|
|
2909
|
+
p_templates_list = templates_sub.add_parser("list", help="List built-in and custom templates.")
|
|
2910
|
+
p_templates_list.set_defaults(func=cmd_templates_list)
|
|
2911
|
+
|
|
2912
|
+
p_templates_apply = templates_sub.add_parser("apply", help="Render a template with variables.")
|
|
2913
|
+
p_templates_apply.add_argument("name", help="Template name.")
|
|
2914
|
+
p_templates_apply.add_argument(
|
|
2915
|
+
"--var",
|
|
2916
|
+
dest="variables",
|
|
2917
|
+
action="append",
|
|
2918
|
+
default=[],
|
|
2919
|
+
help="Template variable KEY=VALUE (repeatable).",
|
|
2920
|
+
)
|
|
2921
|
+
p_templates_apply.add_argument(
|
|
2922
|
+
"--output",
|
|
2923
|
+
default=None,
|
|
2924
|
+
help="Output path for the rendered template (default: .GCC/templates/rendered/<name>.md).",
|
|
2925
|
+
)
|
|
2926
|
+
p_templates_apply.set_defaults(func=cmd_templates_apply)
|
|
2927
|
+
|
|
2928
|
+
p_templates_init = templates_sub.add_parser("init", help="Create the custom template extension directory.")
|
|
2929
|
+
p_templates_init.set_defaults(func=cmd_templates_init)
|
|
2930
|
+
|
|
2931
|
+
# S24: reasoning-plus subcommand
|
|
2932
|
+
rp_parser = subparsers.add_parser("reasoning-plus", help="Manage Reasoning Plus settings")
|
|
2933
|
+
rp_sub = rp_parser.add_subparsers(dest="reasoning_plus_cmd")
|
|
2934
|
+
rp_enable = rp_sub.add_parser("enable", help="Enable Reasoning Plus for this project")
|
|
2935
|
+
rp_enable.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2936
|
+
rp_enable.set_defaults(func=cmd_reasoning_plus_enable)
|
|
2937
|
+
rp_disable = rp_sub.add_parser("disable", help="Disable Reasoning Plus for this project")
|
|
2938
|
+
rp_disable.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2939
|
+
rp_disable.set_defaults(func=cmd_reasoning_plus_disable)
|
|
2940
|
+
rp_status = rp_sub.add_parser("status", help="Show current Reasoning Plus settings and source")
|
|
2941
|
+
rp_status.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2942
|
+
rp_status.set_defaults(func=cmd_reasoning_plus_status)
|
|
2943
|
+
rp_config = rp_sub.add_parser("config", help="Update Reasoning Plus settings")
|
|
2944
|
+
rp_config.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2945
|
+
rp_config.add_argument("--smart-top-n", type=int, default=None)
|
|
2946
|
+
rp_config.add_argument("--smart-max-lines", type=int, default=None)
|
|
2947
|
+
rp_config.add_argument("--context-k", type=int, default=None)
|
|
2948
|
+
rp_config.add_argument("--require-thinking", type=lambda x: x.lower() in ("1", "true", "yes", "on"), default=None)
|
|
2949
|
+
rp_config.add_argument("--max-keywords", type=int, default=None)
|
|
2950
|
+
rp_config.add_argument("--cache-ttl", type=int, default=None)
|
|
2951
|
+
rp_config.add_argument("--learning-enabled", type=lambda x: x.lower() in ("1", "true", "yes", "on"), default=None)
|
|
2952
|
+
rp_config.add_argument("--learning-top-n", type=int, default=None)
|
|
2953
|
+
rp_config.add_argument("--learning-max-lines", type=int, default=None)
|
|
2954
|
+
rp_config.add_argument("--learning-embedding-backend", type=str, default=None)
|
|
2955
|
+
rp_config.add_argument("--learning-embedding-model", type=str, default=None)
|
|
2956
|
+
rp_config.add_argument("--learning-extraction-strategy", type=str, default=None)
|
|
2957
|
+
rp_config.add_argument("--learning-relevance-cache-ttl", type=int, default=None)
|
|
2958
|
+
rp_config.set_defaults(func=cmd_reasoning_plus_config)
|
|
2959
|
+
rp_reset = rp_sub.add_parser("reset", help="Reset project Reasoning Plus settings to default")
|
|
2960
|
+
rp_reset.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2961
|
+
rp_reset.set_defaults(func=cmd_reasoning_plus_reset)
|
|
2962
|
+
|
|
2963
|
+
rp_suggest = rp_sub.add_parser(
|
|
2964
|
+
"cross-project-suggest",
|
|
2965
|
+
help="Suggest relevant learnings from sibling governed projects.",
|
|
2966
|
+
)
|
|
2967
|
+
rp_suggest.add_argument("--path", default=".", help="Path to project root (default: .)")
|
|
2968
|
+
rp_suggest.add_argument("query", help="Query to match against project name/tags and learnings.")
|
|
2969
|
+
rp_suggest.add_argument("--top-n", type=int, default=3, help="Maximum number of learnings to return (default: 3).")
|
|
2970
|
+
rp_suggest.set_defaults(func=cmd_reasoning_plus_cross_project_suggest)
|
|
2971
|
+
|
|
2972
|
+
# Phase 3: learning analytics CLI
|
|
2973
|
+
learning_parser = subparsers.add_parser("learning", help="Per-concept learning statistics and analytics (Phase 3).")
|
|
2974
|
+
learning_sub = learning_parser.add_subparsers(dest="learning_command", required=True)
|
|
2975
|
+
p_learning_stats = learning_sub.add_parser("stats", help="Show learning statistics.")
|
|
2976
|
+
p_learning_stats.add_argument("--by-concept", action="store_true", help="Group statistics by concept.")
|
|
2977
|
+
p_learning_stats.set_defaults(func=cmd_learning_stats)
|
|
2978
|
+
|
|
2979
|
+
sync_parser = subparsers.add_parser("sync", help="Sync REP ledger or team Θ with peers.")
|
|
2980
|
+
sync_parser.add_argument("--interval", type=int, default=0,
|
|
2981
|
+
help="If >0, repeat sync every N seconds (runs until interrupted).")
|
|
2982
|
+
sync_parser.add_argument("--gcc-dir", default=".GCC",
|
|
2983
|
+
help="Path to .GCC directory (default: .GCC in cwd).")
|
|
2984
|
+
sync_sub = sync_parser.add_subparsers(dest="sync_command", required=False)
|
|
2985
|
+
|
|
2986
|
+
# Existing REP sync (default when no subcommand is given)
|
|
2987
|
+
rep_sync_parser = sync_sub.add_parser("rep", help="Sync REP ledger with registered peers.")
|
|
2988
|
+
rep_sync_parser.set_defaults(func=cmd_sync)
|
|
2989
|
+
|
|
2990
|
+
# Team Θ synchronization (S31)
|
|
2991
|
+
theta_sync_parser = sync_sub.add_parser("theta", help="Synchronize team Θ (coordination vector) via cloud storage.")
|
|
2992
|
+
theta_sync_sub = theta_sync_parser.add_subparsers(dest="theta_sync_command", required=True)
|
|
2993
|
+
theta_push = theta_sync_sub.add_parser("push", help="Push local .GCC/theta.json to the org team Θ store.")
|
|
2994
|
+
theta_push.add_argument("--org", required=True, help="DevTorch org ID")
|
|
2995
|
+
theta_push.add_argument("--gcc-dir", default=".GCC", help="Path to .GCC directory (default: .GCC in cwd)")
|
|
2996
|
+
theta_push.add_argument(
|
|
2997
|
+
"--from-learnings", action="store_true",
|
|
2998
|
+
help="Sync learning success rates into Θ before pushing (Phase 4).",
|
|
2999
|
+
)
|
|
3000
|
+
theta_push.set_defaults(func=cmd_sync_theta_push)
|
|
3001
|
+
theta_pull = theta_sync_sub.add_parser("pull", help="Pull the org team Θ store and merge it into local .GCC/theta.json.")
|
|
3002
|
+
theta_pull.add_argument("--org", required=True, help="DevTorch org ID")
|
|
3003
|
+
theta_pull.add_argument("--gcc-dir", default=".GCC", help="Path to .GCC directory (default: .GCC in cwd)")
|
|
3004
|
+
theta_pull.set_defaults(func=cmd_sync_theta_pull)
|
|
3005
|
+
|
|
3006
|
+
# Phase 4: cross-project learning sync
|
|
3007
|
+
learning_sync_parser = sync_sub.add_parser("learning", help="Synchronize learnings across team projects via cloud storage.")
|
|
3008
|
+
learning_sync_sub = learning_sync_parser.add_subparsers(dest="learning_sync_command", required=True)
|
|
3009
|
+
lp_push = learning_sync_sub.add_parser("push", help="Push local learnings to the org team learning store.")
|
|
3010
|
+
lp_push.add_argument("--org", required=True, help="DevTorch org ID")
|
|
3011
|
+
lp_push.add_argument("--gcc-dir", default=".GCC", help="Path to .GCC directory (default: .GCC in cwd)")
|
|
3012
|
+
lp_push.set_defaults(func=cmd_sync_learning_push)
|
|
3013
|
+
lp_pull = learning_sync_sub.add_parser("pull", help="Pull and merge team learnings into the local store.")
|
|
3014
|
+
lp_pull.add_argument("--org", required=True, help="DevTorch org ID")
|
|
3015
|
+
lp_pull.add_argument("--gcc-dir", default=".GCC", help="Path to .GCC directory (default: .GCC in cwd)")
|
|
3016
|
+
lp_pull.set_defaults(func=cmd_sync_learning_pull)
|
|
3017
|
+
|
|
3018
|
+
# Bidirectional sync (PR #32)
|
|
3019
|
+
sync_all_parser = sync_sub.add_parser("all", help="Bidirectional sync with MCP server (git-inspired, incremental + conflict resolution).")
|
|
3020
|
+
sync_all_parser.add_argument("--interval", type=int, default=0, help="Repeat every N minutes (default: 0 = one-shot).")
|
|
3021
|
+
sync_all_parser.add_argument("--dry-run", action="store_true", help="Show what would sync without making changes.")
|
|
3022
|
+
sync_all_parser.add_argument("--verbose", action="store_true", help="Show per-item details.")
|
|
3023
|
+
sync_all_parser.add_argument("--chunk-size", type=int, default=500, help="Max items per batch (default: 500).")
|
|
3024
|
+
sync_all_parser.add_argument("--force-full", action="store_true", help="Treat sync state as empty, trigger full re-sync.")
|
|
3025
|
+
sync_all_parser.set_defaults(func=cmd_sync_all)
|
|
3026
|
+
|
|
3027
|
+
sync_conflicts_parser = sync_sub.add_parser("conflicts", help="List and resolve sync conflicts.")
|
|
3028
|
+
sync_conflicts_parser.add_argument("--all", action="store_true", dest="show_all", help="Include resolved conflicts.")
|
|
3029
|
+
sync_conflicts_parser.add_argument("--severity", default=None, choices=["high", "medium", "low"], help="Filter by severity.")
|
|
3030
|
+
sync_conflicts_parser.set_defaults(func=cmd_sync_conflicts)
|
|
3031
|
+
|
|
3032
|
+
sync_conflicts_resolve_parser = sync_sub.add_parser("conflicts-resolve", help="Resolve a specific sync conflict.")
|
|
3033
|
+
sync_conflicts_resolve_parser.add_argument("conflict_id", help="Conflict ID to resolve.")
|
|
3034
|
+
sync_conflicts_resolve_parser.add_argument("--use-remote", action="store_true", help="Use the remote value instead of local.")
|
|
3035
|
+
sync_conflicts_resolve_parser.add_argument("--dismiss", action="store_true", help="Dismiss without changing anything.")
|
|
3036
|
+
sync_conflicts_resolve_parser.add_argument("--dry-run", action="store_true", help="Show what would happen without applying.")
|
|
3037
|
+
sync_conflicts_resolve_parser.set_defaults(func=cmd_sync_conflicts_resolve)
|
|
3038
|
+
|
|
3039
|
+
sync_parser.set_defaults(func=cmd_sync, sync_command=None)
|
|
3040
|
+
|
|
3041
|
+
watch_parser = subparsers.add_parser("watch", help="Tail coordination events from .GCC/ in real time.")
|
|
3042
|
+
watch_parser.add_argument("--gcc-dir", default=".GCC",
|
|
3043
|
+
help="Path to .GCC directory (default: .GCC in cwd).")
|
|
3044
|
+
watch_parser.set_defaults(func=cmd_watch)
|
|
3045
|
+
|
|
3046
|
+
# S11: mcp-server subcommand
|
|
3047
|
+
mcp_parser = subparsers.add_parser("mcp-server", help="Start the DevTorch MCP server")
|
|
3048
|
+
mcp_parser.add_argument(
|
|
3049
|
+
"--transport", choices=["stdio", "sse"], default="stdio",
|
|
3050
|
+
help="Transport: stdio (default, local) or sse (cloud HTTPS)",
|
|
3051
|
+
)
|
|
3052
|
+
mcp_parser.add_argument("--host", default="0.0.0.0", help="SSE bind host (default: 0.0.0.0)")
|
|
3053
|
+
mcp_parser.add_argument("--port", type=int, default=8080, help="SSE bind port (default: 8080)")
|
|
3054
|
+
|
|
3055
|
+
# Cloud key management
|
|
3056
|
+
cloud_parser = subparsers.add_parser("cloud", help="Cloud MCP server management")
|
|
3057
|
+
cloud_sub = cloud_parser.add_subparsers(dest="cloud_command", required=True)
|
|
3058
|
+
key_parser = cloud_sub.add_parser("key", help="API key management")
|
|
3059
|
+
key_sub = key_parser.add_subparsers(dest="key_command", required=True)
|
|
3060
|
+
key_create = key_sub.add_parser("create", help="Create a new API key")
|
|
3061
|
+
key_create.add_argument("--org", required=True, help="Org ID")
|
|
3062
|
+
key_create.add_argument("--repo", help="Repo ID (optional, adds to allowed_repos)")
|
|
3063
|
+
key_create.add_argument(
|
|
3064
|
+
"--github", metavar="OWNER/REPO",
|
|
3065
|
+
help="Also set DEVTORCH_API_KEY as a GitHub Repository Secret",
|
|
3066
|
+
)
|
|
3067
|
+
|
|
3068
|
+
key_list = key_sub.add_parser("list", help="List API keys for an org")
|
|
3069
|
+
key_list.add_argument("--org", required=True, help="Org ID")
|
|
3070
|
+
|
|
3071
|
+
key_revoke = key_sub.add_parser("revoke", help="Revoke an API key by hash prefix")
|
|
3072
|
+
key_revoke.add_argument("--org", required=True, help="Org ID")
|
|
3073
|
+
key_revoke.add_argument("key_prefix", help="Hex prefix of the key hash to revoke (min 8 chars)")
|
|
3074
|
+
key_revoke.add_argument("--yes", "-y", action="store_true",
|
|
3075
|
+
help="Skip confirmation prompt (required for non-interactive use)")
|
|
3076
|
+
|
|
3077
|
+
key_rotate = key_sub.add_parser("rotate", help="Rotate an API key by prefix (revoke old, create new)")
|
|
3078
|
+
key_rotate.add_argument("--org", required=True, help="Org ID")
|
|
3079
|
+
key_rotate.add_argument("key_prefix", help="Hex prefix of the key hash to rotate (min 8 chars)")
|
|
3080
|
+
key_rotate.add_argument("--yes", "-y", action="store_true",
|
|
3081
|
+
help="Skip confirmation prompt (required for non-interactive use)")
|
|
3082
|
+
|
|
3083
|
+
repo_parser = cloud_sub.add_parser("repo", help="GitHub repository connection management")
|
|
3084
|
+
repo_sub = repo_parser.add_subparsers(dest="repo_command", required=True)
|
|
3085
|
+
|
|
3086
|
+
repo_connect = repo_sub.add_parser("connect", help="Connect a GitHub repository")
|
|
3087
|
+
repo_connect.add_argument("--org", required=True, help="Org ID")
|
|
3088
|
+
repo_connect.add_argument("--repo", required=True, help="DevTorch repo ID (e.g. 'myproject')")
|
|
3089
|
+
repo_connect.add_argument("--github", required=True, metavar="OWNER/REPO",
|
|
3090
|
+
help="GitHub repository (e.g. 'acme/backend')")
|
|
3091
|
+
repo_connect_auth = repo_connect.add_mutually_exclusive_group(required=True)
|
|
3092
|
+
repo_connect_auth.add_argument("--pat", metavar="TOKEN",
|
|
3093
|
+
help="GitHub Personal Access Token")
|
|
3094
|
+
repo_connect_auth.add_argument("--github-app", action="store_true",
|
|
3095
|
+
help="Use GitHub App (server handles via webhook)")
|
|
3096
|
+
|
|
3097
|
+
repo_list = repo_sub.add_parser("list", help="List connected GitHub repositories")
|
|
3098
|
+
repo_list.add_argument("--org", required=True, help="Org ID")
|
|
3099
|
+
|
|
3100
|
+
repo_disconnect = repo_sub.add_parser("disconnect", help="Remove a connected GitHub repository")
|
|
3101
|
+
repo_disconnect.add_argument("--org", required=True, help="Org ID")
|
|
3102
|
+
repo_disconnect.add_argument("--repo", required=True, help="DevTorch repo ID")
|
|
3103
|
+
|
|
3104
|
+
# Cloud client configuration installer
|
|
3105
|
+
setup_parser = cloud_sub.add_parser("setup", help="Deploy the self-hosted Cloudflare Worker MCP server (one-command setup).")
|
|
3106
|
+
setup_parser.add_argument("--env-file", help="Environment file to source")
|
|
3107
|
+
setup_parser.add_argument("--org", help="DevTorch org ID")
|
|
3108
|
+
setup_parser.add_argument("--repo", help="DevTorch repo ID")
|
|
3109
|
+
setup_parser.add_argument("--ide", choices=["opencode", "claude", "cursor", "vscode", "stdio"],
|
|
3110
|
+
help="IDE to configure locally")
|
|
3111
|
+
setup_parser.add_argument("--openai-key", help="OpenAI API key for DRPL embedding backend")
|
|
3112
|
+
setup_parser.add_argument("--non-interactive", action="store_true",
|
|
3113
|
+
help="Run without prompts (requires --org and --repo)")
|
|
3114
|
+
setup_parser.add_argument("--skip-key", action="store_true", help="Skip creating API key")
|
|
3115
|
+
setup_parser.add_argument("--skip-client-config", action="store_true", help="Skip IDE config")
|
|
3116
|
+
setup_parser.add_argument("--dry-run", action="store_true", help="Print command instead of running")
|
|
3117
|
+
setup_parser.set_defaults(func=cmd_cloud_setup)
|
|
3118
|
+
|
|
3119
|
+
client_config_parser = cloud_sub.add_parser(
|
|
3120
|
+
"client-config", help="Install MCP client config for an IDE (opencode, claude, cursor, vscode, stdio)."
|
|
3121
|
+
)
|
|
3122
|
+
client_config_parser.add_argument("ide", choices=["opencode", "claude", "cursor", "vscode", "stdio"], help="IDE target")
|
|
3123
|
+
client_config_parser.add_argument("--org", required=True, help="DevTorch org ID")
|
|
3124
|
+
client_config_parser.add_argument("--repo", required=True, help="DevTorch repo ID")
|
|
3125
|
+
client_config_parser.add_argument("--api-key", required=True, help="API key from 'devtorch cloud key create'")
|
|
3126
|
+
client_config_parser.add_argument(
|
|
3127
|
+
"--domain", default=None, help="Cloudflare Worker domain (default: DEVTORCH_MCP_DOMAIN env var)"
|
|
3128
|
+
)
|
|
3129
|
+
client_config_parser.add_argument(
|
|
3130
|
+
"--path", default=".", help="Project root to install config into (default: current directory)"
|
|
3131
|
+
)
|
|
3132
|
+
client_config_parser.add_argument(
|
|
3133
|
+
"--use-env-key", action="store_true",
|
|
3134
|
+
help="claude/stdio targets: write ${DEVTORCH_API_KEY} into .mcp.json instead of "
|
|
3135
|
+
"the literal key (Claude Code expands env vars; export DEVTORCH_API_KEY yourself)",
|
|
3136
|
+
)
|
|
3137
|
+
client_config_parser.set_defaults(func=cmd_cloud_client_config)
|
|
3138
|
+
|
|
3139
|
+
# Cloud reasoning sync (Task 6)
|
|
3140
|
+
sync_parser = cloud_sub.add_parser(
|
|
3141
|
+
"sync", help="Push local .GCC commits to the cloud MCP store (one-way)."
|
|
3142
|
+
)
|
|
3143
|
+
sync_parser.add_argument(
|
|
3144
|
+
"--from-now", action="store_true", dest="from_now",
|
|
3145
|
+
help="Mark all existing local commits as synced without pushing them; "
|
|
3146
|
+
"only commits made after this point will sync (go-forward adoption).",
|
|
3147
|
+
)
|
|
3148
|
+
sync_parser.add_argument("--path", default=".", help="Project root (default: current directory)")
|
|
3149
|
+
|
|
3150
|
+
# S11: hooks subcommand
|
|
3151
|
+
p_hooks = subparsers.add_parser("hooks", help="Manage DevTorch hooks for Claude Code and git (S11).")
|
|
3152
|
+
hooks_sub = p_hooks.add_subparsers(dest="hooks_command", required=True)
|
|
3153
|
+
|
|
3154
|
+
p_hooks_install = hooks_sub.add_parser("install", help="Install .claude/hooks.json and git commit-msg hook.")
|
|
3155
|
+
p_hooks_install.set_defaults(hooks_command="install")
|
|
3156
|
+
|
|
3157
|
+
p_hooks_status = hooks_sub.add_parser("status", help="Show which DevTorch hooks are installed.")
|
|
3158
|
+
p_hooks_status.set_defaults(hooks_command="status")
|
|
3159
|
+
|
|
3160
|
+
p_hooks_git_commit = hooks_sub.add_parser("git-commit", help="Git commit-msg hook entry point.")
|
|
3161
|
+
p_hooks_git_commit.add_argument("commit_msg_file", help="Path to the git commit message file.")
|
|
3162
|
+
p_hooks_git_commit.set_defaults(hooks_command="git-commit")
|
|
3163
|
+
|
|
3164
|
+
p_hooks_git_capture = hooks_sub.add_parser(
|
|
3165
|
+
"git-capture",
|
|
3166
|
+
help="Git capture hook entry point for post-commit, post-merge, post-checkout, post-rewrite.",
|
|
3167
|
+
)
|
|
3168
|
+
p_hooks_git_capture.add_argument(
|
|
3169
|
+
"event",
|
|
3170
|
+
choices=["post-commit", "post-merge", "post-checkout", "post-rewrite", "conflict"],
|
|
3171
|
+
help="Git hook event to capture.",
|
|
3172
|
+
)
|
|
3173
|
+
p_hooks_git_capture.add_argument(
|
|
3174
|
+
"args",
|
|
3175
|
+
nargs="*",
|
|
3176
|
+
help="Extra positional arguments passed by the git hook (e.g. squash flag, prev/new HEAD).",
|
|
3177
|
+
)
|
|
3178
|
+
p_hooks_git_capture.set_defaults(hooks_command="git-capture")
|
|
3179
|
+
|
|
3180
|
+
p_hooks_run = hooks_sub.add_parser("run", help="Run a Claude Code hook (reads JSON from stdin).")
|
|
3181
|
+
p_hooks_run.add_argument("hook_name", choices=["pre-tool-use", "post-tool-use", "session-end"])
|
|
3182
|
+
p_hooks_run.set_defaults(hooks_command="run")
|
|
3183
|
+
|
|
3184
|
+
# S16: alert subcommand
|
|
3185
|
+
p_alert = subparsers.add_parser("alert", help="Send a test governance alert to configured channels (Slack, Teams, Jira, Linear, PagerDuty).")
|
|
3186
|
+
p_alert.add_argument("--title", default="DevTorch test alert", help="Alert title.")
|
|
3187
|
+
p_alert.add_argument("--body", default="This is a test alert from the DevTorch CLI.", help="Alert body.")
|
|
3188
|
+
p_alert.add_argument("--severity", choices=["info", "warning", "critical"], default="warning", help="Alert severity.")
|
|
3189
|
+
p_alert.add_argument("--event-type", default="TEST_ALERT", help="Event type label.")
|
|
3190
|
+
p_alert.add_argument("--channel", choices=["slack", "teams", "jira", "linear", "pagerduty"], help="Send only to this channel (requires env vars for that channel).")
|
|
3191
|
+
p_alert.set_defaults(func=cmd_alert)
|
|
3192
|
+
|
|
3193
|
+
return parser
|
|
3194
|
+
|
|
3195
|
+
|
|
3196
|
+
def _set_github_secret(owner_repo: str, secret_name: str, secret_value: str) -> None:
|
|
3197
|
+
import os
|
|
3198
|
+
import urllib.request
|
|
3199
|
+
import json as _json
|
|
3200
|
+
|
|
3201
|
+
token = os.environ.get("GITHUB_TOKEN", "")
|
|
3202
|
+
if not token:
|
|
3203
|
+
print("Warning: GITHUB_TOKEN not set; skipping GitHub secret creation.")
|
|
3204
|
+
return
|
|
3205
|
+
owner, repo = owner_repo.split("/", 1)
|
|
3206
|
+
|
|
3207
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/actions/secrets/public-key"
|
|
3208
|
+
req = urllib.request.Request(
|
|
3209
|
+
url,
|
|
3210
|
+
headers={
|
|
3211
|
+
"Authorization": f"token {token}",
|
|
3212
|
+
"Accept": "application/vnd.github+json",
|
|
3213
|
+
},
|
|
3214
|
+
)
|
|
3215
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
3216
|
+
pub_key_data = _json.loads(resp.read())
|
|
3217
|
+
|
|
3218
|
+
try:
|
|
3219
|
+
import nacl.public
|
|
3220
|
+
from base64 import b64decode, b64encode
|
|
3221
|
+
pub_key = nacl.public.PublicKey(b64decode(pub_key_data["key"]))
|
|
3222
|
+
sealed = nacl.public.SealedBox(pub_key).encrypt(secret_value.encode())
|
|
3223
|
+
encrypted = b64encode(sealed).decode()
|
|
3224
|
+
except ImportError:
|
|
3225
|
+
print("Warning: PyNaCl not installed; cannot encrypt GitHub secret. Install with: pip install PyNaCl")
|
|
3226
|
+
return
|
|
3227
|
+
|
|
3228
|
+
payload = _json.dumps({
|
|
3229
|
+
"encrypted_value": encrypted,
|
|
3230
|
+
"key_id": pub_key_data["key_id"],
|
|
3231
|
+
}).encode()
|
|
3232
|
+
put_url = f"https://api.github.com/repos/{owner}/{repo}/actions/secrets/{secret_name}"
|
|
3233
|
+
put_req = urllib.request.Request(
|
|
3234
|
+
put_url, data=payload, method="PUT",
|
|
3235
|
+
headers={
|
|
3236
|
+
"Authorization": f"token {token}",
|
|
3237
|
+
"Accept": "application/vnd.github+json",
|
|
3238
|
+
"Content-Type": "application/json",
|
|
3239
|
+
},
|
|
3240
|
+
)
|
|
3241
|
+
with urllib.request.urlopen(put_req, timeout=10):
|
|
3242
|
+
pass
|
|
3243
|
+
print(f"GitHub secret {secret_name} set on {owner_repo}.")
|
|
3244
|
+
|
|
3245
|
+
|
|
3246
|
+
def cmd_sync(args: argparse.Namespace) -> int:
|
|
3247
|
+
import time
|
|
3248
|
+
from pathlib import Path
|
|
3249
|
+
from devtorch_core.rep import REPLedger
|
|
3250
|
+
from devtorch_core.rep_network.node import NodeRegistry
|
|
3251
|
+
from devtorch_core.rep_network.sync import sync_all_peers
|
|
3252
|
+
|
|
3253
|
+
if getattr(args, "sync_command", None) == "theta":
|
|
3254
|
+
theta_cmd = getattr(args, "theta_sync_command", None)
|
|
3255
|
+
if theta_cmd == "push":
|
|
3256
|
+
return cmd_sync_theta_push(args)
|
|
3257
|
+
if theta_cmd == "pull":
|
|
3258
|
+
return cmd_sync_theta_pull(args)
|
|
3259
|
+
print("error: unknown theta sync command", file=sys.stderr)
|
|
3260
|
+
return 1
|
|
3261
|
+
|
|
3262
|
+
gcc_dir = Path(args.gcc_dir)
|
|
3263
|
+
ledger = REPLedger(gcc_dir / "rep")
|
|
3264
|
+
registry = NodeRegistry(gcc_dir / "rep")
|
|
3265
|
+
|
|
3266
|
+
def _do_sync():
|
|
3267
|
+
results = sync_all_peers(ledger, registry)
|
|
3268
|
+
if not results:
|
|
3269
|
+
print("No peers configured.")
|
|
3270
|
+
return
|
|
3271
|
+
for r in results:
|
|
3272
|
+
status = "✓" if r.success else "✗"
|
|
3273
|
+
print(f" {status} {r.peer_node_id} ({r.peer_url}): "
|
|
3274
|
+
f"pushed={r.pushed_count} pulled={r.pulled_count}"
|
|
3275
|
+
+ (f" errors={r.errors}" if r.errors else ""))
|
|
3276
|
+
|
|
3277
|
+
if args.interval > 0:
|
|
3278
|
+
print(f"Syncing every {args.interval}s — Ctrl-C to stop.")
|
|
3279
|
+
try:
|
|
3280
|
+
while True:
|
|
3281
|
+
_do_sync()
|
|
3282
|
+
time.sleep(args.interval)
|
|
3283
|
+
except KeyboardInterrupt:
|
|
3284
|
+
print("\nSync stopped.")
|
|
3285
|
+
else:
|
|
3286
|
+
_do_sync()
|
|
3287
|
+
return 0
|
|
3288
|
+
|
|
3289
|
+
|
|
3290
|
+
def cmd_sync_theta_push(args: argparse.Namespace) -> int:
|
|
3291
|
+
from pathlib import Path
|
|
3292
|
+
from devtorch_core.cloud.team_sync import push_theta
|
|
3293
|
+
|
|
3294
|
+
gcc_dir = Path(args.gcc_dir).resolve()
|
|
3295
|
+
|
|
3296
|
+
if getattr(args, "from_learnings", False):
|
|
3297
|
+
from devtorch_core.reasoning_plus.learning.theta_learning_bridge import (
|
|
3298
|
+
sync_learnings_to_theta,
|
|
3299
|
+
)
|
|
3300
|
+
bridge_result = sync_learnings_to_theta(gcc_dir, min_samples=3)
|
|
3301
|
+
if bridge_result["concepts_synced"]:
|
|
3302
|
+
print(f"Synced {bridge_result['concepts_synced']} concept(s) from learnings into Θ.")
|
|
3303
|
+
else:
|
|
3304
|
+
print("No concepts with sufficient learnings to sync into Θ.")
|
|
3305
|
+
|
|
3306
|
+
result = push_theta(args.org, gcc_dir)
|
|
3307
|
+
print(f"Pushed {result['concepts_pushed']} concept(s) to team Θ for {result['org_id']}.")
|
|
3308
|
+
return 0
|
|
3309
|
+
|
|
3310
|
+
|
|
3311
|
+
def cmd_sync_learning_push(args: argparse.Namespace) -> int:
|
|
3312
|
+
from pathlib import Path
|
|
3313
|
+
from devtorch_core.cloud.team_sync import push_learnings
|
|
3314
|
+
|
|
3315
|
+
result = push_learnings(args.org, Path(args.gcc_dir).resolve())
|
|
3316
|
+
print(
|
|
3317
|
+
f"Pushed {result['learnings_pushed']} learning(s) to team store for "
|
|
3318
|
+
f"{result['org_id']} (total remote: {result['learnings_total_remote']})."
|
|
3319
|
+
)
|
|
3320
|
+
return 0
|
|
3321
|
+
|
|
3322
|
+
|
|
3323
|
+
def cmd_sync_learning_pull(args: argparse.Namespace) -> int:
|
|
3324
|
+
from pathlib import Path
|
|
3325
|
+
from devtorch_core.cloud.team_sync import pull_learnings
|
|
3326
|
+
|
|
3327
|
+
result = pull_learnings(args.org, Path(args.gcc_dir).resolve())
|
|
3328
|
+
print(
|
|
3329
|
+
f"Pulled {result['learnings_added']} new learning(s) from team store for "
|
|
3330
|
+
f"{result['org_id']} (total local: {result['learnings_total_local']})."
|
|
3331
|
+
)
|
|
3332
|
+
return 0
|
|
3333
|
+
|
|
3334
|
+
|
|
3335
|
+
def cmd_sync_theta_pull(args: argparse.Namespace) -> int:
|
|
3336
|
+
from pathlib import Path
|
|
3337
|
+
from devtorch_core.cloud.team_sync import pull_theta
|
|
3338
|
+
|
|
3339
|
+
result = pull_theta(args.org, Path(args.gcc_dir).resolve())
|
|
3340
|
+
print(f"Pulled team Θ for {result['org_id']}: "
|
|
3341
|
+
f"{result['concepts_after']} concept(s) total "
|
|
3342
|
+
f"(+{result['concepts_added']} from remote).")
|
|
3343
|
+
return 0
|
|
3344
|
+
|
|
3345
|
+
|
|
3346
|
+
def cmd_sync_all(args: argparse.Namespace) -> int:
|
|
3347
|
+
"""Bidirectional sync with MCP server (sync_pull → merge → sync_push)."""
|
|
3348
|
+
import time as _time
|
|
3349
|
+
from devtorch_core.cloud.mcp_client import detect_mcp_config, McpClient
|
|
3350
|
+
from devtorch_core.cloud.sync_state import SyncState
|
|
3351
|
+
from devtorch_core.cloud.sync_bundle import build_local_delta, build_server_state, compress, decompress, merge_pull
|
|
3352
|
+
from devtorch_core.cloud.sync_conflicts import ConflictLog
|
|
3353
|
+
from devtorch_core.gcc import GCCRepository
|
|
3354
|
+
|
|
3355
|
+
root = Path(args.path).resolve()
|
|
3356
|
+
config = detect_mcp_config(root)
|
|
3357
|
+
if not config:
|
|
3358
|
+
print("No MCP server configured. Set DEVTORCH_MCP_URL and DEVTORCH_API_KEY or configure .mcp.json.", file=sys.stderr)
|
|
3359
|
+
return 1
|
|
3360
|
+
|
|
3361
|
+
url, api_key = config
|
|
3362
|
+
repo = GCCRepository.at(root)
|
|
3363
|
+
if not repo.is_initialized():
|
|
3364
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
3365
|
+
return 1
|
|
3366
|
+
|
|
3367
|
+
with McpClient(url, api_key, timeout=60.0) as client:
|
|
3368
|
+
gcc_dir = repo.gcc_dir
|
|
3369
|
+
sync_state = SyncState(gcc_dir)
|
|
3370
|
+
conflict_log = ConflictLog(gcc_dir)
|
|
3371
|
+
|
|
3372
|
+
interval = getattr(args, "interval", 0)
|
|
3373
|
+
dry_run = getattr(args, "dry_run", False)
|
|
3374
|
+
verbose = getattr(args, "verbose", False)
|
|
3375
|
+
chunk_size = getattr(args, "chunk_size", 500)
|
|
3376
|
+
force_full = getattr(args, "force_full", False)
|
|
3377
|
+
|
|
3378
|
+
consecutive_failures = 0
|
|
3379
|
+
|
|
3380
|
+
def _do_sync() -> bool:
|
|
3381
|
+
nonlocal consecutive_failures
|
|
3382
|
+
state = sync_state.load()
|
|
3383
|
+
if force_full:
|
|
3384
|
+
state = sync_state.reset(state)
|
|
3385
|
+
|
|
3386
|
+
# Phase 1: PULL
|
|
3387
|
+
if verbose:
|
|
3388
|
+
print(" Pull (server → local)...")
|
|
3389
|
+
remote_view = sync_state.get_remote_state_view(state)
|
|
3390
|
+
try:
|
|
3391
|
+
pull_result_raw = client.call_tool("devtorch_sync_pull", {"sync_state": remote_view, "chunk_size": chunk_size})
|
|
3392
|
+
pull_result = json.loads(pull_result_raw)
|
|
3393
|
+
except Exception as e:
|
|
3394
|
+
print(f" Pull failed: {e}. Skipping push. Will retry.", file=sys.stderr)
|
|
3395
|
+
return False
|
|
3396
|
+
|
|
3397
|
+
server_state = pull_result.get("server_state", {})
|
|
3398
|
+
if pull_result.get("bundle"):
|
|
3399
|
+
pull_delta = decompress(pull_result["bundle"])
|
|
3400
|
+
else:
|
|
3401
|
+
pull_delta = {}
|
|
3402
|
+
|
|
3403
|
+
# Phase 2: MERGE PULL
|
|
3404
|
+
if pull_delta.get("delta"):
|
|
3405
|
+
if dry_run:
|
|
3406
|
+
print(f" Pull (dry-run): would merge {sum(len(v) if isinstance(v, list) else 1 for v in pull_delta['delta'].values())} items")
|
|
3407
|
+
else:
|
|
3408
|
+
merge_result = merge_pull(gcc_dir, pull_delta["delta"], conflict_log)
|
|
3409
|
+
state = sync_state.update_after_pull(state, {
|
|
3410
|
+
"server_state": server_state,
|
|
3411
|
+
"delta": pull_delta["delta"],
|
|
3412
|
+
})
|
|
3413
|
+
sync_state.save(state)
|
|
3414
|
+
if verbose:
|
|
3415
|
+
for k, v in merge_result["counts"].items():
|
|
3416
|
+
if v:
|
|
3417
|
+
print(f" {k}: +{v} new")
|
|
3418
|
+
for c in merge_result["conflicts"]:
|
|
3419
|
+
print(f" \u26a0 CONFLICT: {c.get('type','')} — {c.get('item_id','')}")
|
|
3420
|
+
|
|
3421
|
+
# Phase 3: PUSH
|
|
3422
|
+
if verbose:
|
|
3423
|
+
print(" Push (local → server)...")
|
|
3424
|
+
local_delta = build_local_delta(gcc_dir, server_state)
|
|
3425
|
+
if any(isinstance(v, (list, dict)) and v for v in local_delta.values() if isinstance(v, (list, dict))):
|
|
3426
|
+
if dry_run:
|
|
3427
|
+
sizes = {k: len(v) if isinstance(v, list) else 1 for k, v in local_delta.items() if isinstance(v, (list, dict)) and v}
|
|
3428
|
+
print(f" Push (dry-run): would send {sizes}")
|
|
3429
|
+
else:
|
|
3430
|
+
compressed_bundle = compress(local_delta)
|
|
3431
|
+
try:
|
|
3432
|
+
push_result_raw = client.call_tool("devtorch_sync_push", {"bundle": compressed_bundle})
|
|
3433
|
+
push_result = json.loads(push_result_raw)
|
|
3434
|
+
except Exception as e:
|
|
3435
|
+
print(f" Push failed: {e}. Local is updated but server may not have latest.", file=sys.stderr)
|
|
3436
|
+
return False
|
|
3437
|
+
|
|
3438
|
+
# Update sync state with push results
|
|
3439
|
+
state = sync_state.update_after_push(state, {"delta": local_delta}, push_result.get("server_state", {}))
|
|
3440
|
+
sync_state.save(state)
|
|
3441
|
+
if verbose:
|
|
3442
|
+
for k, v in push_result.get("counts", {}).items():
|
|
3443
|
+
if v:
|
|
3444
|
+
print(f" {k}: +{v}")
|
|
3445
|
+
for c in push_result.get("conflicts", []):
|
|
3446
|
+
print(f" \u26a0 CONFLICT: {c.get('type','')} — {c.get('item_id','')}")
|
|
3447
|
+
else:
|
|
3448
|
+
if verbose:
|
|
3449
|
+
print(" Nothing to push.")
|
|
3450
|
+
|
|
3451
|
+
# Show unresolved conflicts
|
|
3452
|
+
unresolved = conflict_log.count_unresolved()
|
|
3453
|
+
if unresolved:
|
|
3454
|
+
print(f" \u26a0 {unresolved} unresolved conflict(s) — run 'devtorch sync conflicts' for details")
|
|
3455
|
+
|
|
3456
|
+
consecutive_failures = 0
|
|
3457
|
+
return True
|
|
3458
|
+
|
|
3459
|
+
if interval > 0:
|
|
3460
|
+
interval_seconds = interval * 60
|
|
3461
|
+
consecutive_failures = 0
|
|
3462
|
+
print(f"Syncing every {interval} minute(s) — Ctrl-C to stop.")
|
|
3463
|
+
try:
|
|
3464
|
+
while True:
|
|
3465
|
+
print()
|
|
3466
|
+
success = _do_sync()
|
|
3467
|
+
if success:
|
|
3468
|
+
consecutive_failures = 0
|
|
3469
|
+
sleep_time = interval_seconds
|
|
3470
|
+
print(f" Next sync in {interval} minute(s).")
|
|
3471
|
+
else:
|
|
3472
|
+
consecutive_failures += 1
|
|
3473
|
+
sleep_time = min(interval_seconds * (2 ** min(consecutive_failures - 1, 4)), 3600)
|
|
3474
|
+
print(f" Sync failed ({consecutive_failures} consecutive). Retrying in {sleep_time // 60} minute(s)...")
|
|
3475
|
+
_time.sleep(sleep_time)
|
|
3476
|
+
except KeyboardInterrupt:
|
|
3477
|
+
print("\nSync stopped.")
|
|
3478
|
+
else:
|
|
3479
|
+
_do_sync()
|
|
3480
|
+
|
|
3481
|
+
return 0
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
def cmd_sync_conflicts(args: argparse.Namespace) -> int:
|
|
3485
|
+
"""List sync conflicts."""
|
|
3486
|
+
from devtorch_core.gcc import GCCRepository
|
|
3487
|
+
from devtorch_core.cloud.sync_conflicts import ConflictLog
|
|
3488
|
+
|
|
3489
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
3490
|
+
if not repo.is_initialized():
|
|
3491
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
3492
|
+
return 1
|
|
3493
|
+
|
|
3494
|
+
conflict_log = ConflictLog(repo.gcc_dir)
|
|
3495
|
+
show_all = getattr(args, "show_all", False)
|
|
3496
|
+
severity_filter = getattr(args, "severity", None)
|
|
3497
|
+
|
|
3498
|
+
entries = conflict_log.list_all() if show_all else conflict_log.list_unresolved()
|
|
3499
|
+
|
|
3500
|
+
if severity_filter:
|
|
3501
|
+
entries = [e for e in entries if e.get("severity") == severity_filter]
|
|
3502
|
+
|
|
3503
|
+
if not entries:
|
|
3504
|
+
print("No conflicts found.")
|
|
3505
|
+
return 0
|
|
3506
|
+
|
|
3507
|
+
print(f"Sync conflicts ({len(entries)}):\n")
|
|
3508
|
+
for e in entries:
|
|
3509
|
+
status = "\u2713 resolved" if e.get("resolved") else "\u26a0 unresolved"
|
|
3510
|
+
print(f" [{e.get('severity', '?')}] {e.get('type', '?')} — {e.get('item_id', '?')} ({status})")
|
|
3511
|
+
if e.get("local_value"):
|
|
3512
|
+
lv = e["local_value"]
|
|
3513
|
+
print(f" Local: {lv.get('message', lv.get('content', json.dumps(lv)))[:80]}")
|
|
3514
|
+
if e.get("remote_value"):
|
|
3515
|
+
rv = e["remote_value"]
|
|
3516
|
+
print(f" Remote: {rv.get('message', rv.get('content', json.dumps(rv)))[:80]}")
|
|
3517
|
+
print(f" Resolution: {e.get('resolution', 'pending')}")
|
|
3518
|
+
if not e.get("resolved"):
|
|
3519
|
+
print(f" Use: devtorch sync conflicts-resolve {e['conflict_id']} [--use-remote] [--dismiss]")
|
|
3520
|
+
print()
|
|
3521
|
+
return 0
|
|
3522
|
+
|
|
3523
|
+
|
|
3524
|
+
def cmd_sync_conflicts_resolve(args: argparse.Namespace) -> int:
|
|
3525
|
+
"""Resolve a sync conflict."""
|
|
3526
|
+
from devtorch_core.gcc import GCCRepository
|
|
3527
|
+
from devtorch_core.cloud.sync_conflicts import ConflictLog
|
|
3528
|
+
|
|
3529
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
3530
|
+
if not repo.is_initialized():
|
|
3531
|
+
print("This directory is not tracking DevTorch yet.", file=sys.stderr)
|
|
3532
|
+
return 1
|
|
3533
|
+
|
|
3534
|
+
conflict_log = ConflictLog(repo.gcc_dir)
|
|
3535
|
+
conflict_id = args.conflict_id
|
|
3536
|
+
use_remote = getattr(args, "use_remote", False)
|
|
3537
|
+
dismiss = getattr(args, "dismiss", False)
|
|
3538
|
+
dry_run = getattr(args, "dry_run", False)
|
|
3539
|
+
|
|
3540
|
+
# First check the conflict exists
|
|
3541
|
+
all_entries = conflict_log.list_all()
|
|
3542
|
+
found = None
|
|
3543
|
+
for e in all_entries:
|
|
3544
|
+
if e.get("conflict_id") == conflict_id:
|
|
3545
|
+
found = e
|
|
3546
|
+
break
|
|
3547
|
+
|
|
3548
|
+
if not found:
|
|
3549
|
+
print(f"Conflict '{conflict_id}' not found.", file=sys.stderr)
|
|
3550
|
+
return 1
|
|
3551
|
+
|
|
3552
|
+
if dry_run:
|
|
3553
|
+
rv = found.get("remote_value", {})
|
|
3554
|
+
lv = found.get("local_value", {})
|
|
3555
|
+
print(f"Conflict '{conflict_id}':")
|
|
3556
|
+
print(f" Type: {found.get('type', '?')}")
|
|
3557
|
+
print(f" Current resolution: {found.get('resolution', 'pending')}")
|
|
3558
|
+
if use_remote:
|
|
3559
|
+
print(f" Would apply REMOTE: {rv.get('message', rv.get('content', json.dumps(rv)))[:80]}")
|
|
3560
|
+
elif dismiss:
|
|
3561
|
+
print(f" Would dismiss (keep local).")
|
|
3562
|
+
else:
|
|
3563
|
+
print(f" Would keep LOCAL (default). Add --use-remote to override.")
|
|
3564
|
+
return 0
|
|
3565
|
+
|
|
3566
|
+
result = conflict_log.resolve(conflict_id, use_remote=use_remote, dismiss=dismiss)
|
|
3567
|
+
if result is None:
|
|
3568
|
+
print(f"Conflict '{conflict_id}' not found.", file=sys.stderr)
|
|
3569
|
+
return 1
|
|
3570
|
+
|
|
3571
|
+
print(f"Conflict '{conflict_id}' resolved: {result.get('resolution', 'completed')}.")
|
|
3572
|
+
return 0
|
|
3573
|
+
|
|
3574
|
+
|
|
3575
|
+
def cmd_cloud_setup(args: argparse.Namespace) -> int:
|
|
3576
|
+
from devtorch_core.cloud.setup import run_cloud_setup
|
|
3577
|
+
|
|
3578
|
+
try:
|
|
3579
|
+
result = run_cloud_setup(
|
|
3580
|
+
env_file=args.env_file,
|
|
3581
|
+
org=args.org,
|
|
3582
|
+
repo=args.repo,
|
|
3583
|
+
ide=args.ide,
|
|
3584
|
+
openai_key=args.openai_key,
|
|
3585
|
+
non_interactive=args.non_interactive,
|
|
3586
|
+
skip_key=args.skip_key,
|
|
3587
|
+
skip_client_config=args.skip_client_config,
|
|
3588
|
+
dry_run=args.dry_run,
|
|
3589
|
+
)
|
|
3590
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
3591
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
3592
|
+
return 1
|
|
3593
|
+
|
|
3594
|
+
if args.dry_run:
|
|
3595
|
+
print(" ".join(result["args"]))
|
|
3596
|
+
return 0
|
|
3597
|
+
return int(result.get("returncode", "0"))
|
|
3598
|
+
|
|
3599
|
+
|
|
3600
|
+
def cmd_watch(args: argparse.Namespace) -> int:
|
|
3601
|
+
"""Tail the DevTorch event stream (SSE broadcaster)."""
|
|
3602
|
+
import sys
|
|
3603
|
+
from pathlib import Path
|
|
3604
|
+
from devtorch_core.broadcast.broadcaster import EventBroadcaster
|
|
3605
|
+
from devtorch_core.broadcast.watcher import GCCWatcher
|
|
3606
|
+
|
|
3607
|
+
gcc_dir = Path(args.gcc_dir)
|
|
3608
|
+
if not gcc_dir.exists():
|
|
3609
|
+
print(f"Error: {gcc_dir} does not exist. Run devtorch init first.", file=sys.stderr)
|
|
3610
|
+
return 1
|
|
3611
|
+
|
|
3612
|
+
broadcaster = EventBroadcaster()
|
|
3613
|
+
q = broadcaster.subscribe()
|
|
3614
|
+
watcher = GCCWatcher(gcc_dir, broadcaster, poll_interval=0.2)
|
|
3615
|
+
watcher.start()
|
|
3616
|
+
print(f"Watching {gcc_dir}/events.log.jsonl for coordination events... (Ctrl-C to stop)")
|
|
3617
|
+
import json as _json
|
|
3618
|
+
import time as _time
|
|
3619
|
+
try:
|
|
3620
|
+
while True:
|
|
3621
|
+
_time.sleep(0.1)
|
|
3622
|
+
while not q.empty():
|
|
3623
|
+
event = q.get_nowait()
|
|
3624
|
+
print(_json.dumps(event, indent=None))
|
|
3625
|
+
except KeyboardInterrupt:
|
|
3626
|
+
print("\nStopped.")
|
|
3627
|
+
finally:
|
|
3628
|
+
watcher.stop()
|
|
3629
|
+
return 0
|
|
3630
|
+
|
|
3631
|
+
|
|
3632
|
+
# ---------------------------------------------------------------------------
|
|
3633
|
+
# S24: Reasoning Plus commands
|
|
3634
|
+
# ---------------------------------------------------------------------------
|
|
3635
|
+
|
|
3636
|
+
|
|
3637
|
+
def _reasoning_plus_store(args: argparse.Namespace) -> Any:
|
|
3638
|
+
from devtorch_core.reasoning_plus.config import SettingsStore
|
|
3639
|
+
|
|
3640
|
+
project_root = Path(args.path).resolve()
|
|
3641
|
+
gcc_repo = None
|
|
3642
|
+
try:
|
|
3643
|
+
gcc_repo = GCCRepository.at(project_root)
|
|
3644
|
+
except Exception:
|
|
3645
|
+
pass
|
|
3646
|
+
gcc_dir = gcc_repo.gcc_dir if gcc_repo and gcc_repo.is_initialized() else None
|
|
3647
|
+
return SettingsStore(gcc_dir)
|
|
3648
|
+
|
|
3649
|
+
|
|
3650
|
+
def cmd_reasoning_plus_enable(args: argparse.Namespace) -> int:
|
|
3651
|
+
store = _reasoning_plus_store(args)
|
|
3652
|
+
if store._project_dir is None:
|
|
3653
|
+
print("error: no initialized .GCC/ repo found. Run devtorch init first.", file=sys.stderr)
|
|
3654
|
+
return 1
|
|
3655
|
+
config, _ = store.load()
|
|
3656
|
+
config = config.with_values(enabled=True)
|
|
3657
|
+
store.save_project(config)
|
|
3658
|
+
print("Reasoning Plus enabled for this project.")
|
|
3659
|
+
return 0
|
|
3660
|
+
|
|
3661
|
+
|
|
3662
|
+
def cmd_reasoning_plus_disable(args: argparse.Namespace) -> int:
|
|
3663
|
+
store = _reasoning_plus_store(args)
|
|
3664
|
+
if store._project_dir is None:
|
|
3665
|
+
print("error: no initialized .GCC/ repo found. Run devtorch init first.", file=sys.stderr)
|
|
3666
|
+
return 1
|
|
3667
|
+
config, _ = store.load()
|
|
3668
|
+
config = config.with_values(enabled=False)
|
|
3669
|
+
store.save_project(config)
|
|
3670
|
+
print("Reasoning Plus disabled for this project.")
|
|
3671
|
+
return 0
|
|
3672
|
+
|
|
3673
|
+
|
|
3674
|
+
def cmd_reasoning_plus_status(args: argparse.Namespace) -> int:
|
|
3675
|
+
store = _reasoning_plus_store(args)
|
|
3676
|
+
config, source = store.load()
|
|
3677
|
+
print(f"Reasoning Plus status")
|
|
3678
|
+
print(f" source: {source}")
|
|
3679
|
+
print(f" enabled: {config.enabled}")
|
|
3680
|
+
print(f" smart_top_n: {config.smart_top_n}")
|
|
3681
|
+
print(f" smart_max_lines: {config.smart_max_lines}")
|
|
3682
|
+
print(f" context_k: {config.context_k}")
|
|
3683
|
+
print(f" require_thinking: {config.require_thinking}")
|
|
3684
|
+
print(f" max_keywords: {config.max_keywords}")
|
|
3685
|
+
print(f" cache_ttl_secs: {config.cache_ttl_secs}")
|
|
3686
|
+
print(f" learning_enabled: {config.learning_enabled}")
|
|
3687
|
+
print(f" learning_top_n: {config.learning_top_n}")
|
|
3688
|
+
print(f" learning_max_lines: {config.learning_max_lines}")
|
|
3689
|
+
print(f" learning_embedding_backend: {config.learning_embedding_backend}")
|
|
3690
|
+
print(f" learning_embedding_model: {config.learning_embedding_model}")
|
|
3691
|
+
print(f" learning_extraction_strategy: {config.learning_extraction_strategy}")
|
|
3692
|
+
print(f" learning_relevance_cache_ttl: {config.learning_relevance_cache_ttl}")
|
|
3693
|
+
return 0
|
|
3694
|
+
|
|
3695
|
+
|
|
3696
|
+
def cmd_reasoning_plus_config(args: argparse.Namespace) -> int:
|
|
3697
|
+
store = _reasoning_plus_store(args)
|
|
3698
|
+
if store._project_dir is None:
|
|
3699
|
+
print("error: no initialized .GCC/ repo found. Run devtorch init first.", file=sys.stderr)
|
|
3700
|
+
return 1
|
|
3701
|
+
config, _ = store.load()
|
|
3702
|
+
overrides: dict[str, Any] = {}
|
|
3703
|
+
if args.smart_top_n is not None:
|
|
3704
|
+
overrides["smart_top_n"] = args.smart_top_n
|
|
3705
|
+
if args.smart_max_lines is not None:
|
|
3706
|
+
overrides["smart_max_lines"] = args.smart_max_lines
|
|
3707
|
+
if args.context_k is not None:
|
|
3708
|
+
overrides["context_k"] = args.context_k
|
|
3709
|
+
if args.require_thinking is not None:
|
|
3710
|
+
overrides["require_thinking"] = args.require_thinking
|
|
3711
|
+
if args.max_keywords is not None:
|
|
3712
|
+
overrides["max_keywords"] = args.max_keywords
|
|
3713
|
+
if args.cache_ttl is not None:
|
|
3714
|
+
overrides["cache_ttl_secs"] = args.cache_ttl
|
|
3715
|
+
if args.learning_enabled is not None:
|
|
3716
|
+
overrides["learning_enabled"] = args.learning_enabled
|
|
3717
|
+
if args.learning_top_n is not None:
|
|
3718
|
+
overrides["learning_top_n"] = args.learning_top_n
|
|
3719
|
+
if args.learning_max_lines is not None:
|
|
3720
|
+
overrides["learning_max_lines"] = args.learning_max_lines
|
|
3721
|
+
if args.learning_embedding_backend is not None:
|
|
3722
|
+
overrides["learning_embedding_backend"] = args.learning_embedding_backend
|
|
3723
|
+
if args.learning_embedding_model is not None:
|
|
3724
|
+
overrides["learning_embedding_model"] = args.learning_embedding_model
|
|
3725
|
+
if args.learning_extraction_strategy is not None:
|
|
3726
|
+
overrides["learning_extraction_strategy"] = args.learning_extraction_strategy
|
|
3727
|
+
if args.learning_relevance_cache_ttl is not None:
|
|
3728
|
+
overrides["learning_relevance_cache_ttl"] = args.learning_relevance_cache_ttl
|
|
3729
|
+
config = config.with_values(**overrides)
|
|
3730
|
+
store.save_project(config)
|
|
3731
|
+
print("Reasoning Plus configuration updated.")
|
|
3732
|
+
return 0
|
|
3733
|
+
|
|
3734
|
+
|
|
3735
|
+
def cmd_reasoning_plus_reset(args: argparse.Namespace) -> int:
|
|
3736
|
+
store = _reasoning_plus_store(args)
|
|
3737
|
+
if store._project_dir is None:
|
|
3738
|
+
print("error: no initialized .GCC/ repo found. Run devtorch init first.", file=sys.stderr)
|
|
3739
|
+
return 1
|
|
3740
|
+
store.reset_project()
|
|
3741
|
+
print("Reasoning Plus project settings reset; will fall back to global/default.")
|
|
3742
|
+
return 0
|
|
3743
|
+
|
|
3744
|
+
|
|
3745
|
+
def cmd_reasoning_plus_cross_project_suggest(args: argparse.Namespace) -> int:
|
|
3746
|
+
"""Suggest relevant learnings from sibling governed projects."""
|
|
3747
|
+
from devtorch_core.reasoning_plus.learning import ReasoningPlusLearning
|
|
3748
|
+
from devtorch_core.reasoning_plus import ReasoningPlusConfig
|
|
3749
|
+
|
|
3750
|
+
project_root = Path(args.path).resolve()
|
|
3751
|
+
gcc_dir = project_root / ".GCC"
|
|
3752
|
+
if not gcc_dir.exists():
|
|
3753
|
+
print("error: no initialized .GCC/ repo found. Run devtorch init first.", file=sys.stderr)
|
|
3754
|
+
return 1
|
|
3755
|
+
config = ReasoningPlusConfig(learning_enabled=True)
|
|
3756
|
+
drpl = ReasoningPlusLearning(gcc_dir=gcc_dir, repo_path=project_root, config=config)
|
|
3757
|
+
learnings = drpl.cross_project_suggest(args.query, top_n=args.top_n)
|
|
3758
|
+
if not learnings:
|
|
3759
|
+
print("No relevant cross-project learnings found.")
|
|
3760
|
+
return 0
|
|
3761
|
+
print(f"Found {len(learnings)} relevant cross-project learning(s):")
|
|
3762
|
+
for i, learning in enumerate(learnings, 1):
|
|
3763
|
+
short = learning.short_form(max_chars=200)
|
|
3764
|
+
print(f" {i}. [{learning.type}] {short}")
|
|
3765
|
+
return 0
|
|
3766
|
+
|
|
3767
|
+
|
|
3768
|
+
def main(argv: list[str] | None = None) -> int:
|
|
3769
|
+
parser = build_parser()
|
|
3770
|
+
args = parser.parse_args(argv)
|
|
3771
|
+
|
|
3772
|
+
# Friendly default: show dashboard in an initialized repo, otherwise welcome guide.
|
|
3773
|
+
if args.command is None:
|
|
3774
|
+
repo = GCCRepository.at(Path(args.path).resolve())
|
|
3775
|
+
if repo.is_initialized():
|
|
3776
|
+
return cmd_today(args)
|
|
3777
|
+
return cmd_welcome(args)
|
|
3778
|
+
|
|
3779
|
+
# S11: mcp-server
|
|
3780
|
+
if args.command == "mcp-server":
|
|
3781
|
+
if args.transport == "sse":
|
|
3782
|
+
import os
|
|
3783
|
+
from devtorch_core.mcp.server import _run_with_sse
|
|
3784
|
+
from devtorch_core.storage import create_backend_from_env
|
|
3785
|
+
|
|
3786
|
+
org_id = os.environ.get("DEVTORCH_ORG_ID", "").strip()
|
|
3787
|
+
if not org_id:
|
|
3788
|
+
print("error: DEVTORCH_ORG_ID is required for SSE transport.", file=sys.stderr)
|
|
3789
|
+
return 1
|
|
3790
|
+
|
|
3791
|
+
try:
|
|
3792
|
+
auth_backend = create_backend_from_env(prefix=org_id)
|
|
3793
|
+
except (KeyError, ValueError) as exc:
|
|
3794
|
+
print(f"error: storage configuration: {exc}", file=sys.stderr)
|
|
3795
|
+
return 1
|
|
3796
|
+
|
|
3797
|
+
def repo_factory(req_org: str, repo_id: str):
|
|
3798
|
+
from devtorch_core.gcc import GCCRepository
|
|
3799
|
+
from pathlib import Path
|
|
3800
|
+
backend = create_backend_from_env(prefix=f"{req_org}/{repo_id}/.GCC")
|
|
3801
|
+
return GCCRepository(root=Path(f"/tmp/{req_org}/{repo_id}"), backend=backend)
|
|
3802
|
+
|
|
3803
|
+
_run_with_sse(
|
|
3804
|
+
repo_factory=repo_factory,
|
|
3805
|
+
auth_backend=auth_backend,
|
|
3806
|
+
host=args.host,
|
|
3807
|
+
port=args.port,
|
|
3808
|
+
)
|
|
3809
|
+
else:
|
|
3810
|
+
from devtorch_core.mcp.server import main as mcp_main
|
|
3811
|
+
mcp_main()
|
|
3812
|
+
return 0
|
|
3813
|
+
|
|
3814
|
+
if args.command == "cloud":
|
|
3815
|
+
import os
|
|
3816
|
+
import json
|
|
3817
|
+
import secrets
|
|
3818
|
+
import hashlib
|
|
3819
|
+
import datetime
|
|
3820
|
+
from devtorch_core.storage import create_backend_from_env
|
|
3821
|
+
|
|
3822
|
+
# setup, client-config, and sync do not need the admin backend; they are local.
|
|
3823
|
+
if args.cloud_command == "setup":
|
|
3824
|
+
return args.func(args)
|
|
3825
|
+
if args.cloud_command == "client-config":
|
|
3826
|
+
return args.func(args)
|
|
3827
|
+
if args.cloud_command == "sync":
|
|
3828
|
+
from devtorch_core.cloud.sync import sync_commits
|
|
3829
|
+
|
|
3830
|
+
root = Path(args.path).resolve()
|
|
3831
|
+
from_now = getattr(args, "from_now", False)
|
|
3832
|
+
|
|
3833
|
+
if from_now:
|
|
3834
|
+
# Pure cursor bookkeeping — no network I/O, no credentials.
|
|
3835
|
+
try:
|
|
3836
|
+
result = sync_commits(root / ".GCC", "", "", from_now=True)
|
|
3837
|
+
except ValueError as exc:
|
|
3838
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
3839
|
+
return 1
|
|
3840
|
+
print(
|
|
3841
|
+
f"Cursor set to {result['cursor']}; nothing pushed — "
|
|
3842
|
+
"commits from here on will sync."
|
|
3843
|
+
)
|
|
3844
|
+
return 0
|
|
3845
|
+
|
|
3846
|
+
url = os.environ.get("DEVTORCH_MCP_URL", "")
|
|
3847
|
+
api_key = os.environ.get("DEVTORCH_API_KEY", "")
|
|
3848
|
+
if not url or not api_key:
|
|
3849
|
+
try:
|
|
3850
|
+
mcp = json.loads((root / ".mcp.json").read_text(encoding="utf-8"))
|
|
3851
|
+
env_cfg = mcp["mcpServers"]["devtorch"]["env"]
|
|
3852
|
+
url = url or env_cfg.get("DEVTORCH_MCP_URL", "")
|
|
3853
|
+
api_key = api_key or env_cfg.get("DEVTORCH_API_KEY", "")
|
|
3854
|
+
except (OSError, KeyError, TypeError, json.JSONDecodeError):
|
|
3855
|
+
pass
|
|
3856
|
+
if not url or not api_key:
|
|
3857
|
+
print("error: need DEVTORCH_MCP_URL and DEVTORCH_API_KEY (env or .mcp.json)", file=sys.stderr)
|
|
3858
|
+
return 1
|
|
3859
|
+
messages_url = url.rstrip("/").removesuffix("/sse") + "/messages?sessionId=cli-sync"
|
|
3860
|
+
try:
|
|
3861
|
+
result = sync_commits(root / ".GCC", messages_url, api_key)
|
|
3862
|
+
except Exception as exc:
|
|
3863
|
+
print(f"error: cloud sync failed — {exc}", file=sys.stderr)
|
|
3864
|
+
return 1
|
|
3865
|
+
print(f"Pushed {result['pushed']} commit(s); cursor at {result['cursor']}.")
|
|
3866
|
+
return 0
|
|
3867
|
+
|
|
3868
|
+
try:
|
|
3869
|
+
_admin_backend = create_backend_from_env(prefix=args.org)
|
|
3870
|
+
except (KeyError, ValueError) as exc:
|
|
3871
|
+
print(f"error: storage configuration: {exc}", file=sys.stderr)
|
|
3872
|
+
return 1
|
|
3873
|
+
|
|
3874
|
+
_KEYS_PATH = ".devtorch/api_keys.json"
|
|
3875
|
+
|
|
3876
|
+
if args.cloud_command == "key":
|
|
3877
|
+
if args.key_command == "create":
|
|
3878
|
+
raw_key = secrets.token_urlsafe(32)
|
|
3879
|
+
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
3880
|
+
try:
|
|
3881
|
+
existing = json.loads(_admin_backend.read_bytes(_KEYS_PATH))
|
|
3882
|
+
except FileNotFoundError:
|
|
3883
|
+
existing = {}
|
|
3884
|
+
existing[key_hash] = {
|
|
3885
|
+
"org_id": args.org,
|
|
3886
|
+
"allowed_repos": [args.repo] if args.repo else None,
|
|
3887
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
3888
|
+
}
|
|
3889
|
+
_admin_backend.write_bytes(_KEYS_PATH, json.dumps(existing, indent=2).encode())
|
|
3890
|
+
print(f"API key created. Store this — it will not be shown again:\n\n {raw_key}\n")
|
|
3891
|
+
if args.github:
|
|
3892
|
+
_set_github_secret(args.github, "DEVTORCH_API_KEY", raw_key)
|
|
3893
|
+
return 0
|
|
3894
|
+
|
|
3895
|
+
elif args.key_command == "list":
|
|
3896
|
+
try:
|
|
3897
|
+
data = json.loads(_admin_backend.read_bytes(_KEYS_PATH))
|
|
3898
|
+
except FileNotFoundError:
|
|
3899
|
+
print("No API keys found.")
|
|
3900
|
+
return 0
|
|
3901
|
+
except json.JSONDecodeError as exc:
|
|
3902
|
+
print(f"error: keys file is corrupted: {exc}", file=sys.stderr)
|
|
3903
|
+
return 1
|
|
3904
|
+
if not data:
|
|
3905
|
+
print("No API keys found.")
|
|
3906
|
+
return 0
|
|
3907
|
+
print(f"{'KEY HASH (first 16)':20} {'ORG':16} {'REPOS':20} {'STATUS':10} CREATED")
|
|
3908
|
+
print("-" * 92)
|
|
3909
|
+
for key_hash, entry in sorted(data.items(), key=lambda x: x[1].get("created_at", "")):
|
|
3910
|
+
repos = ",".join(entry.get("allowed_repos") or []) or "(all)"
|
|
3911
|
+
created = entry.get("created_at", "unknown")[:19]
|
|
3912
|
+
status = "revoked" if entry.get("revoked_at") else "active"
|
|
3913
|
+
print(f"{key_hash[:16]:20} {entry.get('org_id',''):16} {repos:20} {status:10} {created}")
|
|
3914
|
+
return 0
|
|
3915
|
+
|
|
3916
|
+
elif args.key_command == "revoke":
|
|
3917
|
+
try:
|
|
3918
|
+
data = json.loads(_admin_backend.read_bytes(_KEYS_PATH))
|
|
3919
|
+
except FileNotFoundError:
|
|
3920
|
+
print("No API keys found.")
|
|
3921
|
+
return 1
|
|
3922
|
+
except json.JSONDecodeError as exc:
|
|
3923
|
+
print(f"error: keys file is corrupted: {exc}", file=sys.stderr)
|
|
3924
|
+
return 1
|
|
3925
|
+
if len(args.key_prefix) < 8:
|
|
3926
|
+
print("error: key_prefix must be at least 8 characters to avoid accidental matches.", file=sys.stderr)
|
|
3927
|
+
return 1
|
|
3928
|
+
prefix_to_revoke = args.key_prefix.lower()
|
|
3929
|
+
matches = [h for h in data if h.startswith(prefix_to_revoke)]
|
|
3930
|
+
if not matches:
|
|
3931
|
+
print(f"No key found with prefix: {prefix_to_revoke}")
|
|
3932
|
+
return 1
|
|
3933
|
+
if len(matches) > 1:
|
|
3934
|
+
print(f"Ambiguous prefix matches {len(matches)} keys. Use more characters.")
|
|
3935
|
+
return 1
|
|
3936
|
+
if not args.yes:
|
|
3937
|
+
confirm = input(f"Revoke key {matches[0][:16]}...? This cannot be undone. [y/N] ").strip().lower()
|
|
3938
|
+
if confirm != "y":
|
|
3939
|
+
print("Aborted.")
|
|
3940
|
+
return 0
|
|
3941
|
+
old_entry = data[matches[0]]
|
|
3942
|
+
old_entry["revoked_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
3943
|
+
data[matches[0]] = old_entry
|
|
3944
|
+
_admin_backend.write_bytes(_KEYS_PATH, json.dumps(data, indent=2).encode())
|
|
3945
|
+
print(f"Revoked key {matches[0][:32]}...")
|
|
3946
|
+
return 0
|
|
3947
|
+
|
|
3948
|
+
elif args.key_command == "rotate":
|
|
3949
|
+
try:
|
|
3950
|
+
data = json.loads(_admin_backend.read_bytes(_KEYS_PATH))
|
|
3951
|
+
except FileNotFoundError:
|
|
3952
|
+
print("No API keys found.")
|
|
3953
|
+
return 1
|
|
3954
|
+
except json.JSONDecodeError as exc:
|
|
3955
|
+
print(f"error: keys file is corrupted: {exc}", file=sys.stderr)
|
|
3956
|
+
return 1
|
|
3957
|
+
if len(args.key_prefix) < 8:
|
|
3958
|
+
print("error: key_prefix must be at least 8 characters to avoid accidental matches.", file=sys.stderr)
|
|
3959
|
+
return 1
|
|
3960
|
+
prefix_to_rotate = args.key_prefix.lower()
|
|
3961
|
+
matches = [h for h in data if h.startswith(prefix_to_rotate)]
|
|
3962
|
+
if not matches:
|
|
3963
|
+
print(f"No key found with prefix: {prefix_to_rotate}")
|
|
3964
|
+
return 1
|
|
3965
|
+
if len(matches) > 1:
|
|
3966
|
+
print(f"Ambiguous prefix matches {len(matches)} keys. Use more characters.")
|
|
3967
|
+
return 1
|
|
3968
|
+
if not args.yes:
|
|
3969
|
+
confirm = input(f"Rotate key {matches[0][:16]}...? This will revoke the old key and create a new one. [y/N] ").strip().lower()
|
|
3970
|
+
if confirm != "y":
|
|
3971
|
+
print("Aborted.")
|
|
3972
|
+
return 0
|
|
3973
|
+
old_hash = matches[0]
|
|
3974
|
+
old_entry = data[old_hash]
|
|
3975
|
+
old_entry["revoked_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
3976
|
+
old_entry["rotated_from"] = old_hash
|
|
3977
|
+
data[old_hash] = old_entry
|
|
3978
|
+
|
|
3979
|
+
raw_key = secrets.token_urlsafe(32)
|
|
3980
|
+
new_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
3981
|
+
data[new_hash] = {
|
|
3982
|
+
"org_id": args.org,
|
|
3983
|
+
"allowed_repos": old_entry.get("allowed_repos"),
|
|
3984
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
3985
|
+
"rotated_from": old_hash,
|
|
3986
|
+
}
|
|
3987
|
+
_admin_backend.write_bytes(_KEYS_PATH, json.dumps(data, indent=2).encode())
|
|
3988
|
+
print(f"Rotated key {old_hash[:32]}... → {new_hash[:32]}...")
|
|
3989
|
+
print(f"New API key (store safely — it will not be shown again):\n\n {raw_key}\n")
|
|
3990
|
+
return 0
|
|
3991
|
+
|
|
3992
|
+
if args.cloud_command == "repo":
|
|
3993
|
+
import os
|
|
3994
|
+
encryption_key_hex = os.environ.get("DEVTORCH_PAT_ENCRYPTION_KEY", "")
|
|
3995
|
+
if not encryption_key_hex:
|
|
3996
|
+
print("error: DEVTORCH_PAT_ENCRYPTION_KEY is required for repo operations.", file=sys.stderr)
|
|
3997
|
+
return 1
|
|
3998
|
+
try:
|
|
3999
|
+
encryption_key = bytes.fromhex(encryption_key_hex)
|
|
4000
|
+
except ValueError:
|
|
4001
|
+
print("error: DEVTORCH_PAT_ENCRYPTION_KEY must be a hex string.", file=sys.stderr)
|
|
4002
|
+
return 1
|
|
4003
|
+
|
|
4004
|
+
from devtorch_core.github.pat import GitHubPATStore
|
|
4005
|
+
pat_store = GitHubPATStore(backend=_admin_backend, encryption_key=encryption_key)
|
|
4006
|
+
|
|
4007
|
+
if args.repo_command == "connect":
|
|
4008
|
+
if args.pat:
|
|
4009
|
+
pat_store.store_pat(repo_id=args.repo, pat=args.pat, github_repo=args.github)
|
|
4010
|
+
print(f"Connected: {args.org}/{args.repo} → github.com/{args.github} (PAT)")
|
|
4011
|
+
elif args.github_app:
|
|
4012
|
+
print("GitHub App connection is managed automatically via webhook.")
|
|
4013
|
+
print("Install the DevTorch GitHub App on your repo — the server handles registration.")
|
|
4014
|
+
return 0
|
|
4015
|
+
|
|
4016
|
+
elif args.repo_command == "list":
|
|
4017
|
+
repos = pat_store.list_repos()
|
|
4018
|
+
if not repos:
|
|
4019
|
+
print("No repositories connected.")
|
|
4020
|
+
else:
|
|
4021
|
+
for r in repos:
|
|
4022
|
+
entry = pat_store.get_pat(r)
|
|
4023
|
+
github_repo = entry["github_repo"] if entry else "(PAT decryption failed)"
|
|
4024
|
+
print(f" {r} → github.com/{github_repo}")
|
|
4025
|
+
return 0
|
|
4026
|
+
|
|
4027
|
+
elif args.repo_command == "disconnect":
|
|
4028
|
+
pat_store.delete_pat(repo_id=args.repo)
|
|
4029
|
+
print(f"Disconnected: {args.repo}")
|
|
4030
|
+
return 0
|
|
4031
|
+
|
|
4032
|
+
print(f"error: unknown cloud command: {args.cloud_command}", file=sys.stderr)
|
|
4033
|
+
return 1
|
|
4034
|
+
|
|
4035
|
+
# S11: hooks
|
|
4036
|
+
if args.command == "hooks":
|
|
4037
|
+
from devtorch_core.hooks.installer import install_all_hooks, get_hooks_status
|
|
4038
|
+
|
|
4039
|
+
# Try to resolve the project root from a GCC repo, fall back to cwd
|
|
4040
|
+
try:
|
|
4041
|
+
gcc_repo = GCCRepository.at(Path(args.path).resolve())
|
|
4042
|
+
project_root = str(gcc_repo.root) if gcc_repo.is_initialized() else args.path
|
|
4043
|
+
except Exception:
|
|
4044
|
+
project_root = args.path
|
|
4045
|
+
|
|
4046
|
+
if args.hooks_command == "install":
|
|
4047
|
+
result = install_all_hooks(project_root)
|
|
4048
|
+
print(f"Claude hooks: {'✓' if result.claude_hooks_written else '✗'} {result.claude_hooks_path}")
|
|
4049
|
+
print(f"Git hook: {'✓' if result.git_hook_written else '✗'} {result.git_hook_path}")
|
|
4050
|
+
if getattr(result, "git_capture_hooks_written", False):
|
|
4051
|
+
print(f"Git capture hooks: ✓ ({len(result.git_capture_hooks)} installed)")
|
|
4052
|
+
else:
|
|
4053
|
+
print(f"Git capture hooks: ✗")
|
|
4054
|
+
if result.errors:
|
|
4055
|
+
for e in result.errors:
|
|
4056
|
+
print(f" Error: {e}")
|
|
4057
|
+
elif args.hooks_command == "status":
|
|
4058
|
+
status = get_hooks_status(project_root)
|
|
4059
|
+
for k, v in status.items():
|
|
4060
|
+
print(f"{k}: {v}")
|
|
4061
|
+
elif args.hooks_command == "git-commit":
|
|
4062
|
+
from devtorch_core.hooks.git_commit import handle_commit_msg
|
|
4063
|
+
handle_commit_msg(args.commit_msg_file)
|
|
4064
|
+
elif args.hooks_command == "git-capture":
|
|
4065
|
+
from devtorch_core.hooks.git_capture import handle_git_capture
|
|
4066
|
+
handle_git_capture(args.event, args.args)
|
|
4067
|
+
elif args.hooks_command == "run":
|
|
4068
|
+
from devtorch_core.hooks.runner import run as run_hook
|
|
4069
|
+
run_hook(args.hook_name)
|
|
4070
|
+
return 0
|
|
4071
|
+
|
|
4072
|
+
if args.command == "reasoning":
|
|
4073
|
+
if getattr(args, "reasoning_cmd", None) is None:
|
|
4074
|
+
# Print help when no subcommand given
|
|
4075
|
+
build_parser().parse_args(["reasoning", "--help"])
|
|
4076
|
+
return 0
|
|
4077
|
+
return args.func(args)
|
|
4078
|
+
|
|
4079
|
+
if args.command == "reasoning-plus":
|
|
4080
|
+
if getattr(args, "reasoning_plus_cmd", None) is None:
|
|
4081
|
+
build_parser().parse_args(["reasoning-plus", "--help"])
|
|
4082
|
+
return 0
|
|
4083
|
+
return args.func(args)
|
|
4084
|
+
|
|
4085
|
+
return args.func(args)
|
|
4086
|
+
|
|
4087
|
+
|
|
4088
|
+
if __name__ == "__main__":
|
|
4089
|
+
raise SystemExit(main())
|
|
4090
|
+
|