@misterhuydo/sentinel 1.0.68 → 1.0.70

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T16:22:20.785Z",
3
- "checkpoint_at": "2026-03-22T16:22:20.786Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T16:47:05.682Z",
3
+ "checkpoint_at": "2026-03-22T16:47:05.683Z",
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.0.68",
3
+ "version": "1.0.70",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -454,19 +454,19 @@ _TOOLS = [
454
454
  {
455
455
  "name": "ask_codebase",
456
456
  "description": (
457
- "Ask any natural-language question about a managed repo's codebase. "
457
+ "Ask any natural-language question about a managed codebase. "
458
+ "Accepts a repo name (e.g. 'STS', 'elprint-sales') OR a project name (e.g. '1881', 'elprint') "
459
+ "— if a project name is given and it has multiple repos, all are queried. "
458
460
  "Claude Code answers using its full codebase knowledge — no need to specify how. "
459
- "Use for any codebase question: 'what does the 1881 backend do?', "
460
- "'find PIN validation code in elprint', 'are there TODOs in cairn?', "
461
- "'what classes handle auth?', 'any security issues in elprint-sales?', "
462
- "'summarize the architecture of 1881', 'show me the dependency graph'."
461
+ "Use for: 'what does 1881 do?', 'TODOs in 1881', 'find PIN validation in STS', "
462
+ "'security issues in elprint-sales?', 'summarize the cairn repo'."
463
463
  ),
464
464
  "input_schema": {
465
465
  "type": "object",
466
466
  "properties": {
467
467
  "repo": {
468
468
  "type": "string",
469
- "description": "Repo name (partial match, e.g. '1881', 'elprint-sales', 'cairn')",
469
+ "description": "Repo name (e.g. 'STS', 'elprint-sales') OR project name (e.g. '1881', 'elprint') — project name queries all its repos",
470
470
  },
471
471
  "question": {
472
472
  "type": "string",
@@ -1034,49 +1034,63 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
1034
1034
  })
1035
1035
 
1036
1036
  if name == "ask_codebase":
1037
- repo_name = inputs.get("repo", "").lower()
1038
- question = inputs.get("question", "")
1037
+ target = inputs.get("repo", "").lower()
1038
+ question = inputs.get("question", "")
1039
1039
 
1040
- repo_cfg = next(
1041
- (r for rn, r in cfg_loader.repos.items() if repo_name in rn.lower()),
1042
- None,
1043
- )
1044
- if not repo_cfg:
1045
- return json.dumps({"error": f"Repo '{repo_name}' not found", "available": list(cfg_loader.repos.keys())})
1040
+ # 1. Find repos whose name contains the target (e.g. "STS", "elprint-sales")
1041
+ matched = [(rn, r) for rn, r in cfg_loader.repos.items() if target in rn.lower()]
1046
1042
 
1047
- local_path = Path(repo_cfg.local_path)
1048
- if not local_path.exists():
1049
- return json.dumps({"error": f"Repo not cloned yet at {local_path}. Run pull_repo first."})
1043
+ # 2. No repo match — check if target is a project name → use ALL repos in cfg_loader
1044
+ # (each Sentinel instance is scoped to one project, so all repos belong to it)
1045
+ if not matched:
1046
+ current_project = _read_project_name(Path("."))
1047
+ if target in current_project.lower() or current_project.lower() in target:
1048
+ matched = list(cfg_loader.repos.items())
1050
1049
 
1051
- prompt = (
1052
- f"You are a code analyst. Answer the following question about the codebase at: {local_path}\n\n"
1053
- f"Question: {question}\n\n"
1054
- f"Use whatever tools you need to answer accurately. Be concise and direct. Plain text only."
1055
- )
1050
+ if not matched:
1051
+ return json.dumps({
1052
+ "error": f"No repo or project found matching '{target}'",
1053
+ "available_repos": list(cfg_loader.repos.keys()),
1054
+ })
1056
1055
 
1057
1056
  cfg = cfg_loader.sentinel
1058
1057
  env = os.environ.copy()
1059
1058
  if cfg.anthropic_api_key:
1060
1059
  env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key
1061
1060
 
1062
- try:
1063
- r = subprocess.run(
1064
- [cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt],
1065
- capture_output=True, text=True, timeout=180, env=env,
1066
- cwd=str(local_path),
1061
+ def _ask_one(repo_name, repo_cfg) -> dict:
1062
+ local_path = Path(repo_cfg.local_path)
1063
+ if not local_path.exists():
1064
+ return {"repo": repo_name, "error": f"not cloned yet at {local_path}"}
1065
+ prompt = (
1066
+ f"You are a code analyst. Answer the following question about the codebase at: {local_path}\n\n"
1067
+ f"Question: {question}\n\n"
1068
+ f"Use whatever tools you need to answer accurately. Be concise and direct. Plain text only."
1067
1069
  )
1068
- output = (r.stdout or "").strip()
1069
- if r.returncode != 0 and not output:
1070
- return json.dumps({
1071
- "error": f"`claude --print` failed (rc={r.returncode})",
1072
- "stderr": (r.stderr or "").strip()[:300],
1073
- })
1074
- logger.info("Boss ask_codebase %s rc=%d len=%d", repo_cfg.repo_name, r.returncode, len(output))
1075
- return json.dumps({"repo": repo_cfg.repo_name, "answer": output[:4000]})
1076
- except subprocess.TimeoutExpired:
1077
- return json.dumps({"error": "Codebase query timed out after 180s"})
1078
- except Exception as e:
1079
- return json.dumps({"error": str(e)})
1070
+ try:
1071
+ r = subprocess.run(
1072
+ [cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt],
1073
+ capture_output=True, text=True, timeout=180, env=env,
1074
+ cwd=str(local_path),
1075
+ )
1076
+ output = (r.stdout or "").strip()
1077
+ logger.info("Boss ask_codebase %s rc=%d len=%d", repo_name, r.returncode, len(output))
1078
+ if r.returncode != 0 and not output:
1079
+ return {"repo": repo_name, "error": f"claude --print failed (rc={r.returncode}): {(r.stderr or '')[:200]}"}
1080
+ return {"repo": repo_name, "answer": output[:3000]}
1081
+ except subprocess.TimeoutExpired:
1082
+ return {"repo": repo_name, "error": "timed out after 180s"}
1083
+ except Exception as e:
1084
+ return {"repo": repo_name, "error": str(e)}
1085
+
1086
+ if len(matched) == 1:
1087
+ result = _ask_one(*matched[0])
1088
+ # Unwrap single-repo result for cleaner response
1089
+ return json.dumps(result)
1090
+
1091
+ # Multiple repos — query each and combine
1092
+ results = [_ask_one(rn, r) for rn, r in matched]
1093
+ return json.dumps({"project": target, "repos_queried": len(results), "results": results})
1080
1094
 
1081
1095
  if name == "restart_project":
1082
1096
  project_arg = inputs.get("project", "").lower()
@@ -318,9 +318,15 @@ async def _dispatch(event: dict, client, cfg_loader, store) -> None:
318
318
 
319
319
  # ── Turn processor ────────────────────────────────────────────────────────────
320
320
 
321
+ _MAX_HISTORY_TURNS = 20 # keep last 20 exchanges (~40 messages) to stay well within context limits
322
+
321
323
  async def _run_turn(session: _Session, message: str, client, cfg_loader, store) -> None:
322
324
  channel = session.channel
323
325
 
326
+ # Trim history to avoid context overflow on long conversations
327
+ if len(session.history) > _MAX_HISTORY_TURNS * 2:
328
+ session.history = session.history[-(_MAX_HISTORY_TURNS * 2):]
329
+
324
330
  # Typing indicator
325
331
  await _post(client, channel, "_thinking..._")
326
332