xrefkit 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.
- xrefkit/__init__.py +5 -0
- xrefkit/__main__.py +5 -0
- xrefkit/catalog_cli.py +57 -0
- xrefkit/cli.py +71 -0
- xrefkit/contracts.py +297 -0
- xrefkit/ctx.py +160 -0
- xrefkit/dashboard.py +1220 -0
- xrefkit/discovery.py +85 -0
- xrefkit/gate.py +428 -0
- xrefkit/goalstate.py +555 -0
- xrefkit/hashing.py +18 -0
- xrefkit/import_skill.py +469 -0
- xrefkit/instance.py +133 -0
- xrefkit/loaders.py +77 -0
- xrefkit/mcp/__init__.py +26 -0
- xrefkit/mcp/audit.py +168 -0
- xrefkit/mcp/bootstrap.py +337 -0
- xrefkit/mcp/catalog.py +2638 -0
- xrefkit/mcp/cli.py +173 -0
- xrefkit/mcp/client_cache.py +356 -0
- xrefkit/mcp/context_registry.py +277 -0
- xrefkit/mcp/contracts.py +243 -0
- xrefkit/mcp/dist.py +234 -0
- xrefkit/mcp/ownership.py +246 -0
- xrefkit/mcp/repository.py +217 -0
- xrefkit/mcp/schemas.py +349 -0
- xrefkit/mcp/server.py +773 -0
- xrefkit/mcp/startup_contract_pack.py +154 -0
- xrefkit/mcp_tools.py +47 -0
- xrefkit/models/__init__.py +41 -0
- xrefkit/models/common.py +185 -0
- xrefkit/models/effective_bundle.py +131 -0
- xrefkit/models/local_manifest.py +217 -0
- xrefkit/models/package_manifest.py +126 -0
- xrefkit/models/run_log.py +276 -0
- xrefkit/models/server_config.py +160 -0
- xrefkit/models/skill_definition.py +131 -0
- xrefkit/operations_cli.py +670 -0
- xrefkit/ownership.py +276 -0
- xrefkit/packmeta.py +289 -0
- xrefkit/registry.py +334 -0
- xrefkit/resolver.py +252 -0
- xrefkit/resource_provider.py +187 -0
- xrefkit/resources/base/contracts.json +178 -0
- xrefkit/resources/base/current.json +6 -0
- xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
- xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
- xrefkit/resources/base/model_body.md +22 -0
- xrefkit/runlog.py +45 -0
- xrefkit/skillmeta.py +1034 -0
- xrefkit/skillrun.py +2381 -0
- xrefkit/structure_catalog.py +199 -0
- xrefkit/tools/__init__.py +119 -0
- xrefkit/tools/__main__.py +4 -0
- xrefkit/v2_cli.py +130 -0
- xrefkit/workspace.py +117 -0
- xrefkit/xref.py +1048 -0
- xrefkit-0.3.0.dist-info/METADATA +203 -0
- xrefkit-0.3.0.dist-info/RECORD +65 -0
- xrefkit-0.3.0.dist-info/WHEEL +5 -0
- xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
- xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
- xrefkit-0.3.0.dist-info/top_level.txt +1 -0
xrefkit/goalstate.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VALID_PACKET_STATUSES = {"valid", "superseded", "blocked", "cancelled"}
|
|
12
|
+
VALID_WAKE_RECOVERY_TYPES = {"five_hour", "weekly", "unknown"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StateCorruptionError(ValueError):
|
|
16
|
+
"""Persisted goal state exists but cannot be trusted."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GoalStateResult:
|
|
21
|
+
ok: bool
|
|
22
|
+
action: str
|
|
23
|
+
data: dict[str, object] | None = None
|
|
24
|
+
errors: list[str] | None = None
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, object]:
|
|
27
|
+
return {
|
|
28
|
+
"ok": self.ok,
|
|
29
|
+
"action": self.action,
|
|
30
|
+
"data": self.data or {},
|
|
31
|
+
"errors": self.errors or [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_iso() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).isoformat()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _safe_goal_slug(value: str) -> str:
|
|
40
|
+
chars = [ch.lower() if ch.isalnum() else "_" for ch in value]
|
|
41
|
+
slug = "".join(chars).strip("_")
|
|
42
|
+
while "__" in slug:
|
|
43
|
+
slug = slug.replace("__", "_")
|
|
44
|
+
slug = slug or "goal"
|
|
45
|
+
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12]
|
|
46
|
+
return f"{slug}-{digest}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _goal_mode_dir(root: Path) -> Path:
|
|
50
|
+
return root / "work" / "goal_mode"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _packets_path(root: Path) -> Path:
|
|
54
|
+
return _goal_mode_dir(root) / "continuation_packets.jsonl"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lease_events_path(root: Path) -> Path:
|
|
58
|
+
return _goal_mode_dir(root) / "lease_events.jsonl"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _wake_events_path(root: Path) -> Path:
|
|
62
|
+
return _goal_mode_dir(root) / "wake_events.jsonl"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _leases_dir(root: Path) -> Path:
|
|
66
|
+
return _goal_mode_dir(root) / "leases"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _goals_dir(root: Path) -> Path:
|
|
70
|
+
return _goal_mode_dir(root) / "goals"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _wake_dir(root: Path) -> Path:
|
|
74
|
+
return _goal_mode_dir(root) / "wake"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _goal_lease_path(root: Path, goal_id: str) -> Path:
|
|
78
|
+
return _leases_dir(root) / f"{_safe_goal_slug(goal_id)}.json"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _goal_wake_path(root: Path, goal_id: str) -> Path:
|
|
82
|
+
return _wake_dir(root) / f"{_safe_goal_slug(goal_id)}.json"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _goal_lock_path(root: Path, goal_id: str) -> Path:
|
|
86
|
+
return _goal_mode_dir(root) / "locks" / f"{_safe_goal_slug(goal_id)}.lock"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _goal_record_path(root: Path, goal_id: str) -> Path:
|
|
90
|
+
return _goals_dir(root) / f"{_safe_goal_slug(goal_id)}.json"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _ensure_goal_dirs(root: Path) -> None:
|
|
94
|
+
_goal_mode_dir(root).mkdir(parents=True, exist_ok=True)
|
|
95
|
+
_goals_dir(root).mkdir(parents=True, exist_ok=True)
|
|
96
|
+
_leases_dir(root).mkdir(parents=True, exist_ok=True)
|
|
97
|
+
_wake_dir(root).mkdir(parents=True, exist_ok=True)
|
|
98
|
+
(_goal_mode_dir(root) / "locks").mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _append_jsonl(path: Path, payload: dict[str, object]) -> None:
|
|
102
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
104
|
+
fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _read_json(path: Path) -> dict[str, object] | None:
|
|
108
|
+
if not path.exists():
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
112
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
113
|
+
raise StateCorruptionError(f"corrupt persisted state: {path}") from exc
|
|
114
|
+
if not isinstance(loaded, dict):
|
|
115
|
+
raise StateCorruptionError(f"persisted state must be an object: {path}")
|
|
116
|
+
return loaded
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _GoalLock:
|
|
120
|
+
def __init__(self, path: Path, owner: str, timeout_seconds: float = 3.0) -> None:
|
|
121
|
+
self.path = path
|
|
122
|
+
self.owner = owner
|
|
123
|
+
self.timeout_seconds = timeout_seconds
|
|
124
|
+
self._acquired = False
|
|
125
|
+
|
|
126
|
+
def __enter__(self) -> "_GoalLock":
|
|
127
|
+
deadline = time.time() + self.timeout_seconds
|
|
128
|
+
payload = json.dumps({"owner": self.owner, "acquired_at": _now_iso()}, ensure_ascii=False)
|
|
129
|
+
while True:
|
|
130
|
+
try:
|
|
131
|
+
with self.path.open("x", encoding="utf-8") as fh:
|
|
132
|
+
fh.write(payload + "\n")
|
|
133
|
+
self._acquired = True
|
|
134
|
+
return self
|
|
135
|
+
except FileExistsError:
|
|
136
|
+
if time.time() >= deadline:
|
|
137
|
+
raise TimeoutError(f"timed out acquiring goal lock: {self.path}")
|
|
138
|
+
time.sleep(0.05)
|
|
139
|
+
|
|
140
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
141
|
+
if self._acquired and self.path.exists():
|
|
142
|
+
try:
|
|
143
|
+
self.path.unlink()
|
|
144
|
+
except FileNotFoundError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _load_packets(root: Path) -> list[dict[str, object]]:
|
|
149
|
+
path = _packets_path(root)
|
|
150
|
+
if not path.exists():
|
|
151
|
+
return []
|
|
152
|
+
packets: list[dict[str, object]] = []
|
|
153
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line:
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
payload = json.loads(line)
|
|
159
|
+
except Exception:
|
|
160
|
+
continue
|
|
161
|
+
if isinstance(payload, dict):
|
|
162
|
+
packets.append(payload)
|
|
163
|
+
return packets
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _latest_packet(root: Path, goal_id: str, *, valid_only: bool = False) -> dict[str, object] | None:
|
|
167
|
+
packets = _load_packets(root)
|
|
168
|
+
for packet in reversed(packets):
|
|
169
|
+
if packet.get("goal_id") != goal_id:
|
|
170
|
+
continue
|
|
171
|
+
if valid_only and packet.get("packet_status") != "valid":
|
|
172
|
+
continue
|
|
173
|
+
return packet
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _parse_iso(value: object) -> datetime | None:
|
|
178
|
+
if not isinstance(value, str) or not value:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
return datetime.fromisoformat(value)
|
|
182
|
+
except ValueError:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_acceptance(values: list[str]) -> tuple[list[dict[str, object]], list[str]]:
|
|
187
|
+
conditions: list[dict[str, object]] = []
|
|
188
|
+
errors: list[str] = []
|
|
189
|
+
seen: set[str] = set()
|
|
190
|
+
for raw in values:
|
|
191
|
+
condition_id, separator, text = str(raw).partition(":")
|
|
192
|
+
condition_id = condition_id.strip()
|
|
193
|
+
text = text.strip()
|
|
194
|
+
if not separator or not condition_id or not text:
|
|
195
|
+
errors.append(f"invalid acceptance condition: {raw!r}; expected id:text")
|
|
196
|
+
continue
|
|
197
|
+
if condition_id in seen:
|
|
198
|
+
errors.append(f"duplicate acceptance condition: {condition_id}")
|
|
199
|
+
continue
|
|
200
|
+
seen.add(condition_id)
|
|
201
|
+
conditions.append({"id": condition_id, "text": text, "status": "pending", "evidence": []})
|
|
202
|
+
return conditions, errors
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def define_goal(args) -> GoalStateResult:
|
|
206
|
+
root = Path(args.root).resolve()
|
|
207
|
+
_ensure_goal_dirs(root)
|
|
208
|
+
goal_id = str(args.goal).strip()
|
|
209
|
+
desired_state = str(args.state).strip()
|
|
210
|
+
conditions, errors = _parse_acceptance(list(args.acceptance or []))
|
|
211
|
+
if not goal_id:
|
|
212
|
+
errors.append("missing --goal")
|
|
213
|
+
if not desired_state:
|
|
214
|
+
errors.append("missing --state")
|
|
215
|
+
if not conditions:
|
|
216
|
+
errors.append("at least one acceptance condition is required")
|
|
217
|
+
if errors:
|
|
218
|
+
return GoalStateResult(ok=False, action="goal.define", errors=errors)
|
|
219
|
+
path = _goal_record_path(root, goal_id)
|
|
220
|
+
previous = _read_json(path)
|
|
221
|
+
now = _now_iso()
|
|
222
|
+
record = {
|
|
223
|
+
"schema": "xrefkit.goal/v1",
|
|
224
|
+
"goal_id": goal_id,
|
|
225
|
+
"desired_state": desired_state,
|
|
226
|
+
"acceptance_conditions": conditions,
|
|
227
|
+
"observed_state": "",
|
|
228
|
+
"blockers": [],
|
|
229
|
+
"risks": [],
|
|
230
|
+
"status": "active",
|
|
231
|
+
"acceptance_owner": str(args.owner or "").strip(),
|
|
232
|
+
"created_at": previous.get("created_at", now) if previous else now,
|
|
233
|
+
"updated_at": now,
|
|
234
|
+
"completed_at": None,
|
|
235
|
+
}
|
|
236
|
+
path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
237
|
+
return GoalStateResult(ok=True, action="goal.define", data=record)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def show_goal(args) -> GoalStateResult:
|
|
241
|
+
root = Path(args.root).resolve()
|
|
242
|
+
goal_id = str(args.goal).strip()
|
|
243
|
+
record = _read_json(_goal_record_path(root, goal_id))
|
|
244
|
+
if record is None:
|
|
245
|
+
return GoalStateResult(ok=False, action="goal.show", errors=[f"no goal found: {goal_id}"])
|
|
246
|
+
return GoalStateResult(ok=True, action="goal.show", data=record)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def complete_goal(args) -> GoalStateResult:
|
|
250
|
+
root = Path(args.root).resolve()
|
|
251
|
+
_ensure_goal_dirs(root)
|
|
252
|
+
goal_id = str(args.goal).strip()
|
|
253
|
+
path = _goal_record_path(root, goal_id)
|
|
254
|
+
record = _read_json(path)
|
|
255
|
+
if record is None:
|
|
256
|
+
return GoalStateResult(ok=False, action="goal.complete", errors=[f"no goal found: {goal_id}"])
|
|
257
|
+
evidence: dict[str, list[str]] = {}
|
|
258
|
+
errors: list[str] = []
|
|
259
|
+
for raw in list(args.evidence or []):
|
|
260
|
+
condition_id, separator, reference = str(raw).partition("=")
|
|
261
|
+
if not separator or not condition_id.strip() or not reference.strip():
|
|
262
|
+
errors.append(f"invalid evidence: {raw!r}; expected condition_id=reference")
|
|
263
|
+
continue
|
|
264
|
+
evidence.setdefault(condition_id.strip(), []).append(reference.strip())
|
|
265
|
+
known = {str(item["id"]) for item in record.get("acceptance_conditions", [])}
|
|
266
|
+
unknown = sorted(set(evidence) - known)
|
|
267
|
+
if unknown:
|
|
268
|
+
errors.append(f"unknown acceptance conditions: {unknown}")
|
|
269
|
+
updated_conditions: list[dict[str, object]] = []
|
|
270
|
+
missing: list[str] = []
|
|
271
|
+
for item in record.get("acceptance_conditions", []):
|
|
272
|
+
condition_id = str(item["id"])
|
|
273
|
+
refs = evidence.get(condition_id, [])
|
|
274
|
+
if not refs:
|
|
275
|
+
missing.append(condition_id)
|
|
276
|
+
updated_conditions.append({**item, "status": "met" if refs else "pending", "evidence": refs})
|
|
277
|
+
if missing:
|
|
278
|
+
errors.append(f"missing acceptance evidence: {missing}")
|
|
279
|
+
observed = str(args.observed_state).strip()
|
|
280
|
+
if not observed:
|
|
281
|
+
errors.append("missing --observed-state")
|
|
282
|
+
if errors:
|
|
283
|
+
return GoalStateResult(
|
|
284
|
+
ok=False,
|
|
285
|
+
action="goal.complete",
|
|
286
|
+
data={**record, "acceptance_conditions": updated_conditions},
|
|
287
|
+
errors=errors,
|
|
288
|
+
)
|
|
289
|
+
now = _now_iso()
|
|
290
|
+
completed = {
|
|
291
|
+
**record,
|
|
292
|
+
"acceptance_conditions": updated_conditions,
|
|
293
|
+
"observed_state": observed,
|
|
294
|
+
"status": "complete",
|
|
295
|
+
"updated_at": now,
|
|
296
|
+
"completed_at": now,
|
|
297
|
+
}
|
|
298
|
+
path.write_text(json.dumps(completed, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
299
|
+
return GoalStateResult(ok=True, action="goal.complete", data=completed)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def append_packet(args) -> GoalStateResult:
|
|
303
|
+
root = Path(args.root).resolve()
|
|
304
|
+
_ensure_goal_dirs(root)
|
|
305
|
+
|
|
306
|
+
goal_id = str(args.goal).strip()
|
|
307
|
+
summary = str(args.summary).strip()
|
|
308
|
+
next_action = str(args.next_action).strip()
|
|
309
|
+
packet_status = str(args.status).strip().lower()
|
|
310
|
+
|
|
311
|
+
if not goal_id:
|
|
312
|
+
return GoalStateResult(ok=False, action="packet.append", errors=["missing --goal"])
|
|
313
|
+
if not summary:
|
|
314
|
+
return GoalStateResult(ok=False, action="packet.append", errors=["missing --summary"])
|
|
315
|
+
if not next_action:
|
|
316
|
+
return GoalStateResult(ok=False, action="packet.append", errors=["missing --next-action"])
|
|
317
|
+
if packet_status not in VALID_PACKET_STATUSES:
|
|
318
|
+
return GoalStateResult(ok=False, action="packet.append", errors=[f"invalid packet status: {packet_status}"])
|
|
319
|
+
|
|
320
|
+
created_at = _now_iso()
|
|
321
|
+
packet = {
|
|
322
|
+
"goal_id": goal_id,
|
|
323
|
+
"packet_id": f"{_safe_goal_slug(goal_id)}-{created_at}",
|
|
324
|
+
"created_at": created_at,
|
|
325
|
+
"created_by": str(args.created_by or "").strip() or "xrefkit goal packet append",
|
|
326
|
+
"continuation_log": str(args.continuation_log or "").strip(),
|
|
327
|
+
"continuation_artifacts": [str(v).strip() for v in (args.artifact or []) if str(v).strip()],
|
|
328
|
+
"goal_state_summary": summary,
|
|
329
|
+
"next_first_action": next_action,
|
|
330
|
+
"current_boundary": str(args.boundary or "").strip(),
|
|
331
|
+
"stop_conditions": [str(v).strip() for v in (args.stop_condition or []) if str(v).strip()],
|
|
332
|
+
"drift_check_points": [str(v).strip() for v in (args.drift_check or []) if str(v).strip()],
|
|
333
|
+
"packet_status": packet_status,
|
|
334
|
+
"source_run_key": str(args.source_run_key or "").strip(),
|
|
335
|
+
"trace_id": str(args.trace_id or "").strip(),
|
|
336
|
+
"parent_packet_id": str(args.parent_packet or "").strip(),
|
|
337
|
+
"subgoal_id": str(args.subgoal or "").strip(),
|
|
338
|
+
"resume_blockers": [str(v).strip() for v in (args.resume_blocker or []) if str(v).strip()],
|
|
339
|
+
"expiry_hint": str(args.expiry_hint or "").strip(),
|
|
340
|
+
}
|
|
341
|
+
_append_jsonl(_packets_path(root), packet)
|
|
342
|
+
return GoalStateResult(ok=True, action="packet.append", data=packet)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def latest_packet(args) -> GoalStateResult:
|
|
346
|
+
root = Path(args.root).resolve()
|
|
347
|
+
goal_id = str(args.goal).strip()
|
|
348
|
+
if not goal_id:
|
|
349
|
+
return GoalStateResult(ok=False, action="packet.latest", errors=["missing --goal"])
|
|
350
|
+
packet = _latest_packet(root, goal_id, valid_only=bool(args.valid_only))
|
|
351
|
+
if packet is None:
|
|
352
|
+
return GoalStateResult(ok=False, action="packet.latest", errors=[f"no packet found for goal: {goal_id}"])
|
|
353
|
+
return GoalStateResult(ok=True, action="packet.latest", data=packet)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def observe_wake(args) -> GoalStateResult:
|
|
357
|
+
root = Path(args.root).resolve()
|
|
358
|
+
_ensure_goal_dirs(root)
|
|
359
|
+
|
|
360
|
+
goal_id = str(args.goal).strip()
|
|
361
|
+
source = str(args.source).strip()
|
|
362
|
+
recovery_type = str(args.recovery_type).strip().lower()
|
|
363
|
+
if not goal_id:
|
|
364
|
+
return GoalStateResult(ok=False, action="wake.observe", errors=["missing --goal"])
|
|
365
|
+
if not source:
|
|
366
|
+
return GoalStateResult(ok=False, action="wake.observe", errors=["missing --source"])
|
|
367
|
+
if recovery_type not in VALID_WAKE_RECOVERY_TYPES:
|
|
368
|
+
return GoalStateResult(ok=False, action="wake.observe", errors=[f"invalid recovery type: {recovery_type}"])
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
with _GoalLock(_goal_lock_path(root, goal_id), owner=f"wake:{source}"):
|
|
372
|
+
wake = {
|
|
373
|
+
"goal_id": goal_id,
|
|
374
|
+
"observed_at": _now_iso(),
|
|
375
|
+
"source": source,
|
|
376
|
+
"recovery_type": recovery_type,
|
|
377
|
+
"note": str(args.note or "").strip(),
|
|
378
|
+
"status": "wakeup_observed",
|
|
379
|
+
}
|
|
380
|
+
_goal_wake_path(root, goal_id).write_text(json.dumps(wake, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
381
|
+
_append_jsonl(_wake_events_path(root), {"event": "observe", **wake})
|
|
382
|
+
except TimeoutError as exc:
|
|
383
|
+
return GoalStateResult(ok=False, action="wake.observe", errors=[str(exc)])
|
|
384
|
+
return GoalStateResult(ok=True, action="wake.observe", data=wake)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def show_wake(args) -> GoalStateResult:
|
|
388
|
+
root = Path(args.root).resolve()
|
|
389
|
+
goal_id = str(args.goal).strip()
|
|
390
|
+
if not goal_id:
|
|
391
|
+
return GoalStateResult(ok=False, action="wake.show", errors=["missing --goal"])
|
|
392
|
+
wake = _read_json(_goal_wake_path(root, goal_id))
|
|
393
|
+
if wake is None:
|
|
394
|
+
return GoalStateResult(ok=False, action="wake.show", errors=[f"no wake state found for goal: {goal_id}"])
|
|
395
|
+
return GoalStateResult(ok=True, action="wake.show", data=wake)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def acquire_lease(args) -> GoalStateResult:
|
|
399
|
+
root = Path(args.root).resolve()
|
|
400
|
+
_ensure_goal_dirs(root)
|
|
401
|
+
|
|
402
|
+
goal_id = str(args.goal).strip()
|
|
403
|
+
owner = str(args.owner).strip()
|
|
404
|
+
ttl_hours = int(args.ttl_hours)
|
|
405
|
+
if not goal_id:
|
|
406
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=["missing --goal"])
|
|
407
|
+
if not owner:
|
|
408
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=["missing --owner"])
|
|
409
|
+
if ttl_hours < 1:
|
|
410
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=["--ttl-hours must be >= 1"])
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
with _GoalLock(_goal_lock_path(root, goal_id), owner=f"lease:{owner}"):
|
|
414
|
+
wake = _read_json(_goal_wake_path(root, goal_id))
|
|
415
|
+
if wake is None:
|
|
416
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=[f"goal is not wakeup_observed: {goal_id}"])
|
|
417
|
+
|
|
418
|
+
lease_path = _goal_lease_path(root, goal_id)
|
|
419
|
+
active = _read_json(lease_path)
|
|
420
|
+
now = datetime.now(timezone.utc)
|
|
421
|
+
if active and active.get("lease_status") == "leased":
|
|
422
|
+
expires_at = _parse_iso(active.get("lease_expires_at"))
|
|
423
|
+
if expires_at and expires_at > now:
|
|
424
|
+
return GoalStateResult(
|
|
425
|
+
ok=False,
|
|
426
|
+
action="lease.acquire",
|
|
427
|
+
errors=[f"active lease already exists for goal {goal_id} owner={active.get('lease_owner', '')}"],
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
source_packet_id = str(args.source_packet or "").strip()
|
|
431
|
+
if not source_packet_id:
|
|
432
|
+
latest = _latest_packet(root, goal_id, valid_only=True)
|
|
433
|
+
if latest is None:
|
|
434
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=[f"no valid packet found for goal: {goal_id}"])
|
|
435
|
+
source_packet_id = str(latest.get("packet_id") or "").strip()
|
|
436
|
+
if not source_packet_id:
|
|
437
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=[f"latest packet missing packet_id for goal: {goal_id}"])
|
|
438
|
+
|
|
439
|
+
lease = {
|
|
440
|
+
"goal_id": goal_id,
|
|
441
|
+
"lease_owner": owner,
|
|
442
|
+
"lease_acquired_at": now.isoformat(),
|
|
443
|
+
"lease_expires_at": (now + timedelta(hours=ttl_hours)).isoformat(),
|
|
444
|
+
"lease_status": "leased",
|
|
445
|
+
"source_packet_id": source_packet_id,
|
|
446
|
+
"attempt_count": int(active.get("attempt_count", 0)) + 1 if active else 1,
|
|
447
|
+
}
|
|
448
|
+
lease_path.write_text(json.dumps(lease, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
449
|
+
_append_jsonl(_lease_events_path(root), {"event": "acquire", **lease})
|
|
450
|
+
except (TimeoutError, StateCorruptionError) as exc:
|
|
451
|
+
return GoalStateResult(ok=False, action="lease.acquire", errors=[str(exc)])
|
|
452
|
+
return GoalStateResult(ok=True, action="lease.acquire", data=lease)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def release_lease(args) -> GoalStateResult:
|
|
456
|
+
root = Path(args.root).resolve()
|
|
457
|
+
_ensure_goal_dirs(root)
|
|
458
|
+
|
|
459
|
+
goal_id = str(args.goal).strip()
|
|
460
|
+
owner = str(args.owner).strip()
|
|
461
|
+
if not goal_id:
|
|
462
|
+
return GoalStateResult(ok=False, action="lease.release", errors=["missing --goal"])
|
|
463
|
+
if not owner and not bool(args.force):
|
|
464
|
+
return GoalStateResult(ok=False, action="lease.release", errors=["missing --owner unless --force is used"])
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
with _GoalLock(_goal_lock_path(root, goal_id), owner=f"release:{owner or 'force'}"):
|
|
468
|
+
lease_path = _goal_lease_path(root, goal_id)
|
|
469
|
+
active = _read_json(lease_path)
|
|
470
|
+
if active is None:
|
|
471
|
+
return GoalStateResult(ok=False, action="lease.release", errors=[f"no lease found for goal: {goal_id}"])
|
|
472
|
+
current_owner = str(active.get("lease_owner") or "")
|
|
473
|
+
if not bool(args.force) and owner != current_owner:
|
|
474
|
+
return GoalStateResult(
|
|
475
|
+
ok=False,
|
|
476
|
+
action="lease.release",
|
|
477
|
+
errors=[f"lease owner mismatch for goal {goal_id}: current={current_owner} requested={owner}"],
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
released = {
|
|
481
|
+
**active,
|
|
482
|
+
"lease_status": "released",
|
|
483
|
+
"released_at": _now_iso(),
|
|
484
|
+
"released_by": owner or "force",
|
|
485
|
+
"release_note": str(args.note or "").strip(),
|
|
486
|
+
}
|
|
487
|
+
lease_path.write_text(json.dumps(released, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
488
|
+
_append_jsonl(_lease_events_path(root), {"event": "release", **released})
|
|
489
|
+
except (TimeoutError, StateCorruptionError) as exc:
|
|
490
|
+
return GoalStateResult(ok=False, action="lease.release", errors=[str(exc)])
|
|
491
|
+
return GoalStateResult(ok=True, action="lease.release", data=released)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def show_lease(args) -> GoalStateResult:
|
|
495
|
+
root = Path(args.root).resolve()
|
|
496
|
+
goal_id = str(args.goal).strip()
|
|
497
|
+
if not goal_id:
|
|
498
|
+
return GoalStateResult(ok=False, action="lease.show", errors=["missing --goal"])
|
|
499
|
+
try:
|
|
500
|
+
lease = _read_json(_goal_lease_path(root, goal_id))
|
|
501
|
+
except StateCorruptionError as exc:
|
|
502
|
+
return GoalStateResult(ok=False, action="lease.show", errors=[str(exc)])
|
|
503
|
+
if lease is None:
|
|
504
|
+
return GoalStateResult(ok=False, action="lease.show", errors=[f"no lease found for goal: {goal_id}"])
|
|
505
|
+
return GoalStateResult(ok=True, action="lease.show", data=lease)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _print_result(result: GoalStateResult, as_json: bool) -> int:
|
|
509
|
+
if as_json:
|
|
510
|
+
print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2))
|
|
511
|
+
elif result.ok:
|
|
512
|
+
print(f"ok: {result.action}")
|
|
513
|
+
for key, value in (result.data or {}).items():
|
|
514
|
+
if value in ("", [], None):
|
|
515
|
+
continue
|
|
516
|
+
if isinstance(value, list):
|
|
517
|
+
print(f" {key}:")
|
|
518
|
+
for item in value:
|
|
519
|
+
print(f" - {item}")
|
|
520
|
+
else:
|
|
521
|
+
print(f" {key}: {value}")
|
|
522
|
+
else:
|
|
523
|
+
print(f"fail: {result.action}")
|
|
524
|
+
for error in result.errors or []:
|
|
525
|
+
print(f" error: {error}")
|
|
526
|
+
return 0 if result.ok else 1
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def cmd_goal(args) -> int:
|
|
530
|
+
if args.goal_cmd == "define":
|
|
531
|
+
return _print_result(define_goal(args), bool(args.json))
|
|
532
|
+
if args.goal_cmd == "show":
|
|
533
|
+
return _print_result(show_goal(args), bool(args.json))
|
|
534
|
+
if args.goal_cmd == "complete":
|
|
535
|
+
return _print_result(complete_goal(args), bool(args.json))
|
|
536
|
+
if args.goal_cmd == "packet":
|
|
537
|
+
if args.packet_cmd == "append":
|
|
538
|
+
return _print_result(append_packet(args), bool(args.json))
|
|
539
|
+
if args.packet_cmd == "latest":
|
|
540
|
+
return _print_result(latest_packet(args), bool(args.json))
|
|
541
|
+
if args.goal_cmd == "lease":
|
|
542
|
+
if args.lease_cmd == "acquire":
|
|
543
|
+
return _print_result(acquire_lease(args), bool(args.json))
|
|
544
|
+
if args.lease_cmd == "release":
|
|
545
|
+
return _print_result(release_lease(args), bool(args.json))
|
|
546
|
+
if args.lease_cmd == "show":
|
|
547
|
+
return _print_result(show_lease(args), bool(args.json))
|
|
548
|
+
if args.goal_cmd == "wake":
|
|
549
|
+
if args.wake_cmd == "observe":
|
|
550
|
+
return _print_result(observe_wake(args), bool(args.json))
|
|
551
|
+
if args.wake_cmd == "show":
|
|
552
|
+
return _print_result(show_wake(args), bool(args.json))
|
|
553
|
+
print("fail: goal")
|
|
554
|
+
print(" error: unsupported goal subcommand")
|
|
555
|
+
return 1
|
xrefkit/hashing.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Content hashing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sha256_text(text: str) -> str:
|
|
10
|
+
return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sha256_file(path: str | Path) -> str:
|
|
14
|
+
digest = hashlib.sha256()
|
|
15
|
+
with Path(path).open("rb") as handle:
|
|
16
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
17
|
+
digest.update(chunk)
|
|
18
|
+
return "sha256:" + digest.hexdigest()
|