sid-harness 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sid_harness/__init__.py +13 -0
- sid_harness/__main__.py +4 -0
- sid_harness/_native.py +1639 -0
- sid_harness/brave_proof.py +246 -0
- sid_harness/bridge.py +851 -0
- sid_harness/cli.py +937 -0
- sid_harness/client.py +161 -0
- sid_harness/companion.py +151 -0
- sid_harness/config.py +68 -0
- sid_harness/context/__init__.py +40 -0
- sid_harness/context/index.py +267 -0
- sid_harness/context/retrieval.py +74 -0
- sid_harness/editor.py +231 -0
- sid_harness/gateway.py +106 -0
- sid_harness/git_write.py +598 -0
- sid_harness/local_workspace.py +421 -0
- sid_harness/loop.py +394 -0
- sid_harness/lsp.py +598 -0
- sid_harness/policy/__init__.py +44 -0
- sid_harness/policy/repo_policy.py +626 -0
- sid_harness/receipt.py +142 -0
- sid_harness/runner.py +468 -0
- sid_harness/selftest.py +149 -0
- sid_harness/session/__init__.py +33 -0
- sid_harness/session/bounds.py +140 -0
- sid_harness/session/diff_gate.py +93 -0
- sid_harness/session/session.py +427 -0
- sid_harness/session/store.py +88 -0
- sid_harness/tools/__init__.py +52 -0
- sid_harness/tools/mcp_client.py +285 -0
- sid_harness/tools/plan.py +138 -0
- sid_harness/tools/registry.py +159 -0
- sid_harness/tools/skills.py +185 -0
- sid_harness/tools/subagents.py +112 -0
- sid_harness/tui.py +96 -0
- sid_harness/workspace.py +107 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/README.md +46 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/README.md +32 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/extension.js +395 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/package.json +120 -0
- sid_harness-0.3.0.dist-info/METADATA +229 -0
- sid_harness-0.3.0.dist-info/RECORD +45 -0
- sid_harness-0.3.0.dist-info/WHEEL +5 -0
- sid_harness-0.3.0.dist-info/entry_points.txt +2 -0
- sid_harness-0.3.0.dist-info/top_level.txt +1 -0
sid_harness/cli.py
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
"""``sid`` CLI entry point.
|
|
2
|
+
|
|
3
|
+
sid run --task "<goal>" --dir <path> [--model auto] [--verify] [--context]
|
|
4
|
+
sid session start|resume|replay
|
|
5
|
+
sid tools [--mcp CMD] [--json]
|
|
6
|
+
sid policy --dir <path> [--json]
|
|
7
|
+
|
|
8
|
+
One engine (the shared SID native loop), SID as the only model backend.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import getpass
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shlex
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import __version__, config
|
|
23
|
+
from .client import SIDClient, SIDClientError
|
|
24
|
+
from .local_workspace import LocalWorkspace, LocalWorkspaceError
|
|
25
|
+
from .runner import RunError, RunOutcome, execute_run
|
|
26
|
+
from .session import Session, SessionError, SessionStoreError
|
|
27
|
+
from .session.bounds import BoundsError, SessionBounds
|
|
28
|
+
from .workspace import WorkspaceError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _add_common_options(parser: argparse.ArgumentParser) -> None:
|
|
32
|
+
parser.add_argument("--base-url", default=None, help="SID /v1 base URL (env SID_BASE_URL).")
|
|
33
|
+
parser.add_argument("--api-key", default=None, help="Your SID key (env SID_API_KEY).")
|
|
34
|
+
parser.add_argument("--model", default="auto", help="Model id or 'auto' (default).")
|
|
35
|
+
parser.add_argument("--routing-profile", default=None, help="X-Routing-Profile (default: agent).")
|
|
36
|
+
parser.add_argument("--criteria", action="append", default=None, help="Acceptance criterion (repeatable).")
|
|
37
|
+
parser.add_argument("--out-of-scope", action="append", default=None, help="Out-of-scope item (repeatable).")
|
|
38
|
+
parser.add_argument("--max-tokens", type=int, default=8000)
|
|
39
|
+
parser.add_argument("--reasoning-effort", default=None)
|
|
40
|
+
parser.add_argument("--verify", action="store_true", help="Also review the produced diff via /v1/verify/patch.")
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--write",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Apply the loop's changed files to the real working tree (default: read-only).",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--allow-delete",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help=(
|
|
50
|
+
"Explicitly approve deletions the run proposes (default: deny). "
|
|
51
|
+
"Deletions are never applied silently."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--test-cmd",
|
|
56
|
+
default=None,
|
|
57
|
+
metavar="CMD",
|
|
58
|
+
help=(
|
|
59
|
+
"Human-approved test command to run locally in --dir (SUB-80). "
|
|
60
|
+
"Split into an argv list and executed without a shell, with a "
|
|
61
|
+
"bounded timeout and bounded output."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--receipt", default=None, help="Receipt JSON path (default: <dir>/.sid/runs/<run_id>.json).")
|
|
65
|
+
parser.add_argument("--json", action="store_true", help="Print the receipt JSON to stdout.")
|
|
66
|
+
parser.add_argument("--quiet", action="store_true")
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--context",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="Prepend repo-scale retrieved context for the task (SUB-55; read-only).",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--context-k",
|
|
74
|
+
type=int,
|
|
75
|
+
default=4,
|
|
76
|
+
help="Number of retrieved context chunks to prepend (default: 4).",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--mcp",
|
|
80
|
+
action="append",
|
|
81
|
+
default=None,
|
|
82
|
+
metavar="CMD",
|
|
83
|
+
help="Connect an MCP stdio server (e.g. --mcp 'npx -y @some/mcp') and expose its tools (repeatable).",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--bounds-config",
|
|
87
|
+
default=None,
|
|
88
|
+
help="JSON file overriding loop bounds (turns/tool calls/workspace size).",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--no-diagnostics",
|
|
92
|
+
dest="diagnostics",
|
|
93
|
+
action="store_false",
|
|
94
|
+
default=True,
|
|
95
|
+
help="Ignore the editor/LSP diagnostics feed for this run (SUB-96).",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _add_project_options(parser: argparse.ArgumentParser) -> None:
|
|
100
|
+
"""SUB-94: project rules/skills/subagents/plan on the CLI run paths."""
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--skills",
|
|
103
|
+
action="append",
|
|
104
|
+
default=None,
|
|
105
|
+
metavar="NAME",
|
|
106
|
+
help=(
|
|
107
|
+
"Load a named skill from the project's .opencode/skills (repeatable; "
|
|
108
|
+
"SUB-94). Without --skills, .sid/config 'skills' applies."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--skills-root",
|
|
113
|
+
default=None,
|
|
114
|
+
metavar="DIR",
|
|
115
|
+
help="Skills directory to load from (default: nearest .opencode/skills).",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--subagent",
|
|
119
|
+
action="append",
|
|
120
|
+
default=None,
|
|
121
|
+
metavar="TASK",
|
|
122
|
+
help=(
|
|
123
|
+
"Run TASK as a parallel subagent through the same loop and feed its "
|
|
124
|
+
"findings to the main run (repeatable; SUB-94)."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--plan",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help="Run read-only plan mode: propose a plan, write nothing (SUB-94).",
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--todo",
|
|
134
|
+
action="append",
|
|
135
|
+
default=None,
|
|
136
|
+
metavar="TEXT",
|
|
137
|
+
help="Seed the run's todo list (repeatable; SUB-94).",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _add_session_options(parser: argparse.ArgumentParser) -> None:
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--approve",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help="Approve the proposed diff for --write without prompting.",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _add_git_write_options(parser: argparse.ArgumentParser) -> None:
|
|
150
|
+
"""Explicit opt-in for the git write path (SUB-95). Default stays read-only."""
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--git-write",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help=(
|
|
155
|
+
"Opt in to the git write path: create a branch, commit the approved "
|
|
156
|
+
"diff, push the branch, and open a PR with gh. Never main/master, "
|
|
157
|
+
"never forced; each step needs its own approval."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--git-branch",
|
|
162
|
+
default=None,
|
|
163
|
+
help="Branch to create/commit/push (default: sid/<run_id>).",
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--git-message",
|
|
167
|
+
default=None,
|
|
168
|
+
help="Commit message and PR body (default: the task text).",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--git-base",
|
|
172
|
+
default=None,
|
|
173
|
+
help="Pull-request base branch (default: main). The PR target is not a push.",
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--no-pr",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="Push the branch but do not open a pull request.",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--approve",
|
|
182
|
+
action="store_true",
|
|
183
|
+
help=(
|
|
184
|
+
"Approve each pending step without prompting (diff apply and every "
|
|
185
|
+
"git-write step). Without it, a non-interactive run fails closed."
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
191
|
+
parser = argparse.ArgumentParser(
|
|
192
|
+
prog="sid",
|
|
193
|
+
description="SID first-party local coding harness (v1) — drives SID's native loop.",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument("--version", action="version", version=f"sid-harness {__version__}")
|
|
196
|
+
sub = parser.add_subparsers(dest="command")
|
|
197
|
+
|
|
198
|
+
setup = sub.add_parser(
|
|
199
|
+
"setup",
|
|
200
|
+
help="Verify your key with /v1/me, save it, and choose a default project dir.",
|
|
201
|
+
)
|
|
202
|
+
setup.add_argument("--api-key", default=None, help="Your SID key (env SID_API_KEY).")
|
|
203
|
+
setup.add_argument("--base-url", default=None, help="SID /v1 base URL (env SID_BASE_URL).")
|
|
204
|
+
setup.add_argument("--dir", default=None, help="Default project directory (default: current dir).")
|
|
205
|
+
setup.add_argument(
|
|
206
|
+
"--non-interactive",
|
|
207
|
+
action="store_true",
|
|
208
|
+
help="Never prompt; use flags, env, config, or the current directory.",
|
|
209
|
+
)
|
|
210
|
+
setup.add_argument("--json", action="store_true")
|
|
211
|
+
|
|
212
|
+
run = sub.add_parser("run", help="Run one bounded task against SID's native loop.")
|
|
213
|
+
run.add_argument("--task", required=True)
|
|
214
|
+
run.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
215
|
+
_add_common_options(run)
|
|
216
|
+
_add_project_options(run)
|
|
217
|
+
_add_git_write_options(run)
|
|
218
|
+
|
|
219
|
+
tui = sub.add_parser("tui", help="Interactive terminal loop over the same engine.")
|
|
220
|
+
tui.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
221
|
+
_add_common_options(tui)
|
|
222
|
+
_add_project_options(tui)
|
|
223
|
+
_add_git_write_options(tui)
|
|
224
|
+
|
|
225
|
+
status = sub.add_parser(
|
|
226
|
+
"status",
|
|
227
|
+
help="Show local git/worktree status for --dir (read-only, no model call).",
|
|
228
|
+
)
|
|
229
|
+
status.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
230
|
+
status.add_argument("--diff", action="store_true", help="Also print `git diff`.")
|
|
231
|
+
status.add_argument("--json", action="store_true")
|
|
232
|
+
|
|
233
|
+
session = sub.add_parser(
|
|
234
|
+
"session",
|
|
235
|
+
help="Durable local sessions: start, resume, replay (approve-before-apply on --write).",
|
|
236
|
+
)
|
|
237
|
+
session_sub = session.add_subparsers(dest="session_command")
|
|
238
|
+
|
|
239
|
+
session_start = session_sub.add_parser("start", help="Start a durable session and run once.")
|
|
240
|
+
session_start.add_argument("--task", required=True)
|
|
241
|
+
session_start.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
242
|
+
_add_common_options(session_start)
|
|
243
|
+
_add_session_options(session_start)
|
|
244
|
+
|
|
245
|
+
session_resume = session_sub.add_parser("resume", help="Continue an existing session.")
|
|
246
|
+
session_resume.add_argument("--session", required=True)
|
|
247
|
+
session_resume.add_argument("--task", default=None, help="New task (default: the session's task).")
|
|
248
|
+
session_resume.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
249
|
+
_add_common_options(session_resume)
|
|
250
|
+
_add_session_options(session_resume)
|
|
251
|
+
|
|
252
|
+
session_replay = session_sub.add_parser("replay", help="Print a session's run receipts (no model call).")
|
|
253
|
+
session_replay.add_argument("--session", required=True)
|
|
254
|
+
session_replay.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
255
|
+
session_replay.add_argument("--json", action="store_true")
|
|
256
|
+
|
|
257
|
+
tools = sub.add_parser(
|
|
258
|
+
"tools",
|
|
259
|
+
help="List the loop's tool contract (base + optional MCP servers).",
|
|
260
|
+
)
|
|
261
|
+
tools.add_argument("--mcp", action="append", default=None, metavar="CMD")
|
|
262
|
+
tools.add_argument("--json", action="store_true")
|
|
263
|
+
|
|
264
|
+
policy = sub.add_parser(
|
|
265
|
+
"policy",
|
|
266
|
+
help="Show the platform worktree/gate policy for --dir (read-only, no model call).",
|
|
267
|
+
)
|
|
268
|
+
policy.add_argument("--dir", default=None, help="Project directory (default: `sid setup`'s dir).")
|
|
269
|
+
policy.add_argument("--json", action="store_true")
|
|
270
|
+
|
|
271
|
+
selftest = sub.add_parser(
|
|
272
|
+
"selftest",
|
|
273
|
+
help="Drive one loop turn against a local stub endpoint and write a receipt.",
|
|
274
|
+
)
|
|
275
|
+
selftest.add_argument("--receipt", default=None)
|
|
276
|
+
|
|
277
|
+
bridge = sub.add_parser(
|
|
278
|
+
"bridge",
|
|
279
|
+
help="Run the localhost filesystem bridge so the Studio web UI can read --dir.",
|
|
280
|
+
)
|
|
281
|
+
_add_bridge_options(bridge, auto_open=False)
|
|
282
|
+
|
|
283
|
+
companion = sub.add_parser(
|
|
284
|
+
"companion",
|
|
285
|
+
help="Start the bridge and open the Studio with one-click pairing (read-only by default).",
|
|
286
|
+
)
|
|
287
|
+
_add_bridge_options(companion, auto_open=True)
|
|
288
|
+
|
|
289
|
+
lsp = sub.add_parser(
|
|
290
|
+
"lsp",
|
|
291
|
+
help="Run the SID language server on stdio; it feeds diagnostics into the loop (SUB-96).",
|
|
292
|
+
)
|
|
293
|
+
lsp.add_argument(
|
|
294
|
+
"--dir",
|
|
295
|
+
default=None,
|
|
296
|
+
help="Workspace root (default: the initialize request's root, else cwd).",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
editor = sub.add_parser(
|
|
300
|
+
"editor",
|
|
301
|
+
help="Install the bundled VS Code extension (SUB-96).",
|
|
302
|
+
)
|
|
303
|
+
editor_sub = editor.add_subparsers(dest="editor_command")
|
|
304
|
+
editor_install = editor_sub.add_parser(
|
|
305
|
+
"install",
|
|
306
|
+
help="Copy the bundled VS Code extension into VS Code's extensions directory.",
|
|
307
|
+
)
|
|
308
|
+
editor_install.add_argument("--target-dir", default=None, help="Override the extensions directory.")
|
|
309
|
+
editor_install.add_argument("--force", action="store_true", help="Refresh an existing install.")
|
|
310
|
+
editor_install.add_argument("--json", action="store_true")
|
|
311
|
+
|
|
312
|
+
apply_cmd = sub.add_parser(
|
|
313
|
+
"apply",
|
|
314
|
+
help="Apply an approved run receipt's readback to --dir through the same write path.",
|
|
315
|
+
)
|
|
316
|
+
apply_cmd.add_argument("--dir", required=True)
|
|
317
|
+
apply_cmd.add_argument("--receipt", required=True)
|
|
318
|
+
apply_cmd.add_argument(
|
|
319
|
+
"--approve",
|
|
320
|
+
action="store_true",
|
|
321
|
+
help="Explicit human approval; without it nothing is written.",
|
|
322
|
+
)
|
|
323
|
+
apply_cmd.add_argument("--json", action="store_true")
|
|
324
|
+
return parser
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _add_bridge_options(parser: argparse.ArgumentParser, *, auto_open: bool) -> None:
|
|
328
|
+
parser.add_argument("--dir", required=True)
|
|
329
|
+
parser.add_argument("--port", type=int, default=8787)
|
|
330
|
+
parser.add_argument("--token", default=None, help="Fixed token (default: generated per launch).")
|
|
331
|
+
parser.add_argument("--pair-code", default=None, help="Fixed pairing code (default: generated per launch).")
|
|
332
|
+
parser.add_argument("--pair-ttl", type=float, default=600)
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
"--write",
|
|
335
|
+
action="store_true",
|
|
336
|
+
help="Make write mode available (default: read-only); the user still grants it explicitly in the Studio.",
|
|
337
|
+
)
|
|
338
|
+
parser.add_argument("--state-dir", default=None, help="Receipts/grant state directory (default: ~/.sid/bridge).")
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--studio-url",
|
|
341
|
+
default=None,
|
|
342
|
+
help="Studio page that opens (companion default: https://app.superintelligencedog.com/studio.html).",
|
|
343
|
+
)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--allow-origin",
|
|
346
|
+
action="append",
|
|
347
|
+
default=None,
|
|
348
|
+
metavar="ORIGIN",
|
|
349
|
+
help="Add a trusted Studio origin (self-hosted); repeatable.",
|
|
350
|
+
)
|
|
351
|
+
parser.add_argument("--base-url", default=None, help="SID /v1 base URL used for runs (never taken from the browser).")
|
|
352
|
+
if auto_open:
|
|
353
|
+
parser.add_argument("--no-open", action="store_true", help="Do not open the browser (print the pair URL).")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _file_link(path: Path) -> str:
|
|
357
|
+
resolved = Path(path).resolve()
|
|
358
|
+
try:
|
|
359
|
+
return resolved.as_uri()
|
|
360
|
+
except ValueError: # pragma: no cover - unusual non-file path
|
|
361
|
+
return str(resolved)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _render(outcome: RunOutcome, *, as_json: bool, quiet: bool) -> None:
|
|
365
|
+
receipt = outcome.receipt
|
|
366
|
+
if as_json:
|
|
367
|
+
print(json.dumps(receipt, indent=2, sort_keys=True, ensure_ascii=False))
|
|
368
|
+
return
|
|
369
|
+
if not quiet:
|
|
370
|
+
print(f"model: {receipt['model']} steps: {receipt['steps']} tool_calls: {receipt['tool_calls']}")
|
|
371
|
+
print(f"changed: {', '.join(receipt['changed']) or '(none)'}")
|
|
372
|
+
if receipt.get("context"):
|
|
373
|
+
print(f"context: {receipt['context'].get('chunks_used', 0)} chunk(s)")
|
|
374
|
+
if receipt.get("diff"):
|
|
375
|
+
print("\n--- diff ---")
|
|
376
|
+
print(receipt["diff"].rstrip("\n"))
|
|
377
|
+
print(f"\n{receipt['final_response']}")
|
|
378
|
+
if receipt.get("verify"):
|
|
379
|
+
print(f"\nverify: {receipt['verify'].get('verdict')}")
|
|
380
|
+
if receipt["tests_executed"]:
|
|
381
|
+
print(
|
|
382
|
+
f"tests_executed: true backend: {receipt.get('test_backend')} "
|
|
383
|
+
f"exit: {receipt.get('test_exit_code')} passed: {receipt.get('test_passed')}"
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
print(f"tests_executed: false ({receipt.get('test', {}).get('detail')})")
|
|
387
|
+
git_write = receipt.get("git_write")
|
|
388
|
+
if git_write:
|
|
389
|
+
print(
|
|
390
|
+
f"git_write: {git_write.get('status')} branch: {git_write.get('branch')} "
|
|
391
|
+
f"commit: {git_write.get('commit')} pr: {git_write.get('pr_url')}"
|
|
392
|
+
)
|
|
393
|
+
if git_write.get("reason"):
|
|
394
|
+
print(f"git_write detail: {git_write['reason']}")
|
|
395
|
+
print(f"receipt: {_file_link(outcome.receipt_path)}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _make_client(args: argparse.Namespace) -> SIDClient:
|
|
399
|
+
return SIDClient(
|
|
400
|
+
base_url=getattr(args, "base_url", None),
|
|
401
|
+
api_key=getattr(args, "api_key", None),
|
|
402
|
+
routing_profile=getattr(args, "routing_profile", None),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _cmd_setup(args: argparse.Namespace) -> int:
|
|
407
|
+
"""Verify the key against ``/v1/me``, then persist config + default dir."""
|
|
408
|
+
saved = config.load_config()
|
|
409
|
+
api_key = (
|
|
410
|
+
args.api_key
|
|
411
|
+
or os.environ.get("SID_API_KEY")
|
|
412
|
+
or (saved.get("api_key") if isinstance(saved.get("api_key"), str) else "")
|
|
413
|
+
)
|
|
414
|
+
if not api_key and not args.non_interactive and sys.stdin.isatty():
|
|
415
|
+
api_key = getpass.getpass("SID API key: ").strip()
|
|
416
|
+
if not api_key:
|
|
417
|
+
print(
|
|
418
|
+
"BLOCKED: no SID API key. Pass --api-key, set SID_API_KEY, or run "
|
|
419
|
+
"interactively.",
|
|
420
|
+
file=sys.stderr,
|
|
421
|
+
)
|
|
422
|
+
return 2
|
|
423
|
+
|
|
424
|
+
base_url = args.base_url or os.environ.get("SID_BASE_URL") or None
|
|
425
|
+
try:
|
|
426
|
+
client = SIDClient(base_url=base_url, api_key=api_key)
|
|
427
|
+
account = client.me()
|
|
428
|
+
except SIDClientError as exc:
|
|
429
|
+
print(f"BLOCKED: key verification failed: {exc}", file=sys.stderr)
|
|
430
|
+
return 2
|
|
431
|
+
|
|
432
|
+
default_dir = args.dir
|
|
433
|
+
if not default_dir:
|
|
434
|
+
existing = saved.get("default_dir")
|
|
435
|
+
default_dir = existing if isinstance(existing, str) and existing else None
|
|
436
|
+
if not default_dir and not args.non_interactive and sys.stdin.isatty():
|
|
437
|
+
answer = input(f"Default project directory [{Path.cwd()}]: ").strip()
|
|
438
|
+
default_dir = answer or str(Path.cwd())
|
|
439
|
+
if not default_dir:
|
|
440
|
+
default_dir = str(Path.cwd())
|
|
441
|
+
directory = Path(default_dir).expanduser().resolve()
|
|
442
|
+
if not directory.is_dir():
|
|
443
|
+
print(f"BLOCKED: default dir is not an existing directory: {directory}", file=sys.stderr)
|
|
444
|
+
return 2
|
|
445
|
+
|
|
446
|
+
merged = dict(saved)
|
|
447
|
+
merged.update(
|
|
448
|
+
{
|
|
449
|
+
"api_key": api_key,
|
|
450
|
+
"base_url": client.base_url,
|
|
451
|
+
"default_dir": str(directory),
|
|
452
|
+
"api_key_id": account.get("api_key_id"),
|
|
453
|
+
"plan": account.get("plan"),
|
|
454
|
+
"verified_at": datetime.now(timezone.utc).isoformat(),
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
path = config.save_config(merged)
|
|
458
|
+
payload = {
|
|
459
|
+
"ok": True,
|
|
460
|
+
"config": str(path),
|
|
461
|
+
"base_url": client.base_url,
|
|
462
|
+
"default_dir": str(directory),
|
|
463
|
+
"api_key_id": account.get("api_key_id"),
|
|
464
|
+
"plan": account.get("plan"),
|
|
465
|
+
}
|
|
466
|
+
if args.json:
|
|
467
|
+
print(json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False))
|
|
468
|
+
return 0
|
|
469
|
+
print(f"key verified: {account.get('name') or 'SID key'} (plan: {account.get('plan') or 'unknown'})")
|
|
470
|
+
print(f"saved: {path}")
|
|
471
|
+
print(f"default dir: {directory}")
|
|
472
|
+
print("next: sid run --task \"...\"")
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _apply_default_dir(args: argparse.Namespace) -> bool:
|
|
477
|
+
if getattr(args, "dir", None):
|
|
478
|
+
return True
|
|
479
|
+
configured = config.get_default_dir()
|
|
480
|
+
if configured:
|
|
481
|
+
args.dir = configured
|
|
482
|
+
return True
|
|
483
|
+
print(
|
|
484
|
+
"BLOCKED: no --dir given and no default dir saved. "
|
|
485
|
+
"Run `sid setup --dir <path>` first.",
|
|
486
|
+
file=sys.stderr,
|
|
487
|
+
)
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _resolve_bounds(args: argparse.Namespace) -> SessionBounds:
|
|
492
|
+
return SessionBounds.resolve(config_path=getattr(args, "bounds_config", None))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _split_command(command: str) -> list[str]:
|
|
496
|
+
"""Split an ``--mcp`` command line, Windows-path aware.
|
|
497
|
+
|
|
498
|
+
POSIX ``shlex.split`` eats backslashes, which corrupts a Windows executable
|
|
499
|
+
path (``C:\\Python\\python.exe``). On Windows, tokenize without POSIX rules
|
|
500
|
+
and strip surrounding quotes from each token.
|
|
501
|
+
"""
|
|
502
|
+
command = (command or "").strip()
|
|
503
|
+
if not command:
|
|
504
|
+
return []
|
|
505
|
+
lexer = shlex.shlex(command, posix=(os.name != "nt"))
|
|
506
|
+
lexer.whitespace_split = True
|
|
507
|
+
lexer.commenters = ""
|
|
508
|
+
tokens = list(lexer)
|
|
509
|
+
if os.name == "nt":
|
|
510
|
+
tokens = [
|
|
511
|
+
token[1:-1]
|
|
512
|
+
if len(token) >= 2 and token[0] == token[-1] and token[0] in ("'", '"')
|
|
513
|
+
else token
|
|
514
|
+
for token in tokens
|
|
515
|
+
]
|
|
516
|
+
return tokens
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _approved_test_argv(args: argparse.Namespace) -> list[str] | None:
|
|
520
|
+
"""The ``--test-cmd`` command as an argv LIST (never a shell string)."""
|
|
521
|
+
command = getattr(args, "test_cmd", None)
|
|
522
|
+
if not command:
|
|
523
|
+
return None
|
|
524
|
+
tokens = _split_command(command)
|
|
525
|
+
return tokens or None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _build_registry(args: argparse.Namespace):
|
|
529
|
+
"""Return (registry, closers) for any --mcp servers; (None, []) when none."""
|
|
530
|
+
commands = getattr(args, "mcp", None)
|
|
531
|
+
if not commands:
|
|
532
|
+
return None, []
|
|
533
|
+
from .tools import MCPClient, StdioTransport, ToolRegistry, register_mcp_tools
|
|
534
|
+
|
|
535
|
+
registry = ToolRegistry()
|
|
536
|
+
closers = []
|
|
537
|
+
for command in commands:
|
|
538
|
+
parts = _split_command(command)
|
|
539
|
+
if not parts:
|
|
540
|
+
continue
|
|
541
|
+
mcp = MCPClient(StdioTransport(parts[0], parts[1:]))
|
|
542
|
+
mcp.initialize()
|
|
543
|
+
register_mcp_tools(registry, mcp)
|
|
544
|
+
closers.append(mcp)
|
|
545
|
+
return registry, closers
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _git_write_approver(args: argparse.Namespace):
|
|
549
|
+
"""One approval per git step; fail closed without a TTY and without --approve."""
|
|
550
|
+
explicit = bool(getattr(args, "approve", False))
|
|
551
|
+
|
|
552
|
+
def approve(step):
|
|
553
|
+
print(f"[git-write] {step.render()}", file=sys.stderr)
|
|
554
|
+
if explicit:
|
|
555
|
+
return True
|
|
556
|
+
if not sys.stdin.isatty():
|
|
557
|
+
print(
|
|
558
|
+
"BLOCKED: --git-write needs an interactive approval (or --approve) "
|
|
559
|
+
"and stdin is not a terminal; no git step was taken.",
|
|
560
|
+
file=sys.stderr,
|
|
561
|
+
)
|
|
562
|
+
return False
|
|
563
|
+
print(f"Approve {step.name}? [y/N] ", end="", file=sys.stderr, flush=True)
|
|
564
|
+
try:
|
|
565
|
+
answer = input()
|
|
566
|
+
except (EOFError, KeyboardInterrupt):
|
|
567
|
+
return False
|
|
568
|
+
return answer.strip().lower() in ("y", "yes")
|
|
569
|
+
|
|
570
|
+
return approve
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _run_common(args: argparse.Namespace, task: str, *, reporter) -> RunOutcome:
|
|
574
|
+
client = _make_client(args)
|
|
575
|
+
bounds = _resolve_bounds(args)
|
|
576
|
+
registry, closers = _build_registry(args)
|
|
577
|
+
git_write = bool(getattr(args, "git_write", False))
|
|
578
|
+
try:
|
|
579
|
+
return execute_run(
|
|
580
|
+
task=task,
|
|
581
|
+
directory=args.dir,
|
|
582
|
+
client=client,
|
|
583
|
+
model=args.model,
|
|
584
|
+
criteria=args.criteria,
|
|
585
|
+
out_of_scope=args.out_of_scope,
|
|
586
|
+
test_argv=_approved_test_argv(args),
|
|
587
|
+
max_tokens=args.max_tokens,
|
|
588
|
+
reasoning_effort=args.reasoning_effort,
|
|
589
|
+
verify=args.verify,
|
|
590
|
+
write=getattr(args, "write", False),
|
|
591
|
+
allow_delete=getattr(args, "allow_delete", False),
|
|
592
|
+
git_write=git_write,
|
|
593
|
+
git_branch=getattr(args, "git_branch", None),
|
|
594
|
+
git_message=getattr(args, "git_message", None),
|
|
595
|
+
git_base=getattr(args, "git_base", None),
|
|
596
|
+
git_pr=not getattr(args, "no_pr", False),
|
|
597
|
+
git_approve=_git_write_approver(args) if git_write else None,
|
|
598
|
+
receipt_path=args.receipt,
|
|
599
|
+
reporter=reporter,
|
|
600
|
+
context=getattr(args, "context", False),
|
|
601
|
+
context_k=getattr(args, "context_k", 4),
|
|
602
|
+
registry=registry,
|
|
603
|
+
bounds=bounds,
|
|
604
|
+
diagnostics=getattr(args, "diagnostics", True),
|
|
605
|
+
skills=getattr(args, "skills", None),
|
|
606
|
+
skills_root=getattr(args, "skills_root", None),
|
|
607
|
+
subagents=getattr(args, "subagent", None),
|
|
608
|
+
plan=bool(getattr(args, "plan", False)),
|
|
609
|
+
todos=getattr(args, "todo", None),
|
|
610
|
+
)
|
|
611
|
+
finally:
|
|
612
|
+
for mcp in closers:
|
|
613
|
+
mcp.close()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
617
|
+
reporter = (lambda _message: None) if args.quiet else (lambda message: print(message, file=sys.stderr))
|
|
618
|
+
try:
|
|
619
|
+
outcome = _run_common(args, args.task, reporter=reporter)
|
|
620
|
+
except (SIDClientError, WorkspaceError, LocalWorkspaceError, BoundsError, RunError) as exc:
|
|
621
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
622
|
+
return 2
|
|
623
|
+
_render(outcome, as_json=args.json, quiet=args.quiet)
|
|
624
|
+
return 0
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _cmd_status(args: argparse.Namespace) -> int:
|
|
628
|
+
try:
|
|
629
|
+
workspace = LocalWorkspace.open(args.dir)
|
|
630
|
+
except LocalWorkspaceError as exc:
|
|
631
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
632
|
+
return 2
|
|
633
|
+
info = workspace.git
|
|
634
|
+
status = workspace.status()
|
|
635
|
+
if args.json:
|
|
636
|
+
payload = {"git": info.as_dict(), "status": status}
|
|
637
|
+
if args.diff:
|
|
638
|
+
payload["diff"] = workspace.diff()
|
|
639
|
+
print(json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False))
|
|
640
|
+
return 0
|
|
641
|
+
print(f"dir: {workspace.root}")
|
|
642
|
+
if not info.is_repo:
|
|
643
|
+
print("git: not a repository")
|
|
644
|
+
return 0
|
|
645
|
+
kind = "worktree" if info.is_worktree else "checkout"
|
|
646
|
+
print(f"git: {kind} branch: {info.branch or '(detached)'} head: {(info.head or '')[:12]}")
|
|
647
|
+
print(f"root: {info.root}")
|
|
648
|
+
if status:
|
|
649
|
+
print(f"status ({len(status)}):")
|
|
650
|
+
for line in status:
|
|
651
|
+
print(f" {line}")
|
|
652
|
+
else:
|
|
653
|
+
print("status: clean")
|
|
654
|
+
if args.diff:
|
|
655
|
+
print("\n--- diff ---")
|
|
656
|
+
print(workspace.diff().rstrip("\n"))
|
|
657
|
+
return 0
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _cmd_tools(args: argparse.Namespace) -> int:
|
|
661
|
+
from .tools import MCPClient, StdioTransport, ToolRegistry, register_mcp_tools
|
|
662
|
+
from .tools.registry import BASE_TOOL_NAMES
|
|
663
|
+
|
|
664
|
+
registry = ToolRegistry()
|
|
665
|
+
closers = []
|
|
666
|
+
try:
|
|
667
|
+
for command in args.mcp or []:
|
|
668
|
+
parts = _split_command(command)
|
|
669
|
+
if not parts:
|
|
670
|
+
continue
|
|
671
|
+
mcp = MCPClient(StdioTransport(parts[0], parts[1:]))
|
|
672
|
+
mcp.initialize()
|
|
673
|
+
register_mcp_tools(registry, mcp)
|
|
674
|
+
closers.append(mcp)
|
|
675
|
+
except Exception as exc:
|
|
676
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
677
|
+
return 2
|
|
678
|
+
finally:
|
|
679
|
+
for mcp in closers:
|
|
680
|
+
mcp.close()
|
|
681
|
+
|
|
682
|
+
payload = {
|
|
683
|
+
"base": list(BASE_TOOL_NAMES),
|
|
684
|
+
"extensions": registry.extension_names(),
|
|
685
|
+
"all": registry.names(),
|
|
686
|
+
}
|
|
687
|
+
if args.json:
|
|
688
|
+
print(json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False))
|
|
689
|
+
return 0
|
|
690
|
+
print(f"base tools ({len(payload['base'])}): {', '.join(payload['base'])}")
|
|
691
|
+
if payload["extensions"]:
|
|
692
|
+
print(f"extension tools ({len(payload['extensions'])}): {', '.join(payload['extensions'])}")
|
|
693
|
+
else:
|
|
694
|
+
print("extension tools: none (pass --mcp '<command>' to add a server's tools)")
|
|
695
|
+
return 0
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _cmd_policy(args: argparse.Namespace) -> int:
|
|
699
|
+
from .policy import policy_report
|
|
700
|
+
|
|
701
|
+
report = policy_report(args.dir)
|
|
702
|
+
if args.json:
|
|
703
|
+
print(json.dumps(report, indent=2, sort_keys=True, ensure_ascii=False))
|
|
704
|
+
return 0
|
|
705
|
+
worktree = report["worktree"]
|
|
706
|
+
gates = report["gates"]
|
|
707
|
+
print(f"dir: {worktree['directory']}")
|
|
708
|
+
if not worktree["is_repo"]:
|
|
709
|
+
print("policy: not a git repository (no platform policy applies)")
|
|
710
|
+
return 0
|
|
711
|
+
kind = "MAIN checkout (integration-only)" if worktree["is_main_checkout"] else "session worktree"
|
|
712
|
+
print(f"worktree: {kind} branch: {worktree['branch'] or '(detached)'}")
|
|
713
|
+
print(f"reason: {worktree['reason']}")
|
|
714
|
+
print(f"gates on disk: {len(gates['gate_files'])} wired: {len(gates['wired_gates'])}")
|
|
715
|
+
if gates["missing_wired"]:
|
|
716
|
+
print(f"missing wired gates: {', '.join(gates['missing_wired'])}")
|
|
717
|
+
return 0
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _session_approver(explicit: bool):
|
|
721
|
+
def approve(proposal):
|
|
722
|
+
print(proposal.render())
|
|
723
|
+
if explicit:
|
|
724
|
+
return True
|
|
725
|
+
if not sys.stdin.isatty():
|
|
726
|
+
print(
|
|
727
|
+
"BLOCKED: --write without --approve and stdin is not a terminal; "
|
|
728
|
+
"changes were not applied.",
|
|
729
|
+
file=sys.stderr,
|
|
730
|
+
)
|
|
731
|
+
return False
|
|
732
|
+
answer = input("Apply these changes to the working tree? [y/N] ").strip().lower()
|
|
733
|
+
return answer in ("y", "yes")
|
|
734
|
+
|
|
735
|
+
return approve
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _render_session(outcome, *, as_json: bool, quiet: bool) -> None:
|
|
739
|
+
receipt = outcome.receipt
|
|
740
|
+
if as_json:
|
|
741
|
+
print(json.dumps(receipt, indent=2, sort_keys=True, ensure_ascii=False))
|
|
742
|
+
return
|
|
743
|
+
if not quiet:
|
|
744
|
+
print(f"session: {receipt['session']['session_id']} status: {receipt['status']}")
|
|
745
|
+
print(f"model: {receipt['model']} steps: {receipt['steps']} tool_calls: {receipt['tool_calls']}")
|
|
746
|
+
print(f"changed: {', '.join(receipt['changed']) or '(none)'}")
|
|
747
|
+
decision = "approved" if outcome.review.approved else "refused"
|
|
748
|
+
print(f"apply: {decision} ({outcome.review.reason})")
|
|
749
|
+
print(f"\n{receipt['final_response']}")
|
|
750
|
+
print(f"receipt: {_file_link(outcome.receipt_path)}")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _cmd_session(args: argparse.Namespace) -> int:
|
|
754
|
+
command = getattr(args, "session_command", None)
|
|
755
|
+
if command is None:
|
|
756
|
+
print("BLOCKED: choose a session subcommand: start | resume | replay", file=sys.stderr)
|
|
757
|
+
return 2
|
|
758
|
+
try:
|
|
759
|
+
if command == "replay":
|
|
760
|
+
session = Session.load(args.dir, args.session)
|
|
761
|
+
runs = session.replay()
|
|
762
|
+
if args.json:
|
|
763
|
+
print(json.dumps(runs, indent=2, sort_keys=True, ensure_ascii=False))
|
|
764
|
+
else:
|
|
765
|
+
print(
|
|
766
|
+
f"session: {session.session_id} status: {session.record['status']} "
|
|
767
|
+
f"runs: {len(runs)}"
|
|
768
|
+
)
|
|
769
|
+
for index, run in enumerate(runs):
|
|
770
|
+
changed = ", ".join(run.get("changed") or []) or "(none)"
|
|
771
|
+
print(f" [{index}] {run['run_id']} status={run.get('status')} changed={changed}")
|
|
772
|
+
return 0
|
|
773
|
+
|
|
774
|
+
bounds = _resolve_bounds(args)
|
|
775
|
+
client = _make_client(args)
|
|
776
|
+
if command == "start":
|
|
777
|
+
session = Session.start(args.dir, task=args.task, bounds=bounds)
|
|
778
|
+
task = None
|
|
779
|
+
else:
|
|
780
|
+
session = Session.resume(args.dir, args.session, bounds=bounds)
|
|
781
|
+
task = args.task
|
|
782
|
+
reporter = (
|
|
783
|
+
(lambda _message: None)
|
|
784
|
+
if args.quiet
|
|
785
|
+
else (lambda message: print(message, file=sys.stderr))
|
|
786
|
+
)
|
|
787
|
+
outcome = session.run(
|
|
788
|
+
client=client,
|
|
789
|
+
task=task,
|
|
790
|
+
criteria=args.criteria,
|
|
791
|
+
out_of_scope=args.out_of_scope,
|
|
792
|
+
test_argv=_approved_test_argv(args),
|
|
793
|
+
model=args.model,
|
|
794
|
+
max_tokens=args.max_tokens,
|
|
795
|
+
reasoning_effort=args.reasoning_effort,
|
|
796
|
+
verify=args.verify,
|
|
797
|
+
write=args.write,
|
|
798
|
+
allow_delete=getattr(args, "allow_delete", False),
|
|
799
|
+
approve=_session_approver(args.approve) if args.write else None,
|
|
800
|
+
reporter=reporter,
|
|
801
|
+
diagnostics=getattr(args, "diagnostics", True),
|
|
802
|
+
)
|
|
803
|
+
except (
|
|
804
|
+
SIDClientError,
|
|
805
|
+
WorkspaceError,
|
|
806
|
+
LocalWorkspaceError,
|
|
807
|
+
SessionError,
|
|
808
|
+
SessionStoreError,
|
|
809
|
+
BoundsError,
|
|
810
|
+
) as exc:
|
|
811
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
812
|
+
return 2
|
|
813
|
+
_render_session(outcome, as_json=args.json, quiet=args.quiet)
|
|
814
|
+
return 0
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _cmd_editor(args: argparse.Namespace) -> int:
|
|
818
|
+
if getattr(args, "editor_command", None) != "install":
|
|
819
|
+
print("BLOCKED: choose an editor subcommand: install", file=sys.stderr)
|
|
820
|
+
return 2
|
|
821
|
+
from .editor import EditorError, install
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
result = install(args.target_dir, force=args.force)
|
|
825
|
+
except EditorError as exc:
|
|
826
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
827
|
+
return 2
|
|
828
|
+
if args.json:
|
|
829
|
+
print(json.dumps(result, indent=2, sort_keys=True, ensure_ascii=False))
|
|
830
|
+
return 0
|
|
831
|
+
print(f"extension {result['status']}: {result['extension']}")
|
|
832
|
+
print("reload VS Code to activate the SID commands.")
|
|
833
|
+
return 0
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _cmd_apply(args: argparse.Namespace) -> int:
|
|
837
|
+
from .editor import EditorError, apply_receipt
|
|
838
|
+
|
|
839
|
+
try:
|
|
840
|
+
result = apply_receipt(args.dir, args.receipt, approve=args.approve)
|
|
841
|
+
except (EditorError, LocalWorkspaceError) as exc:
|
|
842
|
+
print(f"BLOCKED: {exc}", file=sys.stderr)
|
|
843
|
+
return 2
|
|
844
|
+
if args.json:
|
|
845
|
+
print(json.dumps(result, indent=2, sort_keys=True, ensure_ascii=False))
|
|
846
|
+
elif result["status"] == "applied":
|
|
847
|
+
print(f"applied: {', '.join(result['applied'])}")
|
|
848
|
+
elif result["status"] == "nothing-to-apply":
|
|
849
|
+
print("nothing to apply: the receipt carries no changed files")
|
|
850
|
+
else:
|
|
851
|
+
print(f"{result['status']}: no files written")
|
|
852
|
+
return 2 if result["status"] in ("approval-required", "refused-by-policy") else 0
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def main(argv: list[str] | None = None) -> int:
|
|
856
|
+
parser = build_parser()
|
|
857
|
+
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
|
858
|
+
if args.command == "setup":
|
|
859
|
+
return _cmd_setup(args)
|
|
860
|
+
if args.command in {"run", "tui", "status", "session", "policy"} and not _apply_default_dir(args):
|
|
861
|
+
return 2
|
|
862
|
+
if args.command == "run":
|
|
863
|
+
return _cmd_run(args)
|
|
864
|
+
if args.command == "tui":
|
|
865
|
+
from .tui import run_tui
|
|
866
|
+
|
|
867
|
+
return run_tui(args)
|
|
868
|
+
if args.command == "status":
|
|
869
|
+
return _cmd_status(args)
|
|
870
|
+
if args.command == "session":
|
|
871
|
+
return _cmd_session(args)
|
|
872
|
+
if args.command == "tools":
|
|
873
|
+
return _cmd_tools(args)
|
|
874
|
+
if args.command == "policy":
|
|
875
|
+
return _cmd_policy(args)
|
|
876
|
+
if args.command == "selftest":
|
|
877
|
+
from .selftest import run_selftest
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
outcome = run_selftest(receipt_path=args.receipt)
|
|
881
|
+
except Exception as exc: # selftest is a diagnostic; report and fail closed
|
|
882
|
+
print(f"selftest FAILED: {exc}", file=sys.stderr)
|
|
883
|
+
return 1
|
|
884
|
+
print(f"selftest OK — receipt: {_file_link(outcome.receipt_path)}")
|
|
885
|
+
return 0
|
|
886
|
+
if args.command == "bridge":
|
|
887
|
+
from .bridge import ALLOWED_ORIGINS, main as bridge_main
|
|
888
|
+
|
|
889
|
+
argv = ["--dir", args.dir, "--port", str(args.port), "--pair-ttl", str(args.pair_ttl)]
|
|
890
|
+
if args.token:
|
|
891
|
+
argv += ["--token", args.token]
|
|
892
|
+
if args.pair_code:
|
|
893
|
+
argv += ["--pair-code", args.pair_code]
|
|
894
|
+
if args.write:
|
|
895
|
+
argv.append("--write")
|
|
896
|
+
if args.state_dir:
|
|
897
|
+
argv += ["--state-dir", args.state_dir]
|
|
898
|
+
if args.studio_url:
|
|
899
|
+
argv += ["--studio-url", args.studio_url]
|
|
900
|
+
else:
|
|
901
|
+
argv += ["--studio-url", "https://app.superintelligencedog.com/studio.html"]
|
|
902
|
+
for origin in args.allow_origin or []:
|
|
903
|
+
if origin not in ALLOWED_ORIGINS:
|
|
904
|
+
argv += ["--allow-origin", origin]
|
|
905
|
+
if args.base_url:
|
|
906
|
+
argv += ["--base-url", args.base_url]
|
|
907
|
+
return bridge_main(argv)
|
|
908
|
+
if args.command == "companion":
|
|
909
|
+
from .companion import DEFAULT_STUDIO_URL, main as companion_main
|
|
910
|
+
|
|
911
|
+
argv = ["--dir", args.dir, "--port", str(args.port), "--pair-ttl", str(args.pair_ttl)]
|
|
912
|
+
argv += ["--studio-url", args.studio_url or DEFAULT_STUDIO_URL]
|
|
913
|
+
if args.token:
|
|
914
|
+
argv += ["--token", args.token]
|
|
915
|
+
if args.pair_code:
|
|
916
|
+
argv += ["--pair-code", args.pair_code]
|
|
917
|
+
if args.write:
|
|
918
|
+
argv.append("--write")
|
|
919
|
+
if args.state_dir:
|
|
920
|
+
argv += ["--state-dir", args.state_dir]
|
|
921
|
+
if args.no_open:
|
|
922
|
+
argv.append("--no-open")
|
|
923
|
+
for origin in args.allow_origin or []:
|
|
924
|
+
argv += ["--allow-origin", origin]
|
|
925
|
+
if args.base_url:
|
|
926
|
+
argv += ["--base-url", args.base_url]
|
|
927
|
+
return companion_main(argv)
|
|
928
|
+
if args.command == "lsp":
|
|
929
|
+
from .lsp import serve_stdio
|
|
930
|
+
|
|
931
|
+
return serve_stdio(args.dir)
|
|
932
|
+
if args.command == "editor":
|
|
933
|
+
return _cmd_editor(args)
|
|
934
|
+
if args.command == "apply":
|
|
935
|
+
return _cmd_apply(args)
|
|
936
|
+
parser.print_help()
|
|
937
|
+
return 2
|