@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 CHANGED
@@ -1 +1 @@
1
- 2026-03-25T06:03:05.073Z
1
+ 2026-03-25T07:20:08.738Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-25T06:09:17.198Z",
3
- "checkpoint_at": "2026-03-25T06:09:17.200Z",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.4.35",
3
+ "version": "1.4.37",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -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(
@@ -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
- patches_dir = Path(sentinel.workspace_dir) / "patches"
187
- status, patch_path, marker = generate_fix(event, repo, sentinel, patches_dir, store)
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
- if status != "patch" or patch_path is None:
190
- store.record_fix(event.fingerprint, "skipped" if status in ("skip", "needs_human") else "failed",
191
- repo_name=repo.repo_name)
192
- # For user-submitted issues: always notify (person is waiting)
193
- submitter_uid = getattr(event, "submitter_user_id", "")
194
- reason_text = marker if status == "needs_human" else f"Claude Code returned {status.upper()}"
195
- notify_fix_blocked(sentinel, event.source, event.message,
196
- reason=reason_text, repo_name=repo.repo_name,
197
- submitter_user_id=submitter_uid)
198
- mark_done(event.issue_file)
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
- commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
202
- if commit_status != "committed":
203
- store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
204
- submitter_uid = getattr(event, "submitter_user_id", "")
205
- notify_fix_blocked(sentinel, event.source, event.message,
206
- reason="Patch was generated but commit/tests failed",
207
- repo_name=repo.repo_name,
208
- submitter_user_id=submitter_uid)
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
- branch, pr_url = publish(event, repo, sentinel, commit_hash)
213
- store.record_fix(
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
- if repo.auto_publish:
240
- cicd_trigger(repo, store, event.fingerprint)
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):