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 +2 -0
- warpline/cli.py +310 -0
- warpline/commands.py +569 -0
- warpline/dogfood.py +563 -0
- warpline/envelope.py +71 -0
- warpline/errors.py +139 -0
- warpline/git.py +168 -0
- warpline/install.py +25 -0
- warpline/install_support.py +489 -0
- warpline/locators.py +26 -0
- warpline/loomweave.py +238 -0
- warpline/mcp.py +497 -0
- warpline/mcp_smoke.py +194 -0
- warpline/productization.py +123 -0
- warpline/propagation.py +92 -0
- warpline/py.typed +1 -0
- warpline/refs.py +78 -0
- warpline/reverify.py +79 -0
- warpline/siblings.py +159 -0
- warpline/skills/warpline-workflow/SKILL.md +64 -0
- warpline/skills/warpline-workflow/examples/changed-to-reverify.json +71 -0
- warpline/skills/warpline-workflow/references/contract.md +54 -0
- warpline/skills/warpline-workflow/references/degrade-and-federation.md +53 -0
- warpline/skills/warpline-workflow/references/tools.md +62 -0
- warpline/snapshot.py +207 -0
- warpline/store.py +487 -0
- warpline-1.0.0.dist-info/METADATA +234 -0
- warpline-1.0.0.dist-info/RECORD +31 -0
- warpline-1.0.0.dist-info/WHEEL +4 -0
- warpline-1.0.0.dist-info/entry_points.txt +3 -0
- warpline-1.0.0.dist-info/licenses/LICENSE +21 -0
warpline/__init__.py
ADDED
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())
|