warpline 1.0.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.
warpline/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.0"
2
+
warpline/cli.py ADDED
@@ -0,0 +1,310 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from warpline import __version__, commands, install_support
8
+ from warpline.dogfood import DEFAULT_DOGFOOD_RESULTS, REAL_MEMBER_REPO, run_dogfood_evaluator
9
+ from warpline.git import backfill, ingest_commit
10
+ from warpline.install import install_hook
11
+ from warpline.loomweave import LoomweaveMcpClient, LoomweaveProbe, ToolClient
12
+ from warpline.mcp_smoke import run_mcp_smoke
13
+ from warpline.productization import read_productization_decision
14
+ from warpline.store import WarplineStore, default_store_path
15
+
16
+ # install/doctor component flags -> component keys
17
+ _INSTALL_FLAGS = {
18
+ "claude_code": "claude-code",
19
+ "codex": "codex",
20
+ "claude_md": "claude_md",
21
+ "agents_md": "agents_md",
22
+ "gitignore": "gitignore",
23
+ "hooks": "hooks",
24
+ "session_hook": "session-hook",
25
+ "skills": "skills",
26
+ "codex_skills": "codex-skills",
27
+ "config": "config",
28
+ }
29
+
30
+
31
+ def _optional_sei_client(
32
+ repo: Path,
33
+ *,
34
+ enabled: bool,
35
+ command: str,
36
+ ) -> tuple[ToolClient | None, dict[str, object] | None]:
37
+ if not enabled:
38
+ return None, None
39
+ probe = LoomweaveProbe(repo=repo, command=command).probe()
40
+ resolution = {
41
+ "status": probe.get("status"),
42
+ "reason": probe.get("reason"),
43
+ }
44
+ if probe.get("status") != "available":
45
+ return None, resolution
46
+ return LoomweaveMcpClient(repo=repo, command=command), {
47
+ "status": "available",
48
+ "version": probe.get("version"),
49
+ }
50
+
51
+
52
+ def build_parser() -> argparse.ArgumentParser:
53
+ parser = argparse.ArgumentParser(prog="warpline")
54
+ parser.add_argument("--version", action="store_true", help="print version and exit")
55
+ sub = parser.add_subparsers(dest="command")
56
+
57
+ init = sub.add_parser("init")
58
+ init.add_argument("--repo", type=Path, default=Path("."))
59
+
60
+ install_parser = sub.add_parser(
61
+ "install", help="Install warpline MCP bindings, hooks, skills, and config."
62
+ )
63
+ install_parser.add_argument("--repo", type=Path, default=Path("."))
64
+ install_parser.add_argument("--claude-code", action="store_true", help="Claude Code MCP only")
65
+ install_parser.add_argument("--codex", action="store_true", help="Codex MCP only")
66
+ install_parser.add_argument("--claude-md", action="store_true", help="CLAUDE.md block only")
67
+ install_parser.add_argument("--agents-md", action="store_true", help="AGENTS.md block only")
68
+ install_parser.add_argument("--gitignore", action="store_true", help="gitignore only")
69
+ install_parser.add_argument("--hooks", action="store_true", help="git post-commit hook only")
70
+ install_parser.add_argument(
71
+ "--session-hook", action="store_true", help="SessionStart hook only"
72
+ )
73
+ install_parser.add_argument("--skills", action="store_true", help="Claude Code skill only")
74
+ install_parser.add_argument("--codex-skills", action="store_true", help="Codex skill only")
75
+ install_parser.add_argument("--config", action="store_true", help=".weft/warpline config only")
76
+ install_parser.add_argument("--json", action="store_true")
77
+
78
+ doctor_parser = sub.add_parser(
79
+ "doctor", help="Verify the warpline installation; --fix autofixes."
80
+ )
81
+ doctor_parser.add_argument("--repo", type=Path, default=Path("."))
82
+ doctor_parser.add_argument("--fix", action="store_true", help="autofix anything fixable")
83
+ doctor_parser.add_argument("--json", action="store_true")
84
+
85
+ session_parser = sub.add_parser("session-context")
86
+ session_parser.add_argument("--repo", type=Path, default=Path("."))
87
+
88
+ backfill_parser = sub.add_parser("backfill")
89
+ backfill_parser.add_argument("--repo", type=Path, default=Path("."))
90
+ # HX1: SEI resolution is ON by default; degrades cleanly when loomweave is
91
+ # absent. Pass --no-resolve-sei to skip the loomweave probe entirely.
92
+ backfill_parser.add_argument(
93
+ "--resolve-sei", action=argparse.BooleanOptionalAction, default=True
94
+ )
95
+ backfill_parser.add_argument("--loomweave-command", default="loomweave")
96
+ backfill_parser.add_argument("--json", action="store_true")
97
+
98
+ ingest = sub.add_parser("ingest-commit")
99
+ ingest.add_argument("sha")
100
+ ingest.add_argument("--repo", type=Path, default=Path("."))
101
+ ingest.add_argument(
102
+ "--resolve-sei", action=argparse.BooleanOptionalAction, default=True
103
+ )
104
+ ingest.add_argument("--loomweave-command", default="loomweave")
105
+
106
+ loomweave_probe = sub.add_parser("loomweave-probe")
107
+ loomweave_probe.add_argument("--repo", type=Path, default=Path("."))
108
+ loomweave_probe.add_argument("--command", dest="loomweave_command", default="loomweave")
109
+ loomweave_probe.add_argument("--json", action="store_true")
110
+
111
+ changed_parser = sub.add_parser("changed")
112
+ changed_parser.add_argument("--repo", type=Path, default=Path("."))
113
+ changed_parser.add_argument("--rev-range")
114
+ changed_parser.add_argument("--json", action="store_true")
115
+
116
+ timeline_parser = sub.add_parser("timeline")
117
+ timeline_parser.add_argument("--repo", type=Path, default=Path("."))
118
+ timeline_parser.add_argument("--entity", required=True)
119
+ timeline_parser.add_argument("--json", action="store_true")
120
+
121
+ churn_parser = sub.add_parser("churn")
122
+ churn_parser.add_argument("--repo", type=Path, default=Path("."))
123
+ churn_parser.add_argument("--sei", action="append", default=[])
124
+ churn_parser.add_argument("--locator", action="append", default=[])
125
+ churn_parser.add_argument("--json", action="store_true")
126
+
127
+ blast_parser = sub.add_parser("blast-radius")
128
+ blast_parser.add_argument("--repo", type=Path, default=Path("."))
129
+ blast_parser.add_argument("--changed-entity-key-id", type=int, action="append", required=True)
130
+ blast_parser.add_argument("--depth", type=int, default=2)
131
+ blast_parser.add_argument("--json", action="store_true")
132
+
133
+ reverify_parser = sub.add_parser("reverify")
134
+ reverify_parser.add_argument("--repo", type=Path, default=Path("."))
135
+ reverify_parser.add_argument(
136
+ "--changed-entity-key-id",
137
+ type=int,
138
+ action="append",
139
+ required=True,
140
+ )
141
+ reverify_parser.add_argument("--depth", type=int, default=2)
142
+ reverify_parser.add_argument("--json", action="store_true")
143
+
144
+ capture_snapshot_parser = sub.add_parser("capture-snapshot")
145
+ capture_snapshot_parser.add_argument("--repo", type=Path, default=Path("."))
146
+ capture_snapshot_parser.add_argument("--commit")
147
+ capture_snapshot_parser.add_argument("--loomweave-command", default="loomweave")
148
+ capture_snapshot_parser.add_argument("--json", action="store_true")
149
+
150
+ dogfood_parser = sub.add_parser("dogfood-eval")
151
+ dogfood_parser.add_argument("--output", type=Path, default=DEFAULT_DOGFOOD_RESULTS)
152
+ dogfood_parser.add_argument("--work-dir", type=Path)
153
+ dogfood_parser.add_argument("--real-member-repo", type=Path, default=REAL_MEMBER_REPO)
154
+ dogfood_parser.add_argument("--skip-real-member", action="store_true")
155
+ dogfood_parser.add_argument("--json", action="store_true")
156
+
157
+ mcp_smoke = sub.add_parser("mcp-smoke")
158
+ mcp_smoke.add_argument("--repo", type=Path, default=Path("."))
159
+ mcp_smoke.add_argument("--no-bad-input", action="store_true")
160
+ mcp_smoke.add_argument("--json", action="store_true")
161
+
162
+ productization_parser = sub.add_parser("productization-gate")
163
+ productization_parser.add_argument("--report", default="spike/REPORT.md")
164
+ productization_parser.add_argument(
165
+ "--dogfood-results",
166
+ type=Path,
167
+ default=DEFAULT_DOGFOOD_RESULTS,
168
+ )
169
+
170
+ return parser
171
+
172
+
173
+ def main(argv: list[str] | None = None) -> int:
174
+ parser = build_parser()
175
+ args = parser.parse_args(argv)
176
+ if args.version:
177
+ print(f"warpline {__version__}")
178
+ return 0
179
+ if args.command == "init":
180
+ hook = install_hook(args.repo)
181
+ print(str(hook))
182
+ return 0
183
+ if args.command == "install":
184
+ selected = {key for attr, key in _INSTALL_FLAGS.items() if getattr(args, attr, False)}
185
+ install_report = install_support.run_install(args.repo, selected or None)
186
+ if args.json:
187
+ print(
188
+ json.dumps(
189
+ {
190
+ "schema": "warpline.install.v1",
191
+ "ok": install_report.ok,
192
+ "actions": [
193
+ {"component": n, "detail": d} for n, d in install_report.actions
194
+ ],
195
+ "errors": [
196
+ {"component": n, "detail": d} for n, d in install_report.errors
197
+ ],
198
+ },
199
+ sort_keys=True,
200
+ )
201
+ )
202
+ else:
203
+ for name, detail in install_report.actions:
204
+ print(f" ✓ {name}: {detail}")
205
+ for name, detail in install_report.errors:
206
+ print(f" !! {name}: {detail}")
207
+ return 0 if install_report.ok else 1
208
+ if args.command == "doctor":
209
+ doctor_report = install_support.run_doctor(args.repo, fix=args.fix)
210
+ if args.json:
211
+ print(json.dumps(install_support.doctor_summary(doctor_report), sort_keys=True))
212
+ else:
213
+ for result in doctor_report.results:
214
+ print(f" {'✓' if result.ok else '!!'} {result.name}: {result.detail}")
215
+ for name, detail in doctor_report.fixed:
216
+ print(f" → fixed {name}: {detail}")
217
+ return 0 if doctor_report.ok else 1
218
+ if args.command == "session-context":
219
+ print(commands.session_context(args.repo))
220
+ return 0
221
+ if args.command == "backfill":
222
+ sei_client, sei_resolution = _optional_sei_client(
223
+ args.repo,
224
+ enabled=args.resolve_sei,
225
+ command=args.loomweave_command,
226
+ )
227
+ with WarplineStore.open(default_store_path(args.repo)) as store:
228
+ report = backfill(store, args.repo, sei_client=sei_client)
229
+ if sei_resolution is not None:
230
+ report["sei_resolution"] = sei_resolution
231
+ print(json.dumps(report, sort_keys=True) if args.json else report)
232
+ return 0
233
+ if args.command == "ingest-commit":
234
+ try:
235
+ sei_client, _sei_resolution = _optional_sei_client(
236
+ args.repo,
237
+ enabled=args.resolve_sei,
238
+ command=args.loomweave_command,
239
+ )
240
+ with WarplineStore.open(default_store_path(args.repo)) as store:
241
+ ingest_commit(store, args.repo, args.sha, sei_client=sei_client)
242
+ except Exception as exc: # fail-soft hook contract
243
+ with WarplineStore.open(default_store_path(args.repo)) as store:
244
+ store.log_health(args.repo, "HOOK_INGEST_FAILED", str(exc))
245
+ return 0
246
+ if args.command == "loomweave-probe":
247
+ payload = LoomweaveProbe(repo=args.repo, command=args.loomweave_command).probe()
248
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
249
+ return 0
250
+ if args.command == "changed":
251
+ payload = commands.change_list(args.repo, args.rev_range)
252
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
253
+ return 0
254
+ if args.command == "timeline":
255
+ payload = commands.entity_timeline(args.repo, args.entity)
256
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
257
+ return 0
258
+ if args.command == "churn":
259
+ refs = [{"kind": "sei", "value": s} for s in args.sei]
260
+ refs += [{"kind": "locator", "value": loc} for loc in args.locator]
261
+ payload = commands.entity_churn_count(args.repo, refs)
262
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
263
+ return 0
264
+ if args.command == "blast-radius":
265
+ payload = commands.impact_radius(args.repo, args.changed_entity_key_id, args.depth)
266
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
267
+ return 0
268
+ if args.command == "reverify":
269
+ payload = commands.reverify_worklist(args.repo, args.changed_entity_key_id, args.depth)
270
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
271
+ return 0
272
+ if args.command == "capture-snapshot":
273
+ payload = commands.capture_snapshot(
274
+ args.repo,
275
+ commit=args.commit,
276
+ loomweave_command=args.loomweave_command,
277
+ )
278
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
279
+ return 0
280
+ if args.command == "dogfood-eval":
281
+ payload = run_dogfood_evaluator(
282
+ output_path=args.output,
283
+ work_dir=args.work_dir,
284
+ real_member_repo=None if args.skip_real_member else args.real_member_repo,
285
+ require_real_member=not args.skip_real_member,
286
+ )
287
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
288
+ return 0 if payload["ready"] else 2
289
+ if args.command == "mcp-smoke":
290
+ payload = run_mcp_smoke(args.repo, include_bad_input=not args.no_bad_input)
291
+ print(json.dumps(payload, sort_keys=True) if args.json else json.dumps(payload, indent=2))
292
+ return 0 if payload["ok"] else 2
293
+ if args.command == "productization-gate":
294
+ decision = read_productization_decision(
295
+ Path(args.report),
296
+ dogfood_results_path=args.dogfood_results,
297
+ )
298
+ payload = {
299
+ "allowed": decision.allowed,
300
+ "recommendation": decision.recommendation,
301
+ "reason": decision.reason,
302
+ }
303
+ print(json.dumps(payload, sort_keys=True))
304
+ return 0 if decision.allowed else 2
305
+ parser.print_help()
306
+ return 0
307
+
308
+
309
+ if __name__ == "__main__":
310
+ raise SystemExit(main())