fix(gitea-sync): survive live Gitea replies; point project memory at /buglog
gitea-sync.py fixes found on the first live run (bug-1855 follow-up): - 204/empty-body replies (label DELETE) no longer crash in json.loads - every request has a 60s timeout, so a stalled call fails instead of hanging - spec.json sync state is inserted as a trailing "gitea" block instead of re-serializing the file (15 hand-formatted spec.json files would reflow) - implementation issue body labels generated vs approved correctly; the validation body uses the same done-phase set as the close rules Project memory now routes all buglog writes through the buglog skill: CLAUDE.md key rule, a cerebrum Do-Not-Repeat entry (with the older duplicate-id / json.dump / tags-vs-fix lessons tagged as superseded), and xce-audit-invariants step 5. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19104f1940
commit
e915f8def4
4 changed files with 53 additions and 20 deletions
|
|
@ -223,8 +223,10 @@ def request(method, path, body=None):
|
|||
req.add_header("Accept", "application/json")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.status, json.loads(resp.read().decode("utf-8"))
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
# DELETE (and some PUT/POST) answer 204 with an empty body.
|
||||
return resp.status, (json.loads(raw) if raw.strip() else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None, None
|
||||
|
|
@ -240,6 +242,8 @@ def request(method, path, body=None):
|
|||
raise RuntimeError(
|
||||
f"Gitea API unreachable: {method} {path}\n{e.reason}"
|
||||
) from e
|
||||
except TimeoutError as e:
|
||||
raise RuntimeError(f"Gitea API timed out: {method} {path}") from e
|
||||
|
||||
|
||||
# ── GiteaAPI ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -493,9 +497,11 @@ def spec_implementation_body(spec_name, spec_data, tasks_path):
|
|||
parts.append("")
|
||||
|
||||
# Phase status
|
||||
tasks_approved = spec_data.get("approvals", {}).get("tasks", {}).get("approved", False)
|
||||
tasks = spec_data.get("approvals", {}).get("tasks", {})
|
||||
ready = spec_data.get("ready_for_implementation", False)
|
||||
parts.append(f"**Tasks generated**: {tasks_approved}")
|
||||
parts.append(f"**Phase**: {spec_data.get('phase', 'unknown')}")
|
||||
parts.append(f"**Tasks generated**: {tasks.get('generated', False)}")
|
||||
parts.append(f"**Tasks approved**: {tasks.get('approved', False)}")
|
||||
parts.append(f"**Ready for implementation**: {ready}")
|
||||
parts.append("")
|
||||
|
||||
|
|
@ -539,10 +545,9 @@ def spec_validation_body(spec_name, spec_data):
|
|||
if v.get("generated") and not v.get("approved")
|
||||
]
|
||||
|
||||
if "complete" in phase or (
|
||||
approvals.get("implementation") and
|
||||
approvals["implementation"].get("approved")
|
||||
):
|
||||
if phase == "superseded":
|
||||
parts.append(f"**Status**: Superseded by {spec_data.get('superseded_by', '?')}")
|
||||
elif phase in DONE_PHASES:
|
||||
parts.append("**Status**: ✅ Complete")
|
||||
elif open_phases:
|
||||
parts.append(f"**Status**: ⏳ Pending — awaiting: {', '.join(open_phases)}")
|
||||
|
|
@ -715,7 +720,7 @@ def sync_spec(api, spec_name, dry_run=False, quiet=False):
|
|||
"milestone_id": milestone_id,
|
||||
"issues": phase_issues,
|
||||
}
|
||||
_write_json(spec_json_path, spec_data)
|
||||
_append_top_level_key(spec_json_path, "gitea", spec_data["gitea"])
|
||||
|
||||
|
||||
def _sync_spec_phase_states(api, spec_name, spec_data, gitea_state,
|
||||
|
|
@ -919,6 +924,29 @@ def sync_bugs(api, dry_run=False, quiet=False, since=None):
|
|||
f".claude/rules/buglog-status.md: {' '.join(needs_status)}")
|
||||
|
||||
|
||||
def _append_top_level_key(path, key, value):
|
||||
"""Add `key` as the last member of a JSON file's top-level object without
|
||||
re-serializing the rest (spec.json files use hand-formatted inline arrays
|
||||
that a json.dumps rewrite would reflow). Falls back to a full rewrite when
|
||||
the key already exists or the file doesn't end in a plain '}' line."""
|
||||
text = path.read_text()
|
||||
data = json.loads(text)
|
||||
lines = text.rstrip("\n").split("\n")
|
||||
if key in data or lines[-1] != "}":
|
||||
data[key] = value
|
||||
_write_json(path, data)
|
||||
return
|
||||
body = json.dumps(value, indent=2, ensure_ascii=False).split("\n")
|
||||
last = len(lines) - 2
|
||||
if lines[last].strip() and not lines[last].rstrip().endswith(("{", ",")):
|
||||
lines[last] = lines[last].rstrip() + ","
|
||||
lines[-1:] = [f" {json.dumps(key)}: {body[0]}"] + [" " + b for b in body[1:]] + ["}"]
|
||||
out = "\n".join(lines) + "\n"
|
||||
if json.loads(out) != {**data, key: value}:
|
||||
raise RuntimeError(f"{path}: surgical write did not round-trip")
|
||||
path.write_text(out)
|
||||
|
||||
|
||||
# ── JSON Writing ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _write_json(path, data):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue