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.
Files changed (37) hide show
  1. package/hooks/heartbeat-hooks.sh +97 -8
  2. package/package.json +8 -2
  3. package/references/commands.md +1 -0
  4. package/tools/bin/agent-project-cleanup-session +133 -0
  5. package/tools/bin/agent-project-publish-issue-pr +178 -62
  6. package/tools/bin/agent-project-reconcile-issue-session +171 -3
  7. package/tools/bin/agent-project-run-codex-resilient +121 -16
  8. package/tools/bin/agent-project-run-codex-session +60 -10
  9. package/tools/bin/agent-project-run-openclaw-session +82 -8
  10. package/tools/bin/cleanup-worktree.sh +4 -1
  11. package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
  12. package/tools/bin/ensure-runtime-sync.sh +182 -0
  13. package/tools/bin/flow-config-lib.sh +76 -30
  14. package/tools/bin/flow-resident-worker-lib.sh +28 -2
  15. package/tools/bin/flow-shell-lib.sh +15 -1
  16. package/tools/bin/heartbeat-safe-auto.sh +32 -0
  17. package/tools/bin/issue-publish-localization-guard.sh +142 -0
  18. package/tools/bin/project-launchd-bootstrap.sh +17 -4
  19. package/tools/bin/project-runtime-supervisor.sh +7 -1
  20. package/tools/bin/project-runtimectl.sh +78 -15
  21. package/tools/bin/reuse-issue-worktree.sh +46 -0
  22. package/tools/bin/start-issue-worker.sh +76 -6
  23. package/tools/bin/start-resident-issue-loop.sh +1 -0
  24. package/tools/bin/sync-shared-agent-home.sh +26 -0
  25. package/tools/bin/test-smoke.sh +6 -1
  26. package/tools/dashboard/app.js +71 -1
  27. package/tools/dashboard/dashboard_snapshot.py +74 -0
  28. package/tools/dashboard/styles.css +43 -0
  29. package/tools/templates/issue-prompt-template.md +18 -66
  30. package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
  31. package/bin/audit-issue-routing.sh +0 -74
  32. package/tools/bin/audit-agent-worktrees.sh +0 -310
  33. package/tools/bin/audit-issue-routing.sh +0 -11
  34. package/tools/bin/audit-retained-layout.sh +0 -58
  35. package/tools/bin/audit-retained-overlap.sh +0 -135
  36. package/tools/bin/audit-retained-worktrees.sh +0 -228
  37. package/tools/bin/check-skill-contracts.sh +0 -324
@@ -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
- # MANDATORY WORKFLOW (follow in order, no skipping)
15
+ # Required Contract
16
16
 
17
- You MUST complete ALL 5 phases in order. Do not skip any phase. Do not commit until Phase 4 passes.
17
+ Follow this order:
18
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`.
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
- 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`:
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
- - 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:
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 decomposed into follow-up issues, start the first line of `issue-comment.md` with exactly:
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 successfully after writing the result file.
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
- '