touchstone-agent 0.1.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.
- touchstone/__init__.py +3 -0
- touchstone/cli.py +327 -0
- touchstone/config.py +505 -0
- touchstone/discovery.py +89 -0
- touchstone/doctor.py +410 -0
- touchstone/engines/__init__.py +3 -0
- touchstone/engines/base.py +75 -0
- touchstone/engines/claude.py +130 -0
- touchstone/engines/codex.py +84 -0
- touchstone/events.py +122 -0
- touchstone/execution/__init__.py +26 -0
- touchstone/execution/base.py +73 -0
- touchstone/execution/local.py +61 -0
- touchstone/execution/ssh.py +89 -0
- touchstone/forge.py +328 -0
- touchstone/graph.py +167 -0
- touchstone/initialize.py +90 -0
- touchstone/ledger.py +164 -0
- touchstone/lifecycle.py +394 -0
- touchstone/migrate.py +98 -0
- touchstone/nodes/__init__.py +5 -0
- touchstone/nodes/audit.py +149 -0
- touchstone/nodes/classify.py +155 -0
- touchstone/nodes/context.py +75 -0
- touchstone/nodes/publish.py +154 -0
- touchstone/nodes/review.py +103 -0
- touchstone/resources/__init__.py +1 -0
- touchstone/resources/briefs/code-audit.md +133 -0
- touchstone/resources/briefs/harness-review.md +104 -0
- touchstone/resources/briefs/review.md +44 -0
- touchstone/runner.py +336 -0
- touchstone/scheduling/__init__.py +37 -0
- touchstone/scheduling/base.py +84 -0
- touchstone/scheduling/launchd.py +138 -0
- touchstone/scheduling/model.py +84 -0
- touchstone/scheduling/systemd.py +126 -0
- touchstone/setup.py +46 -0
- touchstone/status.py +76 -0
- touchstone/visualise.py +72 -0
- touchstone_agent-0.1.0.dist-info/METADATA +289 -0
- touchstone_agent-0.1.0.dist-info/RECORD +44 -0
- touchstone_agent-0.1.0.dist-info/WHEEL +4 -0
- touchstone_agent-0.1.0.dist-info/entry_points.txt +2 -0
- touchstone_agent-0.1.0.dist-info/licenses/LICENSE +201 -0
touchstone/__init__.py
ADDED
touchstone/cli.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""The command line.
|
|
2
|
+
|
|
3
|
+
The stable command surface for initialization, diagnostics, scheduled runs,
|
|
4
|
+
human decisions, and graph documentation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from touchstone import visualise
|
|
14
|
+
from touchstone.config import ConfigError, load
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _init(args: argparse.Namespace) -> int:
|
|
18
|
+
from touchstone.execution.local import LocalExecutor
|
|
19
|
+
from touchstone.initialize import InitOptions, initialize
|
|
20
|
+
|
|
21
|
+
engine = args.engine
|
|
22
|
+
model = args.model
|
|
23
|
+
workflows = tuple(args.workflow or ())
|
|
24
|
+
schedule = args.schedule
|
|
25
|
+
if args.non_interactive:
|
|
26
|
+
if not engine or not model or not workflows or not schedule:
|
|
27
|
+
raise ConfigError(
|
|
28
|
+
"non-interactive init requires --engine, --model, --schedule, "
|
|
29
|
+
"and at least one --workflow"
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
engine = engine or input("Engine (codex or claude) [codex]: ").strip() or "codex"
|
|
33
|
+
model = model or input("Model: ").strip()
|
|
34
|
+
if not workflows:
|
|
35
|
+
workflow = input("Required workflow [ci.yml]: ").strip() or "ci.yml"
|
|
36
|
+
workflows = (workflow,)
|
|
37
|
+
schedule = schedule or input("Schedule [hourly]: ").strip() or "hourly"
|
|
38
|
+
if engine not in ("codex", "claude"):
|
|
39
|
+
raise ConfigError("engine must be 'codex' or 'claude'")
|
|
40
|
+
path = initialize(
|
|
41
|
+
InitOptions(
|
|
42
|
+
start=args.path,
|
|
43
|
+
engine=engine,
|
|
44
|
+
model=model or "",
|
|
45
|
+
workflows=workflows,
|
|
46
|
+
schedule=schedule or "hourly",
|
|
47
|
+
output=args.output,
|
|
48
|
+
force=args.force,
|
|
49
|
+
),
|
|
50
|
+
LocalExecutor(),
|
|
51
|
+
)
|
|
52
|
+
print(f"wrote {path}")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _doctor(args: argparse.Namespace) -> int:
|
|
57
|
+
from touchstone.doctor import build_context, run_doctor
|
|
58
|
+
|
|
59
|
+
config = load(args.config)
|
|
60
|
+
report = run_doctor(config, build_context(config, offline=args.offline))
|
|
61
|
+
if args.json:
|
|
62
|
+
print(report.to_json())
|
|
63
|
+
else:
|
|
64
|
+
for check in report.checks:
|
|
65
|
+
print(f"{check.level:4} {check.id}: {check.summary}")
|
|
66
|
+
if check.repair:
|
|
67
|
+
print(f" repair: {check.repair}")
|
|
68
|
+
return report.exit_code
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _setup(args: argparse.Namespace) -> int:
|
|
72
|
+
import json
|
|
73
|
+
from dataclasses import asdict
|
|
74
|
+
|
|
75
|
+
from touchstone.setup import setup
|
|
76
|
+
|
|
77
|
+
config = load(args.config)
|
|
78
|
+
report = setup(config, dry_run=args.dry_run)
|
|
79
|
+
if args.json:
|
|
80
|
+
print(json.dumps(asdict(report), indent=2))
|
|
81
|
+
else:
|
|
82
|
+
action = "would configure" if args.dry_run else "configured"
|
|
83
|
+
print(f"{action} state at {config.state_dir}")
|
|
84
|
+
for label in report.planned_labels:
|
|
85
|
+
print(f" label: {label}")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _status(args: argparse.Namespace) -> int:
|
|
90
|
+
import json
|
|
91
|
+
|
|
92
|
+
from touchstone.nodes.context import configure
|
|
93
|
+
from touchstone.status import collect_status
|
|
94
|
+
|
|
95
|
+
config = load(args.config)
|
|
96
|
+
report = collect_status(config, configure(config), scheduler=_scheduler(config))
|
|
97
|
+
if args.json:
|
|
98
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
99
|
+
return 0
|
|
100
|
+
if not report.findings:
|
|
101
|
+
print("no recorded findings")
|
|
102
|
+
for finding in report.findings:
|
|
103
|
+
pull = f" #{finding['pr']}" if finding.get("pr") is not None else ""
|
|
104
|
+
print(f"{finding['loop']}: {finding['state']}{pull} — {finding['title']}")
|
|
105
|
+
for run in report.last_runs:
|
|
106
|
+
print(f"last {run.get('loop', 'unknown')}: {run.get('outcome', 'unknown')}")
|
|
107
|
+
if report.scheduler:
|
|
108
|
+
installed = len(report.scheduler["installed"])
|
|
109
|
+
missing = len(report.scheduler["missing"])
|
|
110
|
+
print(
|
|
111
|
+
f"scheduler: {report.scheduler['adapter']} ({installed} installed, {missing} missing)"
|
|
112
|
+
)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _scheduler(config): # type: ignore[no-untyped-def]
|
|
117
|
+
from touchstone.execution.local import LocalExecutor
|
|
118
|
+
from touchstone.scheduling import current_scheduler
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# The configured executor owns repository/model work. Native user
|
|
122
|
+
# timers belong to the machine running this CLI, even when that work
|
|
123
|
+
# is delegated to an SSH target.
|
|
124
|
+
return current_scheduler(LocalExecutor())
|
|
125
|
+
except RuntimeError as exc:
|
|
126
|
+
raise ConfigError(str(exc)) from None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _install_scheduler(args: argparse.Namespace) -> int:
|
|
130
|
+
import json
|
|
131
|
+
|
|
132
|
+
config = load(args.config)
|
|
133
|
+
try:
|
|
134
|
+
report = _scheduler(config).install(config, target=args.output, dry_run=args.dry_run)
|
|
135
|
+
except RuntimeError as exc:
|
|
136
|
+
raise ConfigError(str(exc)) from None
|
|
137
|
+
payload = {
|
|
138
|
+
"files": [str(path) for path in report.files],
|
|
139
|
+
"changed": [str(path) for path in report.changed],
|
|
140
|
+
"commands": list(report.commands),
|
|
141
|
+
"dry_run": args.dry_run,
|
|
142
|
+
}
|
|
143
|
+
if args.json:
|
|
144
|
+
print(json.dumps(payload, indent=2))
|
|
145
|
+
else:
|
|
146
|
+
verb = "would write" if args.dry_run else "installed"
|
|
147
|
+
for path in report.files:
|
|
148
|
+
print(f"{verb}: {path}")
|
|
149
|
+
for command in report.commands:
|
|
150
|
+
print(f"inspect: {command}")
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _uninstall_scheduler(args: argparse.Namespace) -> int:
|
|
155
|
+
import json
|
|
156
|
+
|
|
157
|
+
config = load(args.config)
|
|
158
|
+
try:
|
|
159
|
+
report = _scheduler(config).uninstall(config, target=args.output, dry_run=args.dry_run)
|
|
160
|
+
except RuntimeError as exc:
|
|
161
|
+
raise ConfigError(str(exc)) from None
|
|
162
|
+
payload = {
|
|
163
|
+
"files": [str(path) for path in report.files],
|
|
164
|
+
"removed": [str(path) for path in report.changed],
|
|
165
|
+
"dry_run": args.dry_run,
|
|
166
|
+
}
|
|
167
|
+
if args.json:
|
|
168
|
+
print(json.dumps(payload, indent=2))
|
|
169
|
+
else:
|
|
170
|
+
verb = "would remove" if args.dry_run else "removed"
|
|
171
|
+
for path in report.changed:
|
|
172
|
+
print(f"{verb}: {path}")
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _scheduler_status(args: argparse.Namespace) -> int:
|
|
177
|
+
import json
|
|
178
|
+
from dataclasses import asdict
|
|
179
|
+
|
|
180
|
+
config = load(args.config)
|
|
181
|
+
status = _scheduler(config).status(config)
|
|
182
|
+
payload = asdict(status)
|
|
183
|
+
payload["installed"] = [str(path) for path in status.installed]
|
|
184
|
+
payload["missing"] = [str(path) for path in status.missing]
|
|
185
|
+
if args.json:
|
|
186
|
+
print(json.dumps(payload, indent=2))
|
|
187
|
+
else:
|
|
188
|
+
print(f"scheduler: {status.adapter}")
|
|
189
|
+
for path in status.installed:
|
|
190
|
+
print(f"installed: {path}")
|
|
191
|
+
for path in status.missing:
|
|
192
|
+
print(f"missing: {path}")
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _migrate_config(args: argparse.Namespace) -> int:
|
|
197
|
+
from touchstone.migrate import migrate_config
|
|
198
|
+
|
|
199
|
+
report = migrate_config(args.path)
|
|
200
|
+
print(f"migrated {report.path} from version {report.from_version} to {report.to_version}")
|
|
201
|
+
print(f"backup: {report.backup}")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _run(args: argparse.Namespace) -> int:
|
|
206
|
+
from touchstone.runner import execute
|
|
207
|
+
|
|
208
|
+
config = load(args.config)
|
|
209
|
+
print(f"touchstone: {config.describe()}", file=sys.stderr)
|
|
210
|
+
return execute(config, loop=args.loop, dry_run=args.dry_run)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _resume(args: argparse.Namespace) -> int:
|
|
214
|
+
from touchstone.runner import resume
|
|
215
|
+
|
|
216
|
+
config = load(args.config)
|
|
217
|
+
return resume(config, thread=args.thread, answer=args.answer)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _graph(args: argparse.Namespace) -> int:
|
|
221
|
+
root = Path.cwd()
|
|
222
|
+
if args.write:
|
|
223
|
+
print(f"wrote {visualise.write(root)}")
|
|
224
|
+
return 0
|
|
225
|
+
if args.check:
|
|
226
|
+
ok, message = visualise.check(root)
|
|
227
|
+
print(message, file=sys.stdout if ok else sys.stderr)
|
|
228
|
+
return 0 if ok else 1
|
|
229
|
+
if args.ascii:
|
|
230
|
+
print(visualise.ascii_art())
|
|
231
|
+
return 0
|
|
232
|
+
print(visualise.mermaid())
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main(argv: list[str] | None = None) -> int:
|
|
237
|
+
parser = argparse.ArgumentParser(prog="touchstone", description=__doc__)
|
|
238
|
+
parser.add_argument("--config", type=Path, help="a TOML file; discovered when omitted")
|
|
239
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
240
|
+
|
|
241
|
+
init = sub.add_parser("init", help="discover a repository and write touchstone.toml")
|
|
242
|
+
init.add_argument("--path", type=Path, default=Path.cwd(), help="repository or child path")
|
|
243
|
+
init.add_argument("--output", type=Path, help="configuration destination")
|
|
244
|
+
init.add_argument("--non-interactive", action="store_true")
|
|
245
|
+
init.add_argument("--engine", choices=("codex", "claude"))
|
|
246
|
+
init.add_argument("--model")
|
|
247
|
+
init.add_argument("--workflow", action="append", help="required default-branch workflow")
|
|
248
|
+
init.add_argument("--schedule", help="hourly, daily@HH:MM, or weekly@DAY,HH:MM")
|
|
249
|
+
init.add_argument("--force", action="store_true", help="replace an existing config")
|
|
250
|
+
init.set_defaults(handler=_init)
|
|
251
|
+
|
|
252
|
+
doctor = sub.add_parser("doctor", help="check prerequisites without changing them")
|
|
253
|
+
doctor.add_argument("--json", action="store_true")
|
|
254
|
+
doctor.add_argument("--offline", action="store_true", help="skip GitHub network checks")
|
|
255
|
+
doctor.set_defaults(handler=_doctor)
|
|
256
|
+
|
|
257
|
+
setup = sub.add_parser("setup", help="create state and configured GitHub labels")
|
|
258
|
+
setup.add_argument("--dry-run", action="store_true")
|
|
259
|
+
setup.add_argument("--json", action="store_true")
|
|
260
|
+
setup.set_defaults(handler=_setup)
|
|
261
|
+
|
|
262
|
+
status = sub.add_parser("status", help="reconcile and report repository lifecycle state")
|
|
263
|
+
status.add_argument("--json", action="store_true")
|
|
264
|
+
status.set_defaults(handler=_status)
|
|
265
|
+
|
|
266
|
+
install_scheduler = sub.add_parser(
|
|
267
|
+
"install-scheduler", help="install native per-user loop timers"
|
|
268
|
+
)
|
|
269
|
+
install_scheduler.add_argument("--dry-run", action="store_true")
|
|
270
|
+
install_scheduler.add_argument("--output", type=Path, help="render without enabling")
|
|
271
|
+
install_scheduler.add_argument("--json", action="store_true")
|
|
272
|
+
install_scheduler.set_defaults(handler=_install_scheduler)
|
|
273
|
+
|
|
274
|
+
uninstall_scheduler = sub.add_parser(
|
|
275
|
+
"uninstall-scheduler", help="remove native per-user loop timers"
|
|
276
|
+
)
|
|
277
|
+
uninstall_scheduler.add_argument("--dry-run", action="store_true")
|
|
278
|
+
uninstall_scheduler.add_argument("--output", type=Path, help="remove rendered files here")
|
|
279
|
+
uninstall_scheduler.add_argument("--json", action="store_true")
|
|
280
|
+
uninstall_scheduler.set_defaults(handler=_uninstall_scheduler)
|
|
281
|
+
|
|
282
|
+
scheduler_status = sub.add_parser(
|
|
283
|
+
"scheduler-status", help="report native timer installation state"
|
|
284
|
+
)
|
|
285
|
+
scheduler_status.add_argument("--json", action="store_true")
|
|
286
|
+
scheduler_status.set_defaults(handler=_scheduler_status)
|
|
287
|
+
|
|
288
|
+
config = sub.add_parser("config", help="inspect or migrate configuration")
|
|
289
|
+
config_sub = config.add_subparsers(dest="config_command", required=True)
|
|
290
|
+
migrate = config_sub.add_parser("migrate", help="migrate an unversioned config")
|
|
291
|
+
migrate.add_argument("path", type=Path)
|
|
292
|
+
migrate.set_defaults(handler=_migrate_config)
|
|
293
|
+
|
|
294
|
+
run = sub.add_parser("run", help="one iteration of a loop")
|
|
295
|
+
run.add_argument("loop", help="which loop, by its [loop.*] name")
|
|
296
|
+
run.add_argument(
|
|
297
|
+
"--dry-run",
|
|
298
|
+
action="store_true",
|
|
299
|
+
help="audit, classify and review for real; stop before publishing",
|
|
300
|
+
)
|
|
301
|
+
run.set_defaults(handler=_run)
|
|
302
|
+
|
|
303
|
+
resume = sub.add_parser("resume", help="answer a parked draft and continue that thread")
|
|
304
|
+
resume.add_argument("thread", help="the thread id the parked run reported")
|
|
305
|
+
resume.add_argument("answer", choices=("merge", "close"))
|
|
306
|
+
resume.set_defaults(handler=_resume)
|
|
307
|
+
|
|
308
|
+
graph = sub.add_parser("graph", help="draw the graph")
|
|
309
|
+
graph.add_argument("--write", action="store_true", help=f"regenerate {visualise.DIAGRAM}")
|
|
310
|
+
graph.add_argument(
|
|
311
|
+
"--check", action="store_true", help="fail if the committed diagram is stale"
|
|
312
|
+
)
|
|
313
|
+
graph.add_argument(
|
|
314
|
+
"--ascii", action="store_true", help="ASCII instead of Mermaid (needs grandalf)"
|
|
315
|
+
)
|
|
316
|
+
graph.set_defaults(handler=_graph)
|
|
317
|
+
|
|
318
|
+
args = parser.parse_args(argv)
|
|
319
|
+
try:
|
|
320
|
+
return int(args.handler(args))
|
|
321
|
+
except ConfigError as exc:
|
|
322
|
+
print(f"touchstone: {exc}", file=sys.stderr)
|
|
323
|
+
return 78 # EX_CONFIG, the same code launchd uses for a job it cannot start
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
raise SystemExit(main())
|