agent-control-plane 0.1.9 → 0.1.12
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/hooks/heartbeat-hooks.sh +97 -8
- package/package.json +8 -2
- package/references/commands.md +1 -0
- package/tools/bin/agent-project-cleanup-session +133 -0
- package/tools/bin/agent-project-publish-issue-pr +178 -62
- package/tools/bin/agent-project-reconcile-issue-session +171 -3
- package/tools/bin/agent-project-run-codex-resilient +121 -16
- package/tools/bin/agent-project-run-codex-session +60 -10
- package/tools/bin/agent-project-run-openclaw-session +82 -8
- package/tools/bin/cleanup-worktree.sh +4 -1
- package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
- package/tools/bin/ensure-runtime-sync.sh +182 -0
- package/tools/bin/flow-config-lib.sh +76 -30
- package/tools/bin/flow-resident-worker-lib.sh +28 -2
- package/tools/bin/flow-shell-lib.sh +15 -1
- package/tools/bin/heartbeat-safe-auto.sh +32 -0
- package/tools/bin/issue-publish-localization-guard.sh +142 -0
- package/tools/bin/project-launchd-bootstrap.sh +17 -4
- package/tools/bin/project-runtime-supervisor.sh +7 -1
- package/tools/bin/project-runtimectl.sh +78 -15
- package/tools/bin/reuse-issue-worktree.sh +46 -0
- package/tools/bin/start-issue-worker.sh +76 -6
- package/tools/bin/start-resident-issue-loop.sh +1 -0
- package/tools/bin/sync-shared-agent-home.sh +26 -0
- package/tools/bin/test-smoke.sh +6 -1
- package/tools/dashboard/app.js +71 -1
- package/tools/dashboard/dashboard_snapshot.py +74 -0
- package/tools/dashboard/styles.css +43 -0
- package/tools/templates/issue-prompt-template.md +18 -66
- package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
- package/bin/audit-issue-routing.sh +0 -74
- package/tools/bin/audit-agent-worktrees.sh +0 -310
- package/tools/bin/audit-issue-routing.sh +0 -11
- package/tools/bin/audit-retained-layout.sh +0 -58
- package/tools/bin/audit-retained-overlap.sh +0 -135
- package/tools/bin/audit-retained-worktrees.sh +0 -228
- package/tools/bin/check-skill-contracts.sh +0 -324
package/tools/dashboard/app.js
CHANGED
|
@@ -2,6 +2,8 @@ const refreshButton = document.querySelector("#refresh-button");
|
|
|
2
2
|
const generatedAtNode = document.querySelector("#generated-at");
|
|
3
3
|
const overviewNode = document.querySelector("#overview");
|
|
4
4
|
const profilesNode = document.querySelector("#profiles");
|
|
5
|
+
const seenAlertIds = new Set();
|
|
6
|
+
let notificationPermissionRequested = false;
|
|
5
7
|
|
|
6
8
|
function relativeTime(input) {
|
|
7
9
|
if (!input) return "n/a";
|
|
@@ -61,9 +63,10 @@ function renderOverview(snapshot) {
|
|
|
61
63
|
acc.controllers += profile.counts.live_resident_controllers;
|
|
62
64
|
acc.cooldowns += profile.counts.provider_cooldowns;
|
|
63
65
|
acc.queue += profile.counts.queued_issues;
|
|
66
|
+
acc.alerts += profile.counts.alerts || 0;
|
|
64
67
|
return acc;
|
|
65
68
|
},
|
|
66
|
-
{ activeRuns: 0, runningRuns: 0, implementedRuns: 0, reportedRuns: 0, blockedRuns: 0, controllers: 0, cooldowns: 0, queue: 0 },
|
|
69
|
+
{ activeRuns: 0, runningRuns: 0, implementedRuns: 0, reportedRuns: 0, blockedRuns: 0, controllers: 0, cooldowns: 0, queue: 0, alerts: 0 },
|
|
67
70
|
);
|
|
68
71
|
|
|
69
72
|
overviewNode.innerHTML = [
|
|
@@ -75,6 +78,7 @@ function renderOverview(snapshot) {
|
|
|
75
78
|
["Blocked", totals.blockedRuns],
|
|
76
79
|
["Live Controllers", totals.controllers],
|
|
77
80
|
["Provider Cooldowns", totals.cooldowns],
|
|
81
|
+
["Alerts", totals.alerts],
|
|
78
82
|
["Queued Issues", totals.queue],
|
|
79
83
|
]
|
|
80
84
|
.map(
|
|
@@ -104,6 +108,36 @@ function renderTable(columns, rows, emptyMessage = "No data right now.") {
|
|
|
104
108
|
return `<div class="table-wrap"><table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table></div>`;
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
function renderAlerts(alerts) {
|
|
112
|
+
if (!alerts.length) {
|
|
113
|
+
return `<div class="empty-state">No active alerts for this profile.</div>`;
|
|
114
|
+
}
|
|
115
|
+
return `
|
|
116
|
+
<div class="alert-list">
|
|
117
|
+
${alerts
|
|
118
|
+
.map(
|
|
119
|
+
(alert) => `
|
|
120
|
+
<article class="alert-card ${statusClass(alert.severity || "warn")}">
|
|
121
|
+
<div class="alert-header">
|
|
122
|
+
<div>
|
|
123
|
+
<h4>${alert.title}</h4>
|
|
124
|
+
<div class="muted mono">${alert.session || "n/a"} · ${alert.task_kind || "task"} ${alert.task_id || ""}</div>
|
|
125
|
+
</div>
|
|
126
|
+
<span class="badge warn">${alert.kind}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<p>${alert.message}</p>
|
|
129
|
+
<div class="alert-meta">
|
|
130
|
+
<span>${alert.reset_at ? `Reset: ${alert.reset_at}` : "Reset: n/a"}</span>
|
|
131
|
+
<span>${alert.updated_at ? `${relativeTime(alert.updated_at)} · ${alert.updated_at}` : "updated n/a"}</span>
|
|
132
|
+
</div>
|
|
133
|
+
</article>
|
|
134
|
+
`,
|
|
135
|
+
)
|
|
136
|
+
.join("")}
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
107
141
|
function renderProfile(profile) {
|
|
108
142
|
const providerBadges = [
|
|
109
143
|
profile.coding_worker ? `<span class="badge good">${profile.coding_worker}</span>` : "",
|
|
@@ -125,6 +159,7 @@ function renderProfile(profile) {
|
|
|
125
159
|
["Live controllers", profile.counts.live_resident_controllers],
|
|
126
160
|
["Stale controllers", profile.counts.stale_resident_controllers],
|
|
127
161
|
["Provider cooldowns", profile.counts.provider_cooldowns],
|
|
162
|
+
["Alerts", profile.counts.alerts || 0],
|
|
128
163
|
["Issue retries", profile.counts.active_retries],
|
|
129
164
|
["Queued issues", profile.counts.queued_issues],
|
|
130
165
|
["Scheduled", profile.counts.scheduled_issues],
|
|
@@ -240,6 +275,11 @@ function renderProfile(profile) {
|
|
|
240
275
|
</header>
|
|
241
276
|
<section class="overview">${summaryCards}</section>
|
|
242
277
|
<section class="profile-grid">
|
|
278
|
+
<section class="panel">
|
|
279
|
+
<h3>Host Alerts</h3>
|
|
280
|
+
<p class="panel-subtitle">High-signal operational blockers surfaced from active run logs and comment artifacts.</p>
|
|
281
|
+
${renderAlerts(profile.alerts || [])}
|
|
282
|
+
</section>
|
|
243
283
|
<section class="panel">
|
|
244
284
|
<h3>Active Runs</h3>
|
|
245
285
|
<p class="panel-subtitle">Lifecycle shows technical session completion. Result shows what the run achieved: implemented, reported, or blocked.</p>
|
|
@@ -275,6 +315,35 @@ function renderProfile(profile) {
|
|
|
275
315
|
`;
|
|
276
316
|
}
|
|
277
317
|
|
|
318
|
+
async function maybeNotifyAlerts(snapshot) {
|
|
319
|
+
const alerts = (snapshot.alerts || []).filter((alert) => alert && alert.id);
|
|
320
|
+
if (!alerts.length || typeof window.Notification === "undefined") return;
|
|
321
|
+
|
|
322
|
+
if (window.Notification.permission === "default" && !notificationPermissionRequested) {
|
|
323
|
+
notificationPermissionRequested = true;
|
|
324
|
+
try {
|
|
325
|
+
await window.Notification.requestPermission();
|
|
326
|
+
} catch (_error) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (window.Notification.permission !== "granted") return;
|
|
332
|
+
|
|
333
|
+
for (const alert of alerts) {
|
|
334
|
+
if (seenAlertIds.has(alert.id)) continue;
|
|
335
|
+
seenAlertIds.add(alert.id);
|
|
336
|
+
const bodyParts = [];
|
|
337
|
+
if (alert.session) bodyParts.push(alert.session);
|
|
338
|
+
if (alert.reset_at) bodyParts.push(`reset ${alert.reset_at}`);
|
|
339
|
+
if (alert.message) bodyParts.push(alert.message);
|
|
340
|
+
new window.Notification(alert.title || "ACP alert", {
|
|
341
|
+
body: bodyParts.join(" · ").slice(0, 240),
|
|
342
|
+
tag: alert.id,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
278
347
|
async function loadSnapshot() {
|
|
279
348
|
refreshButton.disabled = true;
|
|
280
349
|
try {
|
|
@@ -286,6 +355,7 @@ async function loadSnapshot() {
|
|
|
286
355
|
generatedAtNode.textContent = `Snapshot: ${snapshot.generated_at}`;
|
|
287
356
|
renderOverview(snapshot);
|
|
288
357
|
profilesNode.innerHTML = snapshot.profiles.map(renderProfile).join("");
|
|
358
|
+
await maybeNotifyAlerts(snapshot);
|
|
289
359
|
} catch (error) {
|
|
290
360
|
generatedAtNode.textContent = `Snapshot load failed: ${error.message}`;
|
|
291
361
|
profilesNode.innerHTML = `<article class="profile"><div class="empty-state">${error.message}</div></article>`;
|
|
@@ -143,6 +143,19 @@ def file_mtime_iso(path: Path) -> str:
|
|
|
143
143
|
return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def read_tail_text(path: Path, max_bytes: int = 65536) -> str:
|
|
147
|
+
if not path.is_file():
|
|
148
|
+
return ""
|
|
149
|
+
try:
|
|
150
|
+
with path.open("rb") as handle:
|
|
151
|
+
size = path.stat().st_size
|
|
152
|
+
if size > max_bytes:
|
|
153
|
+
handle.seek(size - max_bytes)
|
|
154
|
+
return handle.read().decode("utf-8", errors="replace")
|
|
155
|
+
except OSError:
|
|
156
|
+
return ""
|
|
157
|
+
|
|
158
|
+
|
|
146
159
|
def classify_run_result(status: str, outcome: str, failure_reason: str) -> tuple[str, str]:
|
|
147
160
|
normalized_status = (status or "").strip().upper()
|
|
148
161
|
normalized_outcome = (outcome or "").strip()
|
|
@@ -167,6 +180,59 @@ def classify_run_result(status: str, outcome: str, failure_reason: str) -> tuple
|
|
|
167
180
|
return ("unknown", normalized_status or "Unknown")
|
|
168
181
|
|
|
169
182
|
|
|
183
|
+
GITHUB_RATE_LIMIT_PATTERNS = [
|
|
184
|
+
re.compile(
|
|
185
|
+
r"GitHub core API[^\n]*?rate limit[^\n]*(?:reset(?:s| into)?(?: at)?\s+(?P<reset>[^.\n]+))?",
|
|
186
|
+
re.IGNORECASE,
|
|
187
|
+
),
|
|
188
|
+
re.compile(
|
|
189
|
+
r"gh:\s*API rate limit exceeded[^\n]*(?:reset(?:s| into)?(?: at)?\s+(?P<reset>[^.\n]+))?",
|
|
190
|
+
re.IGNORECASE,
|
|
191
|
+
),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def summarize_whitespace(text: str) -> str:
|
|
196
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def extract_github_rate_limit_alert(run_dir: Path, run: dict[str, Any]) -> dict[str, Any] | None:
|
|
200
|
+
candidate_files = [
|
|
201
|
+
run_dir / "issue-comment.md",
|
|
202
|
+
run_dir / "pr-comment.md",
|
|
203
|
+
run_dir / f"{run['session']}.log",
|
|
204
|
+
]
|
|
205
|
+
for path in candidate_files:
|
|
206
|
+
text = read_tail_text(path)
|
|
207
|
+
if not text:
|
|
208
|
+
continue
|
|
209
|
+
for pattern in GITHUB_RATE_LIMIT_PATTERNS:
|
|
210
|
+
match = pattern.search(text)
|
|
211
|
+
if not match:
|
|
212
|
+
continue
|
|
213
|
+
summary = summarize_whitespace(match.group(0))
|
|
214
|
+
reset_match = re.search(r"reset(?:s| into)?(?: at)?\s+([^.\n]+)", summary, re.IGNORECASE)
|
|
215
|
+
reset_at = summarize_whitespace((reset_match.group(1) if reset_match else "") or match.groupdict().get("reset") or "")
|
|
216
|
+
if not summary:
|
|
217
|
+
summary = "GitHub core API rate limit is blocking host-side actions."
|
|
218
|
+
if reset_at and reset_at not in summary:
|
|
219
|
+
summary = f"{summary} Reset: {reset_at}."
|
|
220
|
+
return {
|
|
221
|
+
"id": f"github-core-rate-limit:{run['session']}:{reset_at or path.name}",
|
|
222
|
+
"kind": "github-core-rate-limit",
|
|
223
|
+
"severity": "warn",
|
|
224
|
+
"title": "GitHub core API rate limit blocks host actions",
|
|
225
|
+
"message": summary,
|
|
226
|
+
"session": run.get("session", ""),
|
|
227
|
+
"task_kind": run.get("task_kind", ""),
|
|
228
|
+
"task_id": run.get("task_id", ""),
|
|
229
|
+
"reset_at": reset_at,
|
|
230
|
+
"updated_at": run.get("updated_at", "") or file_mtime_iso(path),
|
|
231
|
+
"source_file": str(path),
|
|
232
|
+
}
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
170
236
|
def collect_runs(runs_root: Path) -> list[dict[str, Any]]:
|
|
171
237
|
if not runs_root.is_dir():
|
|
172
238
|
return []
|
|
@@ -221,6 +287,8 @@ def collect_runs(runs_root: Path) -> list[dict[str, Any]]:
|
|
|
221
287
|
"provider_pool_name": run_env.get("ACTIVE_PROVIDER_POOL_NAME", ""),
|
|
222
288
|
"run_dir": str(run_dir),
|
|
223
289
|
}
|
|
290
|
+
alert = extract_github_rate_limit_alert(run_dir, item)
|
|
291
|
+
item["alerts"] = [alert] if alert else []
|
|
224
292
|
runs.append(item)
|
|
225
293
|
return runs
|
|
226
294
|
|
|
@@ -430,6 +498,7 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
430
498
|
scheduled = collect_scheduled_issues(state_root)
|
|
431
499
|
retries = collect_issue_retries(state_root)
|
|
432
500
|
queue = collect_issue_queue(state_root)
|
|
501
|
+
alerts = [alert for run in runs for alert in run.get("alerts", [])]
|
|
433
502
|
|
|
434
503
|
return {
|
|
435
504
|
"id": profile_id,
|
|
@@ -471,8 +540,10 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
471
540
|
"provider_cooldowns": sum(1 for item in cooldowns if item["active"]),
|
|
472
541
|
"active_retries": sum(1 for item in retries if not item.get("ready", True)),
|
|
473
542
|
"scheduled_issues": len(scheduled),
|
|
543
|
+
"alerts": len(alerts),
|
|
474
544
|
},
|
|
475
545
|
"runs": runs,
|
|
546
|
+
"alerts": alerts,
|
|
476
547
|
"resident_controllers": controllers,
|
|
477
548
|
"resident_workers": resident_workers,
|
|
478
549
|
"provider_cooldowns": cooldowns,
|
|
@@ -485,11 +556,14 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
485
556
|
def build_snapshot() -> dict[str, Any]:
|
|
486
557
|
registry_root = profile_registry_root()
|
|
487
558
|
profiles = [build_profile_snapshot(profile_id, registry_root) for profile_id in list_profile_ids(registry_root)]
|
|
559
|
+
alerts = [alert for profile in profiles for alert in profile.get("alerts", [])]
|
|
488
560
|
return {
|
|
489
561
|
"generated_at": utc_now_iso(),
|
|
490
562
|
"flow_skill_dir": str(ROOT_DIR),
|
|
491
563
|
"profile_registry_root": str(registry_root),
|
|
492
564
|
"profile_count": len(profiles),
|
|
565
|
+
"alert_count": len(alerts),
|
|
566
|
+
"alerts": alerts,
|
|
493
567
|
"profiles": profiles,
|
|
494
568
|
}
|
|
495
569
|
|
|
@@ -230,6 +230,49 @@ button:hover {
|
|
|
230
230
|
font-size: 14px;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
.alert-list {
|
|
234
|
+
display: grid;
|
|
235
|
+
gap: 12px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.alert-card {
|
|
239
|
+
padding: 14px 16px;
|
|
240
|
+
border-radius: 18px;
|
|
241
|
+
border: 1px solid var(--line);
|
|
242
|
+
background: var(--panel-strong);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.alert-card.warn {
|
|
246
|
+
border-color: #e7c76d;
|
|
247
|
+
background: #fff6dd;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.alert-card h4 {
|
|
251
|
+
margin: 0;
|
|
252
|
+
font-size: 17px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.alert-card p {
|
|
256
|
+
margin: 10px 0 0;
|
|
257
|
+
line-height: 1.5;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.alert-header {
|
|
261
|
+
display: flex;
|
|
262
|
+
gap: 12px;
|
|
263
|
+
justify-content: space-between;
|
|
264
|
+
align-items: flex-start;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.alert-meta {
|
|
268
|
+
margin-top: 10px;
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-wrap: wrap;
|
|
271
|
+
gap: 12px;
|
|
272
|
+
color: var(--muted);
|
|
273
|
+
font-size: 13px;
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
.table-wrap {
|
|
234
277
|
overflow-x: auto;
|
|
235
278
|
}
|
|
@@ -12,67 +12,28 @@ Implement issue #{ISSUE_ID} in `{REPO_SLUG}`.
|
|
|
12
12
|
{ISSUE_RECURRING_CONTEXT}
|
|
13
13
|
{ISSUE_BLOCKER_CONTEXT}
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Required Contract
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Follow this order:
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
- Treat a broad umbrella issue as a coordination brief rather than permission to ship every slice in one PR.
|
|
28
|
-
- Write down your scope decision before coding.
|
|
29
|
-
|
|
30
|
-
## Phase 2: IMPLEMENT
|
|
31
|
-
|
|
32
|
-
- Make the smallest root-cause fix that satisfies the issue.
|
|
33
|
-
- Work only inside the dedicated worktree.
|
|
34
|
-
- Add or update tests when feasible.
|
|
35
|
-
- STOP after implementation. Do not commit yet.
|
|
36
|
-
|
|
37
|
-
## Phase 3: VERIFY (MANDATORY)
|
|
38
|
-
|
|
39
|
-
After implementing, you MUST run verification commands and record each one.
|
|
40
|
-
Every successful command must be recorded or the host publish will fail.
|
|
41
|
-
After each successful verification command, record it with `record-verification.sh`.
|
|
19
|
+
1. Read `AGENTS.md`, choose one narrow target, and stay within that slice.
|
|
20
|
+
2. If the issue is broader than one safe slice, stop and create follow-up issues instead of forcing a large patch:
|
|
21
|
+
```bash
|
|
22
|
+
bash "$ACP_FLOW_TOOLS_DIR/create-follow-up-issue.sh" --parent {ISSUE_ID} --title "..." --body-file /tmp/follow-up.md
|
|
23
|
+
```
|
|
24
|
+
3. Implement the smallest root-cause fix in this worktree only.
|
|
25
|
+
4. Run verification and record every successful command with `record-verification.sh`.
|
|
42
26
|
|
|
43
27
|
```bash
|
|
44
28
|
{ISSUE_VERIFICATION_COMMAND_SNIPPET}
|
|
45
29
|
```
|
|
46
30
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
CRITICAL: `verification.jsonl` must exist in `$ACP_RUN_DIR` with at least one `pass` entry before you can write `OUTCOME=implemented`.
|
|
54
|
-
|
|
55
|
-
## Phase 4: SELF-REVIEW (MANDATORY)
|
|
56
|
-
|
|
57
|
-
Before committing, perform this checklist:
|
|
58
|
-
|
|
59
|
-
- [ ] Run `git diff --check`.
|
|
60
|
-
- [ ] Count non-test product files: if the change is broad, stop and split scope instead of publishing one large PR.
|
|
61
|
-
- [ ] If you touched auth, login, session, or reset flows, verify existing users and legacy data still behave correctly.
|
|
62
|
-
- [ ] If you touched public endpoints, public routes, or operator workflows, search downstream consumers in `scripts/`, `docs/`, and specs.
|
|
63
|
-
- [ ] If you changed localization resources or user-facing copy, confirm localization coverage and scanning are still valid.
|
|
64
|
-
- [ ] If you touched mobile routes or screens, keep route scope narrow and verify loading, empty, and error states.
|
|
65
|
-
|
|
66
|
-
Before committing, verify the journal exists:
|
|
67
|
-
```bash
|
|
68
|
-
test -s "$ACP_RUN_DIR/verification.jsonl" && echo "OK: verification.jsonl exists" || echo "BLOCKED: missing verification.jsonl"
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Phase 5: COMMIT & REPORT
|
|
72
|
-
|
|
73
|
-
- Commit with a conventional commit message.
|
|
74
|
-
- Do NOT push or open a PR; the host handles that.
|
|
75
|
-
- Write `$ACP_RESULT_FILE`:
|
|
31
|
+
5. Before committing, run at least:
|
|
32
|
+
```bash
|
|
33
|
+
git diff --check
|
|
34
|
+
test -s "$ACP_RUN_DIR/verification.jsonl" && echo "OK: verification.jsonl exists" || echo "BLOCKED: missing verification.jsonl"
|
|
35
|
+
```
|
|
36
|
+
6. If verification passes, commit locally and write `$ACP_RESULT_FILE`:
|
|
76
37
|
```bash
|
|
77
38
|
cat > "$ACP_RESULT_FILE" <<'OUTER_EOF'
|
|
78
39
|
OUTCOME=implemented
|
|
@@ -80,16 +41,7 @@ test -s "$ACP_RUN_DIR/verification.jsonl" && echo "OK: verification.jsonl exists
|
|
|
80
41
|
ISSUE_ID={ISSUE_ID}
|
|
81
42
|
OUTER_EOF
|
|
82
43
|
```
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# STOP CONDITIONS
|
|
86
|
-
|
|
87
|
-
Stop and report blocked if:
|
|
88
|
-
- The issue is ambiguous, blocked by missing credentials, or expands into high-risk scope.
|
|
89
|
-
- You cannot complete verification successfully.
|
|
90
|
-
- The issue needs full decomposition into focused follow-up issues.
|
|
91
|
-
|
|
92
|
-
If stopped blocked, write `$ACP_RUN_DIR/issue-comment.md` with a blocker summary, then:
|
|
44
|
+
7. If blocked, write `$ACP_RUN_DIR/issue-comment.md` and then write:
|
|
93
45
|
```bash
|
|
94
46
|
cat > "$ACP_RESULT_FILE" <<'OUTER_EOF'
|
|
95
47
|
OUTCOME=blocked
|
|
@@ -98,7 +50,7 @@ ISSUE_ID={ISSUE_ID}
|
|
|
98
50
|
OUTER_EOF
|
|
99
51
|
```
|
|
100
52
|
|
|
101
|
-
If fully
|
|
53
|
+
If you fully decompose the work, the first line of `issue-comment.md` must be:
|
|
102
54
|
`Superseded by focused follow-up issues: #...`
|
|
103
55
|
|
|
104
56
|
# Git Rules
|
|
@@ -106,4 +58,4 @@ If fully decomposed into follow-up issues, start the first line of `issue-commen
|
|
|
106
58
|
- Do NOT push the branch from inside the worker.
|
|
107
59
|
- Do NOT open a PR from inside the worker.
|
|
108
60
|
- Do NOT comment on the source issue with a PR URL from inside the worker.
|
|
109
|
-
- Exit
|
|
61
|
+
- Exit after writing the result file.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Task
|
|
2
|
+
|
|
3
|
+
Implement issue #{ISSUE_ID} in `{REPO_SLUG}`.
|
|
4
|
+
|
|
5
|
+
# Issue Context
|
|
6
|
+
|
|
7
|
+
- Title: {ISSUE_TITLE}
|
|
8
|
+
- URL: {ISSUE_URL}
|
|
9
|
+
- Auto-merge requested: {ISSUE_AUTOMERGE}
|
|
10
|
+
|
|
11
|
+
{ISSUE_BODY}
|
|
12
|
+
{ISSUE_RECURRING_CONTEXT}
|
|
13
|
+
{ISSUE_BLOCKER_CONTEXT}
|
|
14
|
+
|
|
15
|
+
# MANDATORY WORKFLOW (follow in order, no skipping)
|
|
16
|
+
|
|
17
|
+
You MUST complete ALL 5 phases in order. Do not skip any phase. Do not commit until Phase 4 passes.
|
|
18
|
+
|
|
19
|
+
## Phase 1: READ & SCOPE
|
|
20
|
+
|
|
21
|
+
- Read the repo instructions: `AGENTS.md`, relevant spec or design docs, and any repo conventions tied to this issue.
|
|
22
|
+
- Identify the single primary product surface you will touch.
|
|
23
|
+
- If the issue spans multiple surfaces, pick ONE and create follow-up issues for the rest using:
|
|
24
|
+
```bash
|
|
25
|
+
bash "$ACP_FLOW_TOOLS_DIR/create-follow-up-issue.sh" --parent {ISSUE_ID} --title "..." --body-file /tmp/follow-up.md
|
|
26
|
+
```
|
|
27
|
+
- Treat a broad umbrella issue as a coordination brief rather than permission to ship every slice in one PR.
|
|
28
|
+
- Write down your scope decision before coding.
|
|
29
|
+
|
|
30
|
+
## Phase 2: IMPLEMENT
|
|
31
|
+
|
|
32
|
+
- Make the smallest root-cause fix that satisfies the issue.
|
|
33
|
+
- Work only inside the dedicated worktree.
|
|
34
|
+
- Add or update tests when feasible.
|
|
35
|
+
- STOP after implementation. Do not commit yet.
|
|
36
|
+
|
|
37
|
+
## Phase 3: VERIFY (MANDATORY)
|
|
38
|
+
|
|
39
|
+
After implementing, you MUST run verification commands and record each one.
|
|
40
|
+
Every successful command must be recorded or the host publish will fail.
|
|
41
|
+
After each successful verification command, record it with `record-verification.sh`.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
{ISSUE_VERIFICATION_COMMAND_SNIPPET}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Required verification coverage:
|
|
48
|
+
- Run the narrowest repo-supported `typecheck`, `build`, `test`, or `lint` command that proves the touched surface is safe.
|
|
49
|
+
- If you changed tests only, run the most relevant targeted test command and record it.
|
|
50
|
+
- If you changed localization resources or user-facing copy, run repo locale validation or hardcoded-copy scans if the repo provides them.
|
|
51
|
+
- If a verification command fails, fix the issue and rerun until it passes.
|
|
52
|
+
|
|
53
|
+
CRITICAL: `verification.jsonl` must exist in `$ACP_RUN_DIR` with at least one `pass` entry before you can write `OUTCOME=implemented`.
|
|
54
|
+
|
|
55
|
+
## Phase 4: SELF-REVIEW (MANDATORY)
|
|
56
|
+
|
|
57
|
+
Before committing, perform this checklist:
|
|
58
|
+
|
|
59
|
+
- [ ] Run `git diff --check`.
|
|
60
|
+
- [ ] Count non-test product files: if the change is broad, stop and split scope instead of publishing one large PR.
|
|
61
|
+
- [ ] If you touched auth, login, session, or reset flows, verify existing users and legacy data still behave correctly.
|
|
62
|
+
- [ ] If you touched public endpoints, public routes, or operator workflows, search downstream consumers in `scripts/`, `docs/`, and specs.
|
|
63
|
+
- [ ] If you changed localization resources or user-facing copy, confirm localization coverage and scanning are still valid.
|
|
64
|
+
- [ ] If you touched mobile routes or screens, keep route scope narrow and verify loading, empty, and error states.
|
|
65
|
+
|
|
66
|
+
Before committing, verify the journal exists:
|
|
67
|
+
```bash
|
|
68
|
+
test -s "$ACP_RUN_DIR/verification.jsonl" && echo "OK: verification.jsonl exists" || echo "BLOCKED: missing verification.jsonl"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Phase 5: COMMIT & REPORT
|
|
72
|
+
|
|
73
|
+
- Commit with a conventional commit message.
|
|
74
|
+
- Do NOT push or open a PR; the host handles that.
|
|
75
|
+
- Write `$ACP_RESULT_FILE`:
|
|
76
|
+
```bash
|
|
77
|
+
cat > "$ACP_RESULT_FILE" <<'OUTER_EOF'
|
|
78
|
+
OUTCOME=implemented
|
|
79
|
+
ACTION=host-publish-issue-pr
|
|
80
|
+
ISSUE_ID={ISSUE_ID}
|
|
81
|
+
OUTER_EOF
|
|
82
|
+
```
|
|
83
|
+
- In your final output, include the changed files, verification commands actually run, and one short self-review note naming the main regression risk you checked.
|
|
84
|
+
|
|
85
|
+
# STOP CONDITIONS
|
|
86
|
+
|
|
87
|
+
Stop and report blocked if:
|
|
88
|
+
- The issue is ambiguous, blocked by missing credentials, or expands into high-risk scope.
|
|
89
|
+
- You cannot complete verification successfully.
|
|
90
|
+
- The issue needs full decomposition into focused follow-up issues.
|
|
91
|
+
|
|
92
|
+
If stopped blocked, write `$ACP_RUN_DIR/issue-comment.md` with a blocker summary, then:
|
|
93
|
+
```bash
|
|
94
|
+
cat > "$ACP_RESULT_FILE" <<'OUTER_EOF'
|
|
95
|
+
OUTCOME=blocked
|
|
96
|
+
ACTION=host-comment-blocker
|
|
97
|
+
ISSUE_ID={ISSUE_ID}
|
|
98
|
+
OUTER_EOF
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If fully decomposed into follow-up issues, start the first line of `issue-comment.md` with exactly:
|
|
102
|
+
`Superseded by focused follow-up issues: #...`
|
|
103
|
+
|
|
104
|
+
# Git Rules
|
|
105
|
+
|
|
106
|
+
- Do NOT push the branch from inside the worker.
|
|
107
|
+
- Do NOT open a PR from inside the worker.
|
|
108
|
+
- Do NOT comment on the source issue with a PR URL from inside the worker.
|
|
109
|
+
- Exit successfully after writing the result file.
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
-
# shellcheck source=/dev/null
|
|
6
|
-
source "${SCRIPT_DIR}/../tools/bin/flow-config-lib.sh"
|
|
7
|
-
|
|
8
|
-
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
9
|
-
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
10
|
-
AGENT_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
|
|
11
|
-
AGENT_PR_ISSUE_CAPTURE_REGEX="$(flow_managed_issue_branch_regex "${CONFIG_YAML}")"
|
|
12
|
-
MIN_AGE_MINUTES="${1:-30}"
|
|
13
|
-
|
|
14
|
-
open_agent_pr_issue_ids="$(
|
|
15
|
-
gh pr list -R "$REPO_SLUG" --state open --limit 100 --json headRefName,body,labels,comments \
|
|
16
|
-
| jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
|
|
17
|
-
map(
|
|
18
|
-
. as $pr
|
|
19
|
-
| select(
|
|
20
|
-
any($agentPrPrefixes[]; (($pr.headRefName // "") | startswith(.)))
|
|
21
|
-
or any(($pr.labels // [])[]?; .name == "agent-handoff")
|
|
22
|
-
or any(($pr.comments // [])[]?; ((.body // "") | test("^## PR (final review blocker|repair worker summary|repair summary|repair update)"; "i")))
|
|
23
|
-
)
|
|
24
|
-
| [
|
|
25
|
-
(
|
|
26
|
-
$pr.headRefName
|
|
27
|
-
| capture($branchIssueRegex)?
|
|
28
|
-
| .id
|
|
29
|
-
),
|
|
30
|
-
(
|
|
31
|
-
($pr.body // "")
|
|
32
|
-
| capture("(?i)\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#(?<id>[0-9]+)\\b")?
|
|
33
|
-
| .id
|
|
34
|
-
)
|
|
35
|
-
]
|
|
36
|
-
| .[]
|
|
37
|
-
| select(. != null and . != "")
|
|
38
|
-
)
|
|
39
|
-
| unique
|
|
40
|
-
'
|
|
41
|
-
)"
|
|
42
|
-
|
|
43
|
-
gh issue list -R "$REPO_SLUG" --state open --limit 100 --json number,title,createdAt,updatedAt,labels \
|
|
44
|
-
| jq -r --argjson openAgentPrIssueIds "$open_agent_pr_issue_ids" --argjson minAgeMinutes "$MIN_AGE_MINUTES" '
|
|
45
|
-
def label_names: [.labels[]?.name];
|
|
46
|
-
def age_minutes:
|
|
47
|
-
((now - ((.createdAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601))) / 60);
|
|
48
|
-
def has_open_agent_pr:
|
|
49
|
-
((.number | tostring) as $issueId | ($openAgentPrIssueIds | index($issueId)) != null);
|
|
50
|
-
map(
|
|
51
|
-
. + {
|
|
52
|
-
reason:
|
|
53
|
-
(if any(label_names[]?; . == "agent-running") and (has_open_agent_pr | not) then
|
|
54
|
-
"stale-agent-running"
|
|
55
|
-
elif any(label_names[]?; . == "agent-blocked") then
|
|
56
|
-
"blocked-manual-review"
|
|
57
|
-
else
|
|
58
|
-
""
|
|
59
|
-
end)
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
| map(select(.reason != "" and (age_minutes >= $minAgeMinutes)))
|
|
63
|
-
| sort_by(.createdAt, .number)
|
|
64
|
-
| .[]
|
|
65
|
-
| [
|
|
66
|
-
(.number | tostring),
|
|
67
|
-
.reason,
|
|
68
|
-
.createdAt,
|
|
69
|
-
.updatedAt,
|
|
70
|
-
(label_names | join(",")),
|
|
71
|
-
.title
|
|
72
|
-
]
|
|
73
|
-
| @tsv
|
|
74
|
-
'
|