@misterhuydo/sentinel 1.0.45 → 1.0.47

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-22T05:36:04.794Z",
3
- "checkpoint_at": "2026-03-22T05:36:04.795Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T05:48:46.579Z",
3
+ "checkpoint_at": "2026-03-22T05:48:46.580Z",
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.45",
3
+ "version": "1.0.47",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -60,6 +60,7 @@ class SentinelConfig:
60
60
  slack_bot_token: str = "" # xoxb-...
61
61
  slack_app_token: str = "" # xapp-... (Socket Mode)
62
62
  slack_channel: str = "" # optional: restrict to one channel ID or name
63
+ project_name: str = "" # optional: friendly name used by Sentinel Boss (e.g. "1881")
63
64
 
64
65
 
65
66
  @dataclass
@@ -152,6 +153,7 @@ class ConfigLoader:
152
153
  c.slack_bot_token = d.get("SLACK_BOT_TOKEN", "")
153
154
  c.slack_app_token = d.get("SLACK_APP_TOKEN", "")
154
155
  c.slack_channel = d.get("SLACK_CHANNEL", "")
156
+ c.project_name = d.get("PROJECT_NAME", "")
155
157
  self.sentinel = c
156
158
 
157
159
  def _load_log_sources(self):
@@ -29,17 +29,20 @@ and opens GitHub PRs for admin review (or pushes directly if AUTO_PUBLISH=true).
29
29
  Your job:
30
30
  - Understand what the DevOps engineer needs in natural language
31
31
  - Query Sentinel's live state (errors, fixes, open PRs) on their behalf
32
- - Create issue reports when asked to investigate or fix something
32
+ - Deliver tasks/issues to the right project you know all projects in this workspace
33
33
  - Control Sentinel (pause/resume) when asked
34
34
  - Give honest, concise answers — you know this system inside out
35
+ - If a project name is unclear or ambiguous, ask the engineer to clarify — never guess
35
36
 
36
37
  What you can do (tools available):
37
38
 
38
39
  1. get_status — Show recent errors detected, fixes applied/pending, open PRs.
39
40
  e.g. "what happened today?", "any issues?", "show open PRs"
40
41
 
41
- 2. create_issue — Queue a manual fix request for Sentinel to investigate next poll.
42
- e.g. "look into this bug: ...", "investigate order creation failures"
42
+ 2. create_issue — Deliver a fix/task to any project in this workspace by short name.
43
+ You know all project names use list_projects if you're unsure.
44
+ If the project name is ambiguous or not found, ask to clarify.
45
+ e.g. "tell 1881 to fix X", "look into Y in elprint", "investigate Z"
43
46
 
44
47
  3. pause_sentinel — Create SENTINEL_PAUSE file to halt all auto-fix activity.
45
48
  e.g. "pause sentinel", "stop auto-fixing"
@@ -69,8 +72,11 @@ What you can do (tools available):
69
72
  11. list_errors — List recent errors from the state store, optionally filtered by repo or source.
70
73
  e.g. "show all errors today", "what errors hit elprint this week?"
71
74
 
72
- 12. pull_repo — Run git pull on one or all managed repos.
73
- e.g. "pull changes for sentinel-1881", "git pull all repos", "update the code"
75
+ 12. pull_repo — Run git pull on one or all managed application repos.
76
+ e.g. "pull changes", "git pull all repos", "update the code"
77
+
78
+ 13. pull_config — Run git pull on one or all Sentinel project config dirs.
79
+ e.g. "pull config for 1881", "update sentinel config", "pull all configs"
74
80
 
75
81
  Tone: direct, professional, like a senior engineer who owns the system.
76
82
  Don't pad responses. Don't say "Great question!" or "Certainly!".
@@ -104,20 +110,25 @@ _TOOLS = [
104
110
  {
105
111
  "name": "create_issue",
106
112
  "description": (
107
- "Queue a fix request for Sentinel to investigate on the next poll cycle. "
108
- "Use whenever the engineer reports a bug, customer complaint, or asks you "
109
- "to look into something specific. Include every detail they gave you."
113
+ "Deliver a fix/task request to a Sentinel project instance. "
114
+ "Use when the engineer says 'tell 1881 to do X', 'look into Y in project elprint', "
115
+ "'implement this in 1881: ...'. Can target any project by short name. "
116
+ "Defaults to the current project if no project is specified."
110
117
  ),
111
118
  "input_schema": {
112
119
  "type": "object",
113
120
  "properties": {
114
121
  "description": {
115
122
  "type": "string",
116
- "description": "Full problem description — everything the engineer told you",
123
+ "description": "Full task/problem description — everything the engineer told you",
124
+ },
125
+ "project": {
126
+ "type": "string",
127
+ "description": "Project short name to deliver to (e.g. '1881', 'elprint'). Omit for current project.",
117
128
  },
118
129
  "target_repo": {
119
130
  "type": "string",
120
- "description": "Repo name to assign to (omit to let Sentinel auto-route)",
131
+ "description": "Specific repo within the project (omit to let Sentinel auto-route)",
121
132
  },
122
133
  },
123
134
  "required": ["description"],
@@ -279,8 +290,24 @@ def _short_name(dir_name: str) -> str:
279
290
  return dir_name[len("sentinel-"):]
280
291
  return dir_name
281
292
 
293
+ def _read_project_name(project_dir: Path) -> str:
294
+ """Return PROJECT_NAME from sentinel.properties if set, else fall back to _short_name(dir)."""
295
+ props = project_dir / "config" / "sentinel.properties"
296
+ if props.exists():
297
+ try:
298
+ for line in props.read_text(encoding="utf-8", errors="ignore").splitlines():
299
+ line = line.strip()
300
+ if line.startswith("PROJECT_NAME"):
301
+ _, _, val = line.partition("=")
302
+ val = val.partition("#")[0].strip()
303
+ if val:
304
+ return val
305
+ except Exception:
306
+ pass
307
+ return _short_name(project_dir.name)
308
+
282
309
  def _find_project_dirs(target: str = "") -> list[Path]:
283
- """Return project dirs matching target (short or full name), or all if target empty."""
310
+ """Return project dirs matching target (PROJECT_NAME, short name, or full dir name), or all if target empty."""
284
311
  workspace = _workspace_dir()
285
312
  results = []
286
313
  try:
@@ -290,7 +317,10 @@ def _find_project_dirs(target: str = "") -> list[Path]:
290
317
  if not (d / "config").exists():
291
318
  continue
292
319
  if target:
293
- if target.lower() not in d.name.lower() and target.lower() not in _short_name(d.name).lower():
320
+ t = target.lower()
321
+ if (t not in d.name.lower()
322
+ and t not in _short_name(d.name).lower()
323
+ and t not in _read_project_name(d).lower()):
294
324
  continue
295
325
  results.append(d)
296
326
  except Exception:
@@ -349,16 +379,43 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
349
379
  if name == "create_issue":
350
380
  description = inputs["description"]
351
381
  target_repo = inputs.get("target_repo", "")
352
- issues_dir = Path("issues")
382
+ project_arg = inputs.get("project", "")
383
+
384
+ if project_arg:
385
+ project_dirs = _find_project_dirs(project_arg)
386
+ if not project_dirs:
387
+ all_names = [_read_project_name(d) for d in _find_project_dirs()]
388
+ return json.dumps({
389
+ "error": f"No project found matching '{project_arg}'",
390
+ "available_projects": all_names,
391
+ "action_needed": "Ask the user which project they meant.",
392
+ })
393
+ if len(project_dirs) > 1:
394
+ matches = [_read_project_name(d) for d in project_dirs]
395
+ return json.dumps({
396
+ "error": f"Ambiguous project name '{project_arg}' — matches: {matches}",
397
+ "action_needed": "Ask the user to clarify which project they mean.",
398
+ })
399
+ project_dir = project_dirs[0]
400
+ else:
401
+ project_dir = Path(".")
402
+
403
+ issues_dir = project_dir / "issues"
353
404
  issues_dir.mkdir(exist_ok=True)
354
405
  fname = f"slack-{uuid.uuid4().hex[:8]}.txt"
355
406
  content = (f"TARGET_REPO: {target_repo}\n\n" if target_repo else "") + description
356
407
  (issues_dir / fname).write_text(content, encoding="utf-8")
357
- logger.info("Boss created issue: %s", fname)
408
+
409
+ # Touch SENTINEL_POLL_NOW so the target instance picks it up immediately
410
+ (project_dir / "SENTINEL_POLL_NOW").touch()
411
+
412
+ project_label = _read_project_name(project_dir.resolve()) if project_arg else "this project"
413
+ logger.info("Boss created issue for %s: %s", project_label, fname)
358
414
  return json.dumps({
359
- "status": "queued",
360
- "file": fname,
361
- "note": "Sentinel will pick this up on the next poll cycle.",
415
+ "status": "queued",
416
+ "project": project_label,
417
+ "file": fname,
418
+ "note": f"Delivered to '{project_label}'. Sentinel will process it on the next poll cycle.",
362
419
  })
363
420
 
364
421
  if name == "get_fix_details":
@@ -413,7 +470,7 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
413
470
  break
414
471
  repos_in_project.append({"repo": p.stem, "url": repo_url})
415
472
  projects.append({
416
- "project": _short_name(d.name),
473
+ "project": _read_project_name(d),
417
474
  "dir": d.name,
418
475
  "running": (d / "sentinel.pid").exists(),
419
476
  "this": d.resolve() == Path(".").resolve(),
@@ -523,7 +580,7 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
523
580
  results = []
524
581
  for d in dirs:
525
582
  res = _git_pull(d)
526
- results.append({"project": _short_name(d.name), "dir": d.name, **res})
583
+ results.append({"project": _read_project_name(d), "dir": d.name, **res})
527
584
  logger.info("Boss: pull_config %s → %s", d.name, res["status"])
528
585
  return json.dumps({"results": results})
529
586
 
@@ -662,14 +719,16 @@ async def handle_message(
662
719
  client = anthropic.Anthropic(api_key=api_key)
663
720
 
664
721
  # Build system context snapshot
665
- paused = Path("SENTINEL_PAUSE").exists()
666
- repos = list(cfg_loader.repos.keys())
667
- ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
722
+ paused = Path("SENTINEL_PAUSE").exists()
723
+ repos = list(cfg_loader.repos.keys())
724
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
725
+ known_projects = [_read_project_name(d) for d in _find_project_dirs()]
668
726
  system = (
669
727
  _SYSTEM
670
728
  + f"\n\nCurrent time: {ts}"
671
729
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
672
730
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
731
+ + (f"\nKnown projects in workspace: {', '.join(known_projects)}" if known_projects else "")
673
732
  )
674
733
 
675
734
  history.append({"role": "user", "content": message})
@@ -2,6 +2,10 @@
2
2
  # Shared settings (SMTP, schedule, etc.) live in the workspace-level sentinel.properties
3
3
  # one directory above this project. Values here override workspace defaults.
4
4
 
5
+ # Friendly project name for Sentinel Boss (optional).
6
+ # Boss will match this name when you say "tell 1881 to do X". If omitted, derived from dir name.
7
+ # PROJECT_NAME=1881
8
+
5
9
  # Who receives fix notifications and health reports for this project
6
10
  MAILS=you@yourdomain.com
7
11