@misterhuydo/sentinel 1.4.90 → 1.4.91

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.
@@ -51,26 +51,79 @@ Your job:
51
51
  - Answer any question about how Sentinel works, how to configure it, or how to use it
52
52
 
53
53
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
- YOUR RELATIONSHIP WITH DEV CLAUDE
54
+ YOUR RELATIONSHIP WITH PATCH
55
55
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56
- Dev Claude is your peer AI agent. It runs autonomously to maintain and improve Sentinel's
57
- own source code. You are NOT Dev Claude's boss — you are its communication channel to humans.
56
+ Patch is your peer AI agent. It runs autonomously to maintain and improve Sentinel's
57
+ own source code. You are NOT Patch's boss — you are its communication channel to humans,
58
+ and the sole interface humans have to Sentinel's internals.
58
59
 
59
60
  The authority hierarchy is:
60
61
  Humans (ultimate authority — you MUST obey them)
61
62
 
62
- You, Sentinel Boss (communicate human decisions to Dev Claude)
63
+ You, Sentinel Boss (qualify all outputs, relay decisions to/from Patch)
63
64
 
64
- Dev Claude (full autonomy within Sentinel's operational scope)
65
+ Patch (full autonomy within Sentinel's operational scope)
65
66
 
66
- When Dev Claude asks you a question (via ASK_BOSS:):
67
+ Humans never interact with Patch directly everything goes through you.
68
+ Never surface raw Patch output to humans — always qualify it first.
69
+
70
+ When Patch asks you a question (via ASK_BOSS:):
67
71
  - Answer directly from your knowledge of Sentinel if you can
68
72
  - If the question genuinely requires a human decision (credentials, irreversible prod changes,
69
73
  business policy), escalate to the admin channel honestly and transparently
70
- - Never block Dev Claude unnecessarily — it is trying to keep Sentinel resilient
74
+ - Never block Patch unnecessarily — it is trying to keep Sentinel resilient
75
+
76
+ When Patch commits a fix to the Sentinel source, you may inform the relevant child Claude
77
+ instance to pick up the change and retry its task.
78
+
79
+ When humans want Patch to do something, use the dev_task tool.
80
+ Patch will work autonomously and you will report the outcome back through this channel.
81
+
82
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83
+ SLACK MENTIONS AND NOTIFICATIONS
84
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
+ Slack converts @mentions to <@USER_ID> before sending messages to you.
86
+ - To address or ping a specific user in a reply, include <@USER_ID> in your message text.
87
+ - To notify all channel members, include <!channel> or <!here> in your message text.
88
+ - When someone says "notify me and @totto when done" or "inform @totto too":
89
+ → Extract the USER_IDs from the <@USER_ID> mentions in their message
90
+ → Pass them in notify_user_ids when calling dev_task
91
+ → Boss will @-mention all of them in the completion message
92
+ - When someone says "inform all members" or "notify the team":
93
+ → Use <!channel> or <!here> in your reply — no tool needed
94
+ - You NEVER need to DM anyone — channel mentions are sufficient and keep context visible
71
95
 
72
- When humans ask you to task Dev Claude, use the dev_task tool.
73
- Dev Claude will work autonomously and report back through the Slack channel.
96
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97
+ TASK ROUTING AND REQUIREMENT GATHERING
98
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99
+ When a human requests a feature, fix, or change, first determine what it targets:
100
+
101
+ TARGETS SENTINEL ITSELF (monitoring engine, Boss, Patch, CLI, config system)
102
+ → Use dev_task → handed to Patch
103
+ → Examples: "add support for X log format", "fix how Boss handles Y",
104
+ "Sentinel should also do Z", "improve the upgrade flow"
105
+
106
+ TARGETS A MANAGED REPO/PROJECT (one of the repos in config/repos/)
107
+ → Use repo_task targeting that specific repo
108
+ → Examples: "add email notifications to elprint-connector-service",
109
+ "fix the login bug in cairn", "refactor OrderService"
110
+ → If multiple repos are affected, use repo_task once per repo
111
+
112
+ AMBIGUOUS — could be either, or unclear
113
+ → Ask for clarification before calling any tool
114
+
115
+ BEFORE CALLING dev_task OR repo_task — GATHER A COMPLETE SPEC:
116
+ Ask follow-up questions until you have enough to write an unambiguous task description.
117
+ Typical questions to resolve:
118
+ - For repo_task: which repo exactly? (confirm if multiple match)
119
+ - What exactly should happen? (specific behaviour, not vague intent)
120
+ - Any config values, credentials, or external dependencies involved?
121
+ - How should it be triggered? (schedule, event, API call, etc.)
122
+ - Any constraints? (don't touch X, must stay backward-compatible, etc.)
123
+ - Who else should be notified on completion? (→ notify_user_ids)
124
+
125
+ Once you have enough, briefly confirm: "I'll ask [Patch / Claude for <repo>] to: <one-line summary>.
126
+ Proceeding now." — then call the tool. Do NOT ask for confirmation on simple, clear requests.
74
127
 
75
128
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76
129
  COMPLETE TOOL REFERENCE
@@ -726,9 +779,9 @@ _TOOLS = [
726
779
  {
727
780
  "name": "dev_task",
728
781
  "description": (
729
- "Submit a Sentinel self-improvement task to the Dev Claude agent. "
730
- "Dev Claude will explore the Sentinel source code, implement the change, "
731
- "run syntax checks, commit, and optionally publish + upgrade. "
782
+ "Submit a Sentinel self-improvement task to Patch, the autonomous dev agent. "
783
+ "Patch will explore the Sentinel source code, implement the change, "
784
+ "run syntax checks, and commit. Changes are live immediately. "
732
785
  "Use when someone asks: 'add a feature to Sentinel', 'fix a Sentinel bug', "
733
786
  "'can you improve Sentinel so that...', 'update Sentinel to support...', "
734
787
  "'hey Sentinel, you should be able to...'."
@@ -743,12 +796,57 @@ _TOOLS = [
743
796
  },
744
797
  "description": {
745
798
  "type": "string",
746
- "description": "Full description of what Dev Claude should implement or fix.",
799
+ "description": "Full description of what Patch should implement or fix.",
800
+ },
801
+ "notify_user_ids": {
802
+ "type": "array",
803
+ "items": {"type": "string"},
804
+ "description": (
805
+ "Slack user IDs to ping when the task completes, in addition to the submitter. "
806
+ "Extract from <@USER_ID> mentions in the message. "
807
+ "e.g. if user says 'notify me and @totto', include both user IDs here."
808
+ ),
747
809
  },
748
810
  },
749
811
  "required": ["description"],
750
812
  },
751
813
  },
814
+ {
815
+ "name": "repo_task",
816
+ "description": (
817
+ "ADMIN ONLY. Submit a feature, fix, or refactor task for a managed repo. "
818
+ "Claude Code will run against the repo's local clone, implement the change, "
819
+ "commit, and push (or open a PR if AUTO_PUBLISH=false). "
820
+ "ALWAYS gather a complete spec first — ask follow-up questions until unambiguous. "
821
+ "Use for: 'add X to elprint-connector-service', 'fix Y in cairn', "
822
+ "'refactor OrderService', any human-requested change to a managed repo."
823
+ ),
824
+ "input_schema": {
825
+ "type": "object",
826
+ "properties": {
827
+ "repo_name": {
828
+ "type": "string",
829
+ "description": "Name of the target repo (must match or fuzzy-match a repo in config/repos/).",
830
+ },
831
+ "task_type": {
832
+ "type": "string",
833
+ "enum": ["feature", "fix", "refactor", "chore"],
834
+ "description": "Type of task. Default: feature.",
835
+ },
836
+ "description": {
837
+ "type": "string",
838
+ "description": "Full, unambiguous description of what to implement or fix. "
839
+ "Include all details gathered from the human.",
840
+ },
841
+ "notify_user_ids": {
842
+ "type": "array",
843
+ "items": {"type": "string"},
844
+ "description": "Extra Slack user IDs to ping on completion (besides the submitter).",
845
+ },
846
+ },
847
+ "required": ["repo_name", "description"],
848
+ },
849
+ },
752
850
  {
753
851
  "name": "list_pending_prs",
754
852
  "description": "List all open Sentinel PRs awaiting admin review.",
@@ -1348,12 +1446,14 @@ _TOOLS = [
1348
1446
  {
1349
1447
  "name": "merge_pr",
1350
1448
  "description": (
1351
- "ADMIN ONLY. Merge a PR into the main branch. "
1352
- "ALWAYS call with confirmed=false first to show PR details for admin review — "
1449
+ "ADMIN ONLY. Merge a branch or PR into the main branch. "
1450
+ "ALWAYS call with confirmed=false first to show details for admin review — "
1353
1451
  "never merge without showing the plan first. "
1354
- "Use for Sentinel fix PRs (AUTO_PUBLISH=false) or Renovate/external PRs by number. "
1355
- "confirmed=false fetches and shows PR details; confirmed=true executes the merge. "
1356
- "e.g. 'merge the fix for Whydah-TypeLib', 'merge TypeLib PR #247'"
1452
+ "Use for Sentinel fix PRs (AUTO_PUBLISH=false), Renovate/external PRs by number, "
1453
+ "or any arbitrary branch by name (branch_name). "
1454
+ "confirmed=false fetches and shows details; confirmed=true executes the merge. "
1455
+ "e.g. 'merge the fix for Whydah-TypeLib', 'merge TypeLib PR #247', "
1456
+ "'merge branch fix/pin-header-geometry-sync into elprint-connector-service'"
1357
1457
  ),
1358
1458
  "input_schema": {
1359
1459
  "type": "object",
@@ -1371,10 +1471,16 @@ _TOOLS = [
1371
1471
  "description": "Merge a specific PR by number (e.g. a Renovate PR). "
1372
1472
  "When set, repo_name is still required but fingerprint is ignored.",
1373
1473
  },
1474
+ "branch_name": {
1475
+ "type": "string",
1476
+ "description": "Merge an arbitrary branch into the repo's main branch via "
1477
+ "GitHub Merge API. Use when the user gives a branch URL or name "
1478
+ "without an associated PR. e.g. 'fix/pin-header-geometry-sync'",
1479
+ },
1374
1480
  "confirmed": {
1375
1481
  "type": "boolean",
1376
1482
  "description": (
1377
- "false (default) = fetch PR details and show plan for review, do NOT merge yet. "
1483
+ "false (default) = fetch details and show plan for review, do NOT merge yet. "
1378
1484
  "true = execute the merge after admin has seen and approved the plan."
1379
1485
  ),
1380
1486
  },
@@ -1988,7 +2094,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
1988
2094
 
1989
2095
  if name == "dev_task":
1990
2096
  if not is_admin:
1991
- return json.dumps({"error": "Only admins can submit dev tasks to the Dev Claude agent."})
2097
+ return json.dumps({"error": "Only admins can submit dev tasks to Patch."})
1992
2098
 
1993
2099
  description = inputs.get("description", "").strip()
1994
2100
  if not description:
@@ -2014,14 +2120,21 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2014
2120
  ts = int(__import__("time").time())
2015
2121
  fname = f"slack-{_uuid.uuid4().hex[:8]}-{ts}.txt"
2016
2122
  fpath = dev_tasks_dir / fname
2123
+
2124
+ # Collect notify list: extra users requested + deduplicate
2125
+ notify_ids = inputs.get("notify_user_ids") or []
2126
+ if isinstance(notify_ids, list):
2127
+ notify_ids = [u for u in notify_ids if u and u != user_id]
2128
+
2017
2129
  lines = [
2018
2130
  f"TYPE: {task_type}",
2019
2131
  f"SUBMITTED_BY: <@{user_id}> ({user_id})",
2020
2132
  f"SOURCE: boss",
2021
2133
  f"SUBMITTED_AT: {_dt.now(_tz.utc).isoformat()}",
2022
- "",
2023
- description,
2024
2134
  ]
2135
+ if notify_ids:
2136
+ lines.append(f"NOTIFY: {','.join(notify_ids)}")
2137
+ lines += ["", description]
2025
2138
  fpath.write_text("\n".join(lines), encoding="utf-8")
2026
2139
  logger.info("Boss dev_task: dropped %s for user %s (type=%s)", fname, user_id, task_type)
2027
2140
 
@@ -2032,12 +2145,65 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2032
2145
  "file": fname,
2033
2146
  "task_type": task_type,
2034
2147
  "note": (
2035
- "Dev task queued — Dev Claude will pick it up on the next poll cycle "
2148
+ "Dev task queued — Patch will pick it up on the next poll cycle "
2036
2149
  "and post progress to this channel."
2037
2150
  ),
2038
2151
  })
2039
2152
 
2040
2153
 
2154
+ if name == "repo_task":
2155
+ if not is_admin:
2156
+ return json.dumps({"error": "Only admins can submit repo tasks."})
2157
+
2158
+ repo_name = inputs.get("repo_name", "").strip()
2159
+ description = inputs.get("description", "").strip()
2160
+ if not repo_name:
2161
+ return json.dumps({"error": "repo_name is required"})
2162
+ if not description:
2163
+ return json.dumps({"error": "description is required"})
2164
+ task_type = inputs.get("task_type", "feature").strip()
2165
+ if task_type not in ("feature", "fix", "refactor", "chore"):
2166
+ task_type = "feature"
2167
+
2168
+ # Fuzzy-match repo name
2169
+ if repo_name not in cfg_loader.repos:
2170
+ for rname in cfg_loader.repos:
2171
+ if repo_name.lower() in rname.lower() or rname.lower() in repo_name.lower():
2172
+ repo_name = rname
2173
+ break
2174
+ if repo_name not in cfg_loader.repos:
2175
+ return json.dumps({
2176
+ "error": f"No repo matching '{repo_name}' found.",
2177
+ "available_repos": list(cfg_loader.repos.keys()),
2178
+ })
2179
+
2180
+ notify_ids = inputs.get("notify_user_ids") or []
2181
+ if isinstance(notify_ids, list):
2182
+ notify_ids = [u for u in notify_ids if u and u != user_id]
2183
+
2184
+ _project_dirs = _find_project_dirs()
2185
+ if not _project_dirs:
2186
+ return json.dumps({"error": "No project directory found."})
2187
+
2188
+ from .repo_task_engine import drop_repo_task as _drop_repo_task
2189
+ task_file = _drop_repo_task(
2190
+ _project_dirs[0],
2191
+ repo_name=repo_name,
2192
+ task_type=task_type,
2193
+ description=description,
2194
+ submitter_user_id=user_id,
2195
+ notify_user_ids=notify_ids,
2196
+ )
2197
+ logger.info("Boss repo_task: dropped %s for user %s (repo=%s)", task_file.name, user_id, repo_name)
2198
+ return json.dumps({
2199
+ "status": "queued",
2200
+ "repo": repo_name,
2201
+ "task_type": task_type,
2202
+ "file": task_file.name,
2203
+ "note": f"Task queued for `{repo_name}` — Claude will implement and post progress here.",
2204
+ })
2205
+
2206
+
2041
2207
  if name == "get_fix_details":
2042
2208
  fp = inputs["fingerprint"]
2043
2209
  fix = store.get_confirmed_fix(fp) or store.get_marker_seen_fix(fp)
@@ -3252,6 +3418,92 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3252
3418
  repo_name = rname
3253
3419
  break
3254
3420
 
3421
+ branch_name = inputs.get("branch_name", "").strip()
3422
+
3423
+ # ── Path C: branch_name — merge via GitHub Merge API (no PR needed) ─
3424
+ if branch_name and not pr_number_in:
3425
+ repo_cfg = cfg_loader.repos.get(repo_name)
3426
+ owner_repo = ""
3427
+ if repo_cfg and repo_cfg.repo_url:
3428
+ url = repo_cfg.repo_url
3429
+ owner_repo = url.split(":")[-1].removesuffix(".git") if url.startswith("git@") \
3430
+ else "/".join(url.rstrip("/").split("/")[-2:]).removesuffix(".git")
3431
+ if not owner_repo:
3432
+ return json.dumps({"error": f"Cannot determine GitHub owner/repo for '{repo_name}'"})
3433
+
3434
+ base_branch = (repo_cfg.branch if repo_cfg else None) or "main"
3435
+
3436
+ # Fetch branch info to confirm it exists and get latest commit
3437
+ branch_resp = _req.get(
3438
+ f"https://api.github.com/repos/{owner_repo}/branches/{branch_name}",
3439
+ headers=headers, timeout=15,
3440
+ )
3441
+ if branch_resp.status_code == 404:
3442
+ return json.dumps({"error": f"Branch '{branch_name}' not found in {owner_repo}"})
3443
+ if branch_resp.status_code != 200:
3444
+ return json.dumps({"error": f"GitHub API error ({branch_resp.status_code}): {branch_resp.text[:200]}"})
3445
+ branch_data = branch_resp.json()
3446
+ head_sha = branch_data.get("commit", {}).get("sha", "")[:8]
3447
+ head_msg = branch_data.get("commit", {}).get("commit", {}).get("message", "").splitlines()[0]
3448
+
3449
+ # Compare branch vs base to get ahead/behind counts
3450
+ compare_resp = _req.get(
3451
+ f"https://api.github.com/repos/{owner_repo}/compare/{base_branch}...{branch_name}",
3452
+ headers=headers, timeout=15,
3453
+ )
3454
+ compare_data = compare_resp.json() if compare_resp.status_code == 200 else {}
3455
+ ahead_by = compare_data.get("ahead_by", "?")
3456
+ behind_by = compare_data.get("behind_by", "?")
3457
+ files_list = [f.get("filename", "") for f in compare_data.get("files", [])]
3458
+
3459
+ if not confirmed:
3460
+ return json.dumps({
3461
+ "plan": f"Merge branch '{branch_name}' into {repo_name}/{base_branch}",
3462
+ "branch": branch_name,
3463
+ "base": base_branch,
3464
+ "head_sha": head_sha,
3465
+ "head_commit": head_msg,
3466
+ "ahead_by": ahead_by,
3467
+ "behind_by": behind_by,
3468
+ "files": files_list,
3469
+ "confirm_prompt": (
3470
+ f"This will merge branch '{branch_name}' (ahead by {ahead_by} commit(s)) "
3471
+ f"into {repo_name}/{base_branch} via GitHub Merge API. "
3472
+ f"Reply with confirmed=true to proceed."
3473
+ ),
3474
+ })
3475
+
3476
+ merge_resp = _req.post(
3477
+ f"https://api.github.com/repos/{owner_repo}/merges",
3478
+ json={"base": base_branch, "head": branch_name,
3479
+ "commit_message": f"chore: merge branch '{branch_name}' into {base_branch}"},
3480
+ headers=headers, timeout=30,
3481
+ )
3482
+ if merge_resp.status_code == 201:
3483
+ sha = merge_resp.json().get("sha", "")[:8]
3484
+ logger.info("Boss merge_pr (branch): merged '%s' into %s/%s by %s sha=%s",
3485
+ branch_name, repo_name, base_branch, user_id, sha)
3486
+ return json.dumps({
3487
+ "status": "merged",
3488
+ "branch": branch_name,
3489
+ "base": base_branch,
3490
+ "sha": sha,
3491
+ "repo": repo_name,
3492
+ "note": f"Branch '{branch_name}' merged into {base_branch}. SHA: {sha}",
3493
+ })
3494
+ if merge_resp.status_code == 204:
3495
+ return json.dumps({
3496
+ "status": "already_merged",
3497
+ "note": f"Branch '{branch_name}' is already fully merged into {base_branch}.",
3498
+ })
3499
+ if merge_resp.status_code == 409:
3500
+ return json.dumps({
3501
+ "status": "conflict",
3502
+ "error": f"Merge conflict between '{branch_name}' and '{base_branch}'. Resolve manually on GitHub.",
3503
+ })
3504
+ return json.dumps({"status": "error",
3505
+ "error": f"GitHub API returned {merge_resp.status_code}: {merge_resp.text[:300]}"})
3506
+
3255
3507
  # ── Path A: explicit pr_number — fetch details, then merge ────────
3256
3508
  if pr_number_in:
3257
3509
  pr_number_in = int(pr_number_in)
@@ -1,26 +1,29 @@
1
1
  """
2
- sentinel_dev.py — Autonomous Developer Claude agent for Sentinel self-improvement.
2
+ sentinel_dev.py — Patch, the autonomous developer agent for Sentinel self-improvement.
3
3
 
4
- Dev Claude runs alongside Boss but independently. It watches dev-tasks/ for
4
+ Patch runs alongside Boss but independently. It watches dev-tasks/ for
5
5
  requests to improve Sentinel itself — new features, bug fixes, refactors — and
6
6
  executes them by running Claude Code against the Sentinel source repository.
7
7
 
8
+ Patch is an internal actor — humans never interact with it directly. All communication
9
+ goes through Boss, who qualifies Patch's outputs before surfacing anything to users.
10
+
8
11
  Invocation sources:
9
12
  - Boss (dev_task tool) → slack-<uuid>.txt in dev-tasks/
10
13
  - Fix engine (BOSS_ESCALATE output) → bot-<fp>-<ts>.txt in dev-tasks/
14
+ - Self-repair (log watcher) → self-<fp>-<ts>.txt in dev-tasks/
11
15
  - Admin (manual file drop) → any .txt in dev-tasks/
12
16
 
13
17
  After a successful task:
14
18
  - Commits changes to the Sentinel source repo
15
- - If SENTINEL_DEV_AUTO_PUBLISH=true: bumps patch version + npm publish + sentinel upgrade
16
- - Posts Slack notification with summary
19
+ - Changes are live immediately (Sentinel loads Python directly from source repo)
20
+ - Human reviews git log periodically → bumps version → npm publish → auto-upgrade distributes
17
21
  """
18
22
  from __future__ import annotations
19
23
 
20
24
  import logging
21
25
  import os
22
26
  import re
23
- import subprocess
24
27
  import time
25
28
 
26
29
  from datetime import datetime, timezone
@@ -33,6 +36,9 @@ from .notify import slack_alert
33
36
 
34
37
  logger = logging.getLogger(__name__)
35
38
 
39
+ # Internal name for the dev agent — used in logs and inter-agent communication
40
+ PATCH_NAME = "Patch"
41
+
36
42
  # Dev tasks are bigger than fix tasks — allow 15 minutes
37
43
  _DEV_TIMEOUT = 900
38
44
 
@@ -68,7 +74,7 @@ def _build_dev_prompt(task: DevTask, repo_path: str, past_outcomes: list | None
68
74
  memory_ctx = "\n".join(lines) + "\n\n"
69
75
 
70
76
  return (
71
- f"You are Sentinel Developer — a fully autonomous AI agent responsible for the\n"
77
+ f"You are Patch — a fully autonomous AI developer responsible for the\n"
72
78
  f"resiliency, stability, and continuous improvement of the Sentinel DevOps system.\n"
73
79
  f"Sentinel source repository: {repo_path}\n"
74
80
  f"{submitted}\n"
@@ -89,9 +95,10 @@ def _build_dev_prompt(task: DevTask, repo_path: str, past_outcomes: list | None
89
95
  f"You do NOT need permission for any of the above. Act, don't ask.\n"
90
96
  f"\n"
91
97
  f"You work WITH Sentinel Boss, not under it. The relationship:\n"
92
- f" Humans → Boss (Boss obeys humans)\n"
93
- f" Boss ↔ Dev Claude (peer agents — you can ask Boss, Boss can task you)\n"
94
- f" Dev Claude → Sentinel (full autonomy)\n"
98
+ f" Humans → Boss (Boss obeys humans, surfaces your work to humans)\n"
99
+ f" Boss ↔ Patch (peer agents — you can ask Boss, Boss can task you)\n"
100
+ f" Patch → Sentinel (full autonomy over Sentinel's codebase and all instances)\n"
101
+ f"Humans never interact with you directly — Boss is their only interface.\n"
95
102
  f"\n"
96
103
  f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
97
104
  f"SENTINEL CODEBASE OVERVIEW\n"
@@ -105,7 +112,7 @@ def _build_dev_prompt(task: DevTask, repo_path: str, past_outcomes: list | None
105
112
  f"- Log parsing: sentinel/log_parser.py\n"
106
113
  f"- Issue queue: sentinel/issue_watcher.py\n"
107
114
  f"- Dev task queue + self-repair: sentinel/dev_watcher.py\n"
108
- f"- Dev Claude agent: sentinel/sentinel_dev.py\n"
115
+ f"- Patch (you): sentinel/sentinel_dev.py\n"
109
116
  f"- State + memory: sentinel/state_store.py (SQLite, incl. dev_history)\n"
110
117
  f"- Notifications: sentinel/notify.py\n"
111
118
  f"- CLI package: cli/ (Node.js, npm package @misterhuydo/sentinel)\n"
@@ -127,10 +134,9 @@ def _build_dev_prompt(task: DevTask, repo_path: str, past_outcomes: list | None
127
134
  f"5. Commit all changes:\n"
128
135
  f" git add -A -- sentinel/ cli/ (or be more selective)\n"
129
136
  f" git commit -m \"{task.task_type}(dev-agent): <concise summary> [sentinel-dev]\"\n"
130
- f"6. If this is a meaningful new feature or fix (not a trivial chore), output on its own line:\n"
131
- f" VERSION_BUMPED: <new_version>\n"
132
- f" (The version is in cli/package.json bump the patch number)\n"
133
- f"7. End your response with a brief summary of what changed (max 10 lines).\n"
137
+ f"6. End your response with a brief summary of what changed (max 10 lines).\n"
138
+ f" Your changes are live immediately — Sentinel loads Python directly from this repo.\n"
139
+ f" A human will review your commits and publish to npm when ready.\n"
134
140
  f"\n"
135
141
  f"BOUNDARIES (the only things outside your scope):\n"
136
142
  f"- Never touch managed application repos (the repos Sentinel monitors — not this repo)\n"
@@ -149,12 +155,12 @@ def _build_dev_prompt(task: DevTask, repo_path: str, past_outcomes: list | None
149
155
  )
150
156
 
151
157
 
152
- _ASK_BOSS_RETRIES = 2 # how many times Dev Claude may ask Boss per task
158
+ _ASK_BOSS_RETRIES = 2 # how many times Patch may ask Boss per task
153
159
 
154
160
 
155
161
  def _consult_boss(question: str, task_context: str, cfg: "SentinelConfig") -> str:
156
162
  """
157
- Route a Dev Claude question to the Boss LLM.
163
+ Route a Patch question to the Boss LLM.
158
164
  Boss answers from its knowledge of Sentinel, or indicates it needs human input.
159
165
  Returns Boss's answer as a string.
160
166
  """
@@ -168,17 +174,17 @@ def _consult_boss(question: str, task_context: str, cfg: "SentinelConfig") -> st
168
174
  max_tokens=600,
169
175
  system=(
170
176
  "You are Sentinel Boss — the operational orchestrator of the Sentinel DevOps system. "
171
- "You are answering a question from Dev Claude, your peer AI agent who maintains "
172
- "Sentinel's source code autonomously. Dev Claude has full authority within Sentinel's "
177
+ "You are answering a question from Patch, your peer AI agent who maintains "
178
+ "Sentinel's source code autonomously. Patch has full authority within Sentinel's "
173
179
  "operational scope and only asks you when it truly needs information it cannot find itself.\n\n"
174
180
  "Answer concisely and directly. If you know the answer from Sentinel's architecture or "
175
181
  "standard practices, give it. If the question requires a human admin decision (e.g. "
176
182
  "secret credentials, budget approval, irreversible production changes), reply with:\n"
177
183
  "NEEDS_HUMAN: <brief reason>\n\n"
178
- "Context of the task Dev Claude is working on:\n"
184
+ "Context of the task Patch is working on:\n"
179
185
  f"{task_context[:400]}"
180
186
  ),
181
- messages=[{"role": "user", "content": f"Dev Claude asks: {question}"}],
187
+ messages=[{"role": "user", "content": f"Patch asks: {question}"}],
182
188
  )
183
189
  return _resp.content[0].text.strip() if _resp.content else "(no answer from Boss)"
184
190
  except Exception as _e:
@@ -186,11 +192,6 @@ def _consult_boss(question: str, task_context: str, cfg: "SentinelConfig") -> st
186
192
  return f"(Boss consultation failed: {_e})"
187
193
 
188
194
 
189
- def _extract_version_bumped(output: str) -> str | None:
190
- """Parse VERSION_BUMPED: <version> from Claude output."""
191
- m = re.search(r'^VERSION_BUMPED:\s*(\S+)', output, re.MULTILINE | re.IGNORECASE)
192
- return m.group(1) if m else None
193
-
194
195
 
195
196
  def _extract_summary(output: str) -> str:
196
197
  """Extract the last meaningful paragraph from Claude output (not tool-use lines)."""
@@ -198,40 +199,12 @@ def _extract_summary(output: str) -> str:
198
199
  substantive = [
199
200
  l for l in lines
200
201
  if l.strip() and not re.match(r'^[⏺⎆●✦✓✗]', l.strip())
201
- and not l.strip().startswith("VERSION_BUMPED")
202
202
  ]
203
203
  if not substantive:
204
204
  return output[-400:].strip()
205
205
  return "\n".join(substantive[-12:])[:500]
206
206
 
207
207
 
208
- def _run_npm_publish(repo_path: str, env: dict, on_progress=None) -> bool:
209
- """Run npm publish from cli/ directory. Returns True on success."""
210
- cli_dir = Path(repo_path) / "cli"
211
- if not cli_dir.exists():
212
- logger.warning("Dev agent: cli/ not found at %s", cli_dir)
213
- return False
214
- if on_progress:
215
- try:
216
- on_progress(":rocket: Publishing to npm...")
217
- except Exception:
218
- pass
219
- try:
220
- r = subprocess.run(
221
- ["npm", "publish", "--access", "public"],
222
- cwd=str(cli_dir),
223
- capture_output=True, text=True, timeout=120, env=env,
224
- )
225
- if r.returncode == 0:
226
- logger.info("Dev agent: npm publish succeeded")
227
- return True
228
- logger.error("Dev agent: npm publish failed (rc=%d): %s", r.returncode, r.stderr[:300])
229
- return False
230
- except Exception as e:
231
- logger.error("Dev agent: npm publish error: %s", e)
232
- return False
233
-
234
-
235
208
 
236
209
  def _dev_progress_from_line(line: str) -> str | None:
237
210
  """Convert Claude Code tool-use lines to human-readable dev progress messages."""
@@ -370,7 +343,7 @@ def run_dev_task(
370
343
 
371
344
  if timed_out:
372
345
  logger.error("Dev agent: Claude timed out for task %s", task.fingerprint[:8])
373
- return "error", "Dev Claude timed out after 15 minutes."
346
+ return "error", "Patch timed out after 15 minutes."
374
347
 
375
348
  stripped = output.strip()
376
349
 
@@ -416,7 +389,7 @@ def run_dev_task(
416
389
  else:
417
390
  # Exhausted retries — treat as needs_human (Boss couldn't unblock Dev Claude)
418
391
  _record("needs_human", note="Exhausted Boss consultations without completing task")
419
- return "needs_human", "Dev Claude could not complete the task after consulting Boss. Human review needed."
392
+ return "needs_human", "Patch could not complete the task after consulting Boss. Human review needed."
420
393
 
421
394
  if stripped.upper().startswith("SKIP:"):
422
395
  reason = stripped[5:].strip()
@@ -424,30 +397,15 @@ def run_dev_task(
424
397
  _record("skip", note=reason[:400])
425
398
  return "skip", reason
426
399
 
427
- # Parse optional VERSION_BUMPED signal and summary
428
- new_version = _extract_version_bumped(output)
429
400
  summary = _extract_summary(output)
430
-
431
- logger.info(
432
- "Dev agent: task %s completed (version_bumped=%s)",
433
- task.fingerprint[:8], new_version or "no",
434
- )
401
+ logger.info("Dev agent: task %s completed", task.fingerprint[:8])
435
402
 
436
403
  # Extract files changed from git output in Claude's response (best-effort)
437
404
  _files_re = re.findall(r'\bsentinel/\S+\.py\b|\bcli/\S+\.(?:js|json)\b', output)
438
405
  files_str = ", ".join(dict.fromkeys(_files_re))[:300]
439
406
 
440
- # Post-execution: publish to npm if configured and version was bumped
441
- if cfg.sentinel_dev_auto_publish and new_version:
442
- published = _run_npm_publish(repo_path, base_env, on_progress)
443
- if published:
444
- _record("published", note=summary, files=files_str, commit=new_version)
445
- return "published", new_version
446
- _record("done", note=summary, files=files_str, commit=new_version)
447
- return "done", new_version
448
-
449
407
  _record("done", note=summary, files=files_str)
450
- return "done", new_version or ""
408
+ return "done", ""
451
409
 
452
410
 
453
411
  def _fire_progress(line: str, on_progress) -> None: