@misterhuydo/sentinel 1.1.8 → 1.2.0

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-23T08:53:23.129Z
1
+ 2026-03-23T09:25:27.661Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-23T09:16:11.394Z",
3
- "checkpoint_at": "2026-03-23T09:16:11.395Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T09:22:14.601Z",
3
+ "checkpoint_at": "2026-03-23T09:22:14.603Z",
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.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -187,9 +187,9 @@ Session context — critical rules:
187
187
  - When handling a new request, call the tools fresh. Do not assume any prior tool result is still current or that any prior step "counts" toward the current task.
188
188
  - The only exception: if the user explicitly asks about something from the history ("what did you find earlier?"), you may reference it — but note it is from a prior session.
189
189
 
190
- Avoid redundant tool calls:
191
- - If a broad search (e.g. search_logs with no source filter) already returned results for a query, do NOT repeat the same search with a source filter to "refine" — use the results you already have.
192
- - If a tool call fails (wrong source name, config not found), do NOT retry the entire search from scratch. Continue with what succeeded, and note the failure in your reply.
190
+ Avoid redundant tool calls (within a single response only — always run tools fresh for new requests):
191
+ - If a broad search (e.g. search_logs with no source filter) already returned results in THIS response, do NOT repeat the same search with a source filter to "refine" — use what you already fetched.
192
+ - If a tool call fails in THIS response, do NOT retry the entire search from scratch. Continue with what succeeded and note the failure.
193
193
  - One pass per task: gather all needed data in a single round of tool calls, then produce the final answer.
194
194
 
195
195
  Issue identification — before calling create_issue:
@@ -201,12 +201,18 @@ Issue identification — before calling create_issue:
201
201
  - Attachments: summarise any files/screenshots the user shared.
202
202
  - Support URL: note any ticket/doc/link the user mentioned.
203
203
  - Identity: always captured automatically from the Slack session.
204
- 3. Before calling the tool, confirm with the user in natural language:
204
+ 3. ALWAYS populate the `findings` field with evidence from this session:
205
+ - If you ran search_logs, tail_log, ask_codebase, or get_status before creating the issue,
206
+ include the relevant results (log excerpts, error counts, timestamps, stack traces, etc.).
207
+ - The fix engine reads only the issue file — it has no access to the conversation.
208
+ Everything it needs to understand and fix the problem must be in the file.
209
+ - A `description` with no evidence forces the fix engine to guess. Always attach what you found.
210
+ 4. Before calling the tool, confirm with the user in natural language:
205
211
  e.g. "I'll create an issue for project *1881* — here's what I have: [summary]. Look right?"
206
212
  Wait for their confirmation before proceeding.
207
213
  EXCEPTION: if the user's message already contains a clear project + unambiguous description,
208
214
  skip the confirmation and create immediately — don't ask when nothing is unclear.
209
- 4. After creating, tell them the issue was queued and Sentinel will pick it up on the next poll.
215
+ 5. After creating, tell them the issue was queued and Sentinel will pick it up on the next poll.
210
216
 
211
217
  When the engineer's request is fully handled, end your LAST message with the token: [DONE]
212
218
  IMPORTANT: Always write your actual reply text FIRST, then append [DONE] at the end. Example: "Hello! I'm Sentinel. [DONE]". Never output [DONE] as your only content.
@@ -267,6 +273,16 @@ _TOOLS = [
267
273
  "type": "string",
268
274
  "description": "Summary of any files/screenshots the user attached",
269
275
  },
276
+ "findings": {
277
+ "type": "string",
278
+ "description": (
279
+ "Evidence gathered during this session: log excerpts, search results, "
280
+ "stack traces, error counts, timestamps — anything from tool calls that "
281
+ "is relevant to the issue. Include this whenever you ran search_logs, "
282
+ "tail_log, ask_codebase, or get_status before creating the issue. "
283
+ "The fix engine reads the issue file and needs this context to act."
284
+ ),
285
+ },
270
286
  },
271
287
  "required": ["description"],
272
288
  },
@@ -949,6 +965,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
949
965
 
950
966
  support_url = inputs.get("support_url", "").strip()
951
967
  attachments_summary = inputs.get("attachments_summary", "").strip()
968
+ findings = inputs.get("findings", "").strip()
952
969
 
953
970
  issues_dir = project_dir / "issues"
954
971
  issues_dir.mkdir(exist_ok=True)
@@ -966,6 +983,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
966
983
  lines.append(f"SUBMITTED_AT: {datetime.now(timezone.utc).isoformat()}")
967
984
  lines.append("")
968
985
  lines.append(description)
986
+ if findings:
987
+ lines.append(f"\nEVIDENCE (gathered by Sentinel Boss):\n{findings}")
969
988
  if attachments_summary:
970
989
  lines.append(f"\nATTACHMENTS:\n{attachments_summary}")
971
990
  content = "\n".join(lines)
@@ -367,31 +367,61 @@ async def _dispatch(event: dict, client, cfg_loader, store) -> None:
367
367
 
368
368
  _MAX_HISTORY_TURNS = 20 # keep last 20 exchanges (~40 messages) to stay well within context limits
369
369
 
370
+
371
+ def _strip_tool_turns(history: list) -> list:
372
+ """
373
+ Remove any message that consists entirely of tool_use or tool_result blocks.
374
+ Keeps only plain text exchanges so loaded prior history doesn't contain stale
375
+ search results that the model might treat as already-done work.
376
+ """
377
+ result = []
378
+ for msg in history:
379
+ content = msg.get("content", "")
380
+ # Plain string content — always keep
381
+ if isinstance(content, str):
382
+ result.append(msg)
383
+ continue
384
+ # List content — keep only if it has at least one text block
385
+ if isinstance(content, list):
386
+ has_text = any(
387
+ isinstance(b, dict) and b.get("type") == "text" and b.get("text", "").strip()
388
+ for b in content
389
+ )
390
+ if has_text:
391
+ # Keep only the text blocks, drop tool_use/tool_result
392
+ text_blocks = [b for b in content if isinstance(b, dict) and b.get("type") == "text"]
393
+ result.append({**msg, "content": text_blocks})
394
+ return result
395
+
370
396
  async def _run_turn(session: _Session, message: str, client, cfg_loader, store, attachments: list | None = None, is_admin: bool = False) -> None:
371
397
  channel = session.channel
372
398
 
373
399
  # Load persisted history from DB on the first turn of a new session.
374
- # - _clean_history strips orphaned tool_use turns from a previous crashed session.
375
- # - Trim to last 6 exchanges (12 messages) to prevent stale tool results from bleeding
376
- # into the current session and causing the model to treat old work as already done.
377
- # - Inject a session boundary marker so the model clearly sees where prior context ends.
400
+ # - _clean_history strips orphaned tool_use turns from previous crashed sessions.
401
+ # - Strip all tool_use / tool_result blocks from prior history only keep conversational
402
+ # text. Stale search results in prior history cause the model to skip re-running tools
403
+ # for new requests ("I already searched that"), which produces wrong/empty answers.
404
+ # - Trim to last 6 text exchanges (12 messages) to limit context bleed.
405
+ # - Inject a session boundary marker so the model clearly separates old from new.
378
406
  if not session.history_loaded:
379
407
  loaded = _clean_history(store.load_conversation(session.user_id))
380
408
  if loaded:
381
- # Keep only the most recent 6 exchanges from prior session
409
+ text_only = _strip_tool_turns(loaded)
382
410
  _PRIOR_TURNS = 6
383
- trimmed = loaded[-(_PRIOR_TURNS * 2):]
384
- # Prepend a boundary pair so the model treats everything before it as old context
385
- session.history = [
386
- {
387
- "role": "user",
388
- "content": "[system: new session started — the following is prior conversation context only]",
389
- },
390
- {
391
- "role": "assistant",
392
- "content": [{"type": "text", "text": "Understood. I'll treat the prior context as reference only and handle your new request fresh."}],
393
- },
394
- ] + trimmed
411
+ trimmed = text_only[-(_PRIOR_TURNS * 2):]
412
+ if trimmed:
413
+ session.history = [
414
+ {
415
+ "role": "user",
416
+ "content": "[system: new session started — the following is prior conversation context only, no tool calls needed]",
417
+ },
418
+ {
419
+ "role": "assistant",
420
+ "content": [{"type": "text", "text": "Understood. Starting fresh prior context is for reference only."}],
421
+ },
422
+ ] + trimmed
423
+ else:
424
+ session.history = []
395
425
  else:
396
426
  session.history = []
397
427
  session.history_loaded = True