agent-control-plane 0.3.0 → 0.6.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.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def split_key_value_text(text: str) -> list[str]:
11
+ return [line.strip() for line in re.split(r"(?:\r?\n|\\n)+", text) if line.strip()]
12
+
13
+
14
+ def normalize_value(raw: str) -> str:
15
+ value = raw.strip()
16
+ if value in {"''", '""'}:
17
+ return ""
18
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
19
+ return value[1:-1]
20
+ return value
21
+
22
+
23
+ def parse_key_value_text(text: str) -> dict[str, str]:
24
+ data: dict[str, str] = {}
25
+ for line in split_key_value_text(text):
26
+ if "=" not in line:
27
+ continue
28
+ key, value = line.split("=", 1)
29
+ data[key.strip()] = normalize_value(value)
30
+ return data
31
+
32
+
33
+ def read_env_file(path: Path) -> dict[str, str]:
34
+ if not path.is_file():
35
+ return {}
36
+ return parse_key_value_text(path.read_text(encoding="utf-8", errors="replace"))
37
+
38
+
39
+ def file_mtime_iso(path: Path) -> str:
40
+ return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
41
+
42
+
43
+ def parse_issue_queue_filename(path: Path) -> tuple[str, str]:
44
+ name = path.name
45
+ if name.endswith(".env"):
46
+ name = name[:-4]
47
+ if not name.startswith("issue-"):
48
+ return "", ""
49
+
50
+ payload = name[len("issue-") :]
51
+ if "." not in payload:
52
+ return payload, ""
53
+
54
+ issue_id, remainder = payload.split(".", 1)
55
+ remainder_parts = remainder.split(".")
56
+ if len(remainder_parts) >= 2:
57
+ return issue_id, ".".join(remainder_parts[:-1])
58
+ return issue_id, remainder
59
+
60
+
61
+ def is_pending_queue_file(path: Path) -> bool:
62
+ return path.is_file() and path.name.startswith("issue-") and path.name.endswith(".env") and ".tmp." not in path.name
63
+
64
+
65
+ def is_claim_queue_file(path: Path) -> bool:
66
+ return path.is_file() and path.name.startswith("issue-") and ".tmp." not in path.name
67
+
68
+
69
+ def collect_queue_items(root: Path, kind: str) -> list[dict[str, Any]]:
70
+ if not root.is_dir():
71
+ return []
72
+
73
+ matcher = is_pending_queue_file if kind == "pending" else is_claim_queue_file
74
+ items: list[dict[str, Any]] = []
75
+ for path in sorted((item for item in root.iterdir() if matcher(item)), key=lambda item: item.stat().st_mtime, reverse=True):
76
+ env = read_env_file(path)
77
+ issue_id_from_name, claimer_from_name = parse_issue_queue_filename(path)
78
+ claim_file = env.get("CLAIM_FILE", "")
79
+ state_kind = env.get("STATE_KIND", "")
80
+ items.append(
81
+ {
82
+ "issue_id": env.get("ISSUE_ID", "") or issue_id_from_name,
83
+ "session": env.get("SESSION", "") or claimer_from_name,
84
+ "claim_file": claim_file or (str(path) if kind == "claims" else ""),
85
+ "queued_by": env.get("QUEUED_BY", ""),
86
+ "claimed_by": env.get("CLAIMED_BY", "") or claimer_from_name,
87
+ "state_kind": state_kind or ("claim" if kind == "claims" else "pending"),
88
+ "state_format_version": env.get("STATE_FORMAT_VERSION", ""),
89
+ "updated_at": env.get("UPDATED_AT", "") or env.get("CLAIMED_AT", "") or env.get("QUEUED_AT", "") or file_mtime_iso(path),
90
+ "state_file": str(path),
91
+ }
92
+ )
93
+ return items
94
+
95
+
96
+ def collect_issue_queue(state_root: Path) -> dict[str, list[dict[str, Any]]]:
97
+ queue_root = state_root / "resident-workers" / "issue-queue"
98
+ return {
99
+ "pending": collect_queue_items(queue_root / "pending", "pending"),
100
+ "claims": collect_queue_items(queue_root / "claims", "claims"),
101
+ }
@@ -4,14 +4,18 @@ from __future__ import annotations
4
4
  import argparse
5
5
  import json
6
6
  import os
7
+ import subprocess
7
8
  from functools import partial
8
9
  from http import HTTPStatus
9
10
  from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
10
11
  from pathlib import Path
11
- from urllib.parse import urlparse
12
+ from urllib.parse import urlparse, parse_qs
12
13
 
13
14
  from dashboard_snapshot import build_snapshot
14
15
 
16
+ ROOT_DIR = Path(__file__).resolve().parents[2]
17
+ TOOLS_BIN_DIR = ROOT_DIR / "tools" / "bin"
18
+
15
19
 
16
20
  DASHBOARD_DIR = Path(__file__).resolve().parent
17
21
 
@@ -31,6 +35,124 @@ class DashboardHandler(SimpleHTTPRequestHandler):
31
35
  self.end_headers()
32
36
  self.wfile.write(encoded)
33
37
  return
38
+ if parsed.path == "/api/doctor":
39
+ query = parse_qs(parsed.query)
40
+ profile_id = (query.get("profile_id") or [""])[0]
41
+ if not profile_id:
42
+ self.send_response(HTTPStatus.BAD_REQUEST)
43
+ self.send_header("Content-Type", "application/json")
44
+ self.end_headers()
45
+ self.wfile.write(json.dumps({"error": "profile_id is required"}).encode("utf-8"))
46
+ return
47
+ doctor_script = TOOLS_BIN_DIR / "flow-runtime-doctor.sh"
48
+ if not doctor_script.is_file():
49
+ self.send_response(HTTPStatus.NOT_FOUND)
50
+ self.send_header("Content-Type", "application/json")
51
+ self.end_headers()
52
+ self.wfile.write(json.dumps({"error": "doctor script not found"}).encode("utf-8"))
53
+ return
54
+ try:
55
+ env = os.environ.copy()
56
+ env["ACP_PROJECT_ID"] = profile_id
57
+ output = subprocess.check_output(
58
+ ["bash", str(doctor_script)],
59
+ cwd=str(ROOT_DIR),
60
+ env=env,
61
+ text=True,
62
+ stderr=subprocess.STDOUT,
63
+ timeout=120,
64
+ )
65
+ payload = {"output": output}
66
+ encoded = json.dumps(payload).encode("utf-8")
67
+ self.send_response(HTTPStatus.OK)
68
+ self.send_header("Content-Type", "application/json; charset=utf-8")
69
+ self.send_header("Content-Length", str(len(encoded)))
70
+ self.end_headers()
71
+ self.wfile.write(encoded)
72
+ except subprocess.TimeoutExpired:
73
+ self.send_response(HTTPStatus.GATEWAY_TIMEOUT)
74
+ self.send_header("Content-Type", "application/json")
75
+ self.end_headers()
76
+ self.wfile.write(json.dumps({"error": "doctor timed out"}).encode("utf-8"))
77
+ except subprocess.CalledProcessError as exc:
78
+ payload = {"error": exc.returncode, "output": exc.output}
79
+ encoded = json.dumps(payload).encode("utf-8")
80
+ self.send_response(HTTPStatus.OK)
81
+ self.send_header("Content-Type", "application/json; charset=utf-8")
82
+ self.send_header("Content-Length", str(len(encoded)))
83
+ self.end_headers()
84
+ self.wfile.write(encoded)
85
+ return
86
+ if parsed.path == "/api/profile/export":
87
+ query = parse_qs(parsed.query)
88
+ profile_id = (query.get("profile_id") or [""])[0]
89
+ if not profile_id:
90
+ self.send_response(HTTPStatus.BAD_REQUEST)
91
+ self.send_header("Content-Type", "application/json")
92
+ self.end_headers()
93
+ self.wfile.write(json.dumps({"error": "profile_id is required"}).encode("utf-8"))
94
+ return
95
+ registry_root = Path(os.environ.get("ACP_PROFILE_REGISTRY_ROOT", str(Path.home() / ".agent-runtime" / "control-plane" / "profiles")))
96
+ profile_dir = registry_root / profile_id
97
+ config_file = profile_dir / "control-plane.yaml"
98
+ if not config_file.is_file():
99
+ self.send_response(HTTPStatus.NOT_FOUND)
100
+ self.send_header("Content-Type", "application/json")
101
+ self.end_headers()
102
+ self.wfile.write(json.dumps({"error": "profile config not found"}).encode("utf-8"))
103
+ return
104
+ try:
105
+ config = config_file.read_text(encoding="utf-8")
106
+ payload = {"profile_id": profile_id, "config": config, "config_file": str(config_file)}
107
+ encoded = json.dumps(payload).encode("utf-8")
108
+ self.send_response(HTTPStatus.OK)
109
+ self.send_header("Content-Type", "application/json; charset=utf-8")
110
+ self.send_header("Content-Length", str(len(encoded)))
111
+ self.end_headers()
112
+ self.wfile.write(encoded)
113
+ except Exception as exc:
114
+ self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
115
+ self.send_header("Content-Type", "application/json")
116
+ self.end_headers()
117
+ self.wfile.write(json.dumps({"error": str(exc)}).encode("utf-8"))
118
+ return
119
+ if parsed.path == "/api/profile/import":
120
+ if self.command != "POST":
121
+ self.send_response(HTTPStatus.METHOD_NOT_ALOWED)
122
+ self.send_header("Content-Type", "application/json")
123
+ self.end_headers()
124
+ self.wfile.write(json.dumps({"error": "POST required"}).encode("utf-8"))
125
+ return
126
+ try:
127
+ content_length = int(self.headers.get("Content-Length", 0))
128
+ body = self.rfile.read(content_length)
129
+ data = json.loads(body)
130
+ profile_id = data.get("profile_id", "")
131
+ config = data.get("config", "")
132
+ if not profile_id or not config:
133
+ self.send_response(HTTPStatus.BAD_REQUEST)
134
+ self.send_header("Content-Type", "application/json")
135
+ self.end_headers()
136
+ self.wfile.write(json.dumps({"error": "profile_id and config required"}).encode("utf-8"))
137
+ return
138
+ registry_root = Path(os.environ.get("ACP_PROFILE_REGISTRY_ROOT", str(Path.home() / ".agent-runtime" / "control-plane" / "profiles")))
139
+ profile_dir = registry_root / profile_id
140
+ config_file = profile_dir / "control-plane.yaml"
141
+ profile_dir.mkdir(parents=True, exist_ok=True)
142
+ config_file.write_text(config, encoding="utf-8")
143
+ payload = {"status": "ok", "profile_id": profile_id, "config_file": str(config_file)}
144
+ encoded = json.dumps(payload).encode("utf-8")
145
+ self.send_response(HTTPStatus.OK)
146
+ self.send_header("Content-Type", "application/json; charset=utf-8")
147
+ self.send_header("Content-Length", str(len(encoded)))
148
+ self.end_headers()
149
+ self.wfile.write(encoded)
150
+ except Exception as exc:
151
+ self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
152
+ self.send_header("Content-Type", "application/json")
153
+ self.end_headers()
154
+ self.wfile.write(json.dumps({"error": str(exc)}).encode("utf-8"))
155
+ return
34
156
  return super().do_GET()
35
157
 
36
158
  def end_headers(self) -> None: