@misterhuydo/sentinel 1.4.35 → 1.4.37
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.
- package/.cairn/.hint-lock +1 -1
- package/.cairn/session.json +2 -2
- package/package.json +1 -1
- package/python/sentinel/fix_engine.py +24 -2
- package/python/sentinel/main.py +56 -50
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-25T07:20:08.738Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-
|
|
3
|
-
"checkpoint_at": "2026-03-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-25T07:20:53.334Z",
|
|
3
|
+
"checkpoint_at": "2026-03-25T07:20:53.335Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
|
@@ -29,6 +29,25 @@ _DIFF_BLOCK = re.compile(r"```(?:diff|patch)?\n(.*?)```", re.DOTALL)
|
|
|
29
29
|
_DIFF_HEADER = re.compile(r"^diff --git|^---\s+\S+|^\+\+\+\s+\S+", re.MULTILINE)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def _extract_patch(output: str) -> str | None:
|
|
33
|
+
"""Extract a unified diff patch from Claude's output."""
|
|
34
|
+
# 1. Prefer a fenced ```diff or ```patch block
|
|
35
|
+
m = _DIFF_BLOCK.search(output)
|
|
36
|
+
if m:
|
|
37
|
+
patch = m.group(1).strip()
|
|
38
|
+
if _DIFF_HEADER.search(patch):
|
|
39
|
+
return patch
|
|
40
|
+
|
|
41
|
+
# 2. Fall back to raw diff lines anywhere in the output
|
|
42
|
+
if _DIFF_HEADER.search(output):
|
|
43
|
+
lines = output.splitlines()
|
|
44
|
+
for i, line in enumerate(lines):
|
|
45
|
+
if _DIFF_HEADER.match(line):
|
|
46
|
+
return "\n".join(lines[i:])
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
def _build_prompt(event, repo: RepoConfig, log_file, marker: str, stale_markers: list[str] = None, synced_files: list = None) -> str:
|
|
33
52
|
if log_file and log_file.exists():
|
|
34
53
|
ctx = (
|
|
@@ -141,9 +160,12 @@ def _claude_cmd(bin_path: str, prompt: str) -> list[str]:
|
|
|
141
160
|
skip = _os.getuid() != 0
|
|
142
161
|
except AttributeError:
|
|
143
162
|
skip = True # Windows — always pass flag
|
|
163
|
+
# --bare: forces ANTHROPIC_API_KEY-only auth, skips keychain/OAuth/hooks.
|
|
164
|
+
# Required on headless servers (EC2) where Claude Code 2.x silently returns
|
|
165
|
+
# empty output when keychain auth fails.
|
|
144
166
|
if skip:
|
|
145
|
-
return [bin_path, "--dangerously-skip-permissions", "--print", prompt]
|
|
146
|
-
return [bin_path, "--print", prompt]
|
|
167
|
+
return [bin_path, "--bare", "--dangerously-skip-permissions", "--print", prompt]
|
|
168
|
+
return [bin_path, "--bare", "--print", prompt]
|
|
147
169
|
|
|
148
170
|
|
|
149
171
|
def _run_claude_attempt(
|
package/python/sentinel/main.py
CHANGED
|
@@ -183,61 +183,67 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
183
183
|
)
|
|
184
184
|
return # Leave the file so admin can add the header
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
try:
|
|
187
|
+
patches_dir = Path(sentinel.workspace_dir) / "patches"
|
|
188
|
+
status, patch_path, marker = generate_fix(event, repo, sentinel, patches_dir, store)
|
|
189
|
+
|
|
190
|
+
if status != "patch" or patch_path is None:
|
|
191
|
+
store.record_fix(event.fingerprint, "skipped" if status in ("skip", "needs_human") else "failed",
|
|
192
|
+
repo_name=repo.repo_name)
|
|
193
|
+
# For user-submitted issues: always notify (person is waiting)
|
|
194
|
+
submitter_uid = getattr(event, "submitter_user_id", "")
|
|
195
|
+
reason_text = marker if status == "needs_human" else f"Claude Code returned {status.upper()}"
|
|
196
|
+
notify_fix_blocked(sentinel, event.source, event.message,
|
|
197
|
+
reason=reason_text, repo_name=repo.repo_name,
|
|
198
|
+
submitter_user_id=submitter_uid)
|
|
199
|
+
mark_done(event.issue_file)
|
|
200
|
+
return
|
|
188
201
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return
|
|
202
|
+
commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
|
|
203
|
+
if commit_status != "committed":
|
|
204
|
+
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
205
|
+
submitter_uid = getattr(event, "submitter_user_id", "")
|
|
206
|
+
notify_fix_blocked(sentinel, event.source, event.message,
|
|
207
|
+
reason="Patch was generated but commit/tests failed",
|
|
208
|
+
repo_name=repo.repo_name,
|
|
209
|
+
submitter_user_id=submitter_uid)
|
|
210
|
+
mark_done(event.issue_file)
|
|
211
|
+
return
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
branch, pr_url = publish(event, repo, sentinel, commit_hash)
|
|
214
|
+
store.record_fix(
|
|
215
|
+
event.fingerprint,
|
|
216
|
+
"applied" if repo.auto_publish else "pending",
|
|
217
|
+
patch_path=str(patch_path),
|
|
218
|
+
commit_hash=commit_hash,
|
|
219
|
+
branch=branch,
|
|
220
|
+
pr_url=pr_url,
|
|
221
|
+
repo_name=repo.repo_name,
|
|
222
|
+
sentinel_marker=marker,
|
|
223
|
+
)
|
|
224
|
+
send_fix_notification(sentinel, {
|
|
225
|
+
"source": event.source,
|
|
226
|
+
"severity": "ERROR",
|
|
227
|
+
"fingerprint": event.fingerprint,
|
|
228
|
+
"first_seen": event.timestamp,
|
|
229
|
+
"message": event.message,
|
|
230
|
+
"stack_trace": event.body,
|
|
231
|
+
"repo_name": repo.repo_name,
|
|
232
|
+
"commit_hash": commit_hash,
|
|
233
|
+
"branch": branch,
|
|
234
|
+
"pr_url": pr_url,
|
|
235
|
+
"auto_publish": repo.auto_publish,
|
|
236
|
+
"files_changed": [],
|
|
237
|
+
})
|
|
209
238
|
mark_done(event.issue_file)
|
|
210
|
-
return
|
|
211
239
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
event.fingerprint,
|
|
215
|
-
"applied" if repo.auto_publish else "pending",
|
|
216
|
-
patch_path=str(patch_path),
|
|
217
|
-
commit_hash=commit_hash,
|
|
218
|
-
branch=branch,
|
|
219
|
-
pr_url=pr_url,
|
|
220
|
-
repo_name=repo.repo_name,
|
|
221
|
-
sentinel_marker=marker,
|
|
222
|
-
)
|
|
223
|
-
send_fix_notification(sentinel, {
|
|
224
|
-
"source": event.source,
|
|
225
|
-
"severity": "ERROR",
|
|
226
|
-
"fingerprint": event.fingerprint,
|
|
227
|
-
"first_seen": event.timestamp,
|
|
228
|
-
"message": event.message,
|
|
229
|
-
"stack_trace": event.body,
|
|
230
|
-
"repo_name": repo.repo_name,
|
|
231
|
-
"commit_hash": commit_hash,
|
|
232
|
-
"branch": branch,
|
|
233
|
-
"pr_url": pr_url,
|
|
234
|
-
"auto_publish": repo.auto_publish,
|
|
235
|
-
"files_changed": [],
|
|
236
|
-
})
|
|
237
|
-
mark_done(event.issue_file)
|
|
240
|
+
if repo.auto_publish:
|
|
241
|
+
cicd_trigger(repo, store, event.fingerprint)
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
except Exception:
|
|
244
|
+
logger.exception("Unexpected error processing issue %s — archiving to prevent retry loop", event.source)
|
|
245
|
+
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
246
|
+
mark_done(event.issue_file)
|
|
241
247
|
|
|
242
248
|
|
|
243
249
|
async def poll_cycle(cfg_loader: ConfigLoader, store: StateStore):
|