agent-control-plane 0.2.0 → 0.4.9
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/README.md +69 -19
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +296 -61
- package/package.json +11 -7
- package/tools/bin/agent-github-update-labels +36 -2
- package/tools/bin/agent-project-catch-up-merged-prs +4 -2
- package/tools/bin/agent-project-cleanup-session +49 -5
- package/tools/bin/agent-project-heartbeat-loop +119 -1471
- package/tools/bin/agent-project-publish-issue-pr +6 -3
- package/tools/bin/agent-project-reconcile-issue-session +78 -106
- package/tools/bin/agent-project-reconcile-pr-session +166 -143
- package/tools/bin/agent-project-retry-state +18 -7
- package/tools/bin/agent-project-run-claude-session +10 -0
- package/tools/bin/agent-project-run-codex-resilient +99 -14
- package/tools/bin/agent-project-run-codex-session +16 -5
- package/tools/bin/agent-project-run-kilo-session +10 -0
- package/tools/bin/agent-project-run-openclaw-session +10 -0
- package/tools/bin/agent-project-run-opencode-session +10 -0
- package/tools/bin/agent-project-sync-source-repo-main +163 -0
- package/tools/bin/agent-project-worker-status +10 -7
- package/tools/bin/cleanup-worktree.sh +6 -1
- package/tools/bin/flow-config-lib.sh +1257 -34
- package/tools/bin/flow-resident-worker-lib.sh +119 -1
- package/tools/bin/flow-shell-lib.sh +56 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
- package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
- package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
- package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
- package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
- package/tools/bin/heartbeat-safe-auto.sh +56 -3
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +16 -9
- package/tools/bin/project-runtimectl.sh +46 -2
- package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
- package/tools/bin/resident-issue-controller-lib.sh +448 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/start-pr-fix-worker.sh +47 -10
- package/tools/bin/start-resident-issue-loop.sh +28 -439
- package/tools/dashboard/app.js +37 -1
- package/tools/dashboard/dashboard_snapshot.py +65 -26
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/SKILL.md +0 -149
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/split-retained-slice.sh +0 -124
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ however, and suddenly that "not smart enough" free model is grinding through
|
|
|
33
33
|
your issue backlog like a junior developer who is weirdly enthusiastic about
|
|
34
34
|
reading CI logs.
|
|
35
35
|
|
|
36
|
-
That is what ACP does. It turns a
|
|
36
|
+
That is what ACP does. It turns a forge-backed repo into a managed runtime: a
|
|
37
37
|
repeatable setup, a stable home for state, a heartbeat that keeps agents
|
|
38
38
|
scheduled and supervised, and a dashboard you can actually glance at without
|
|
39
39
|
spelunking through temp folders, worktrees, or half-remembered `tmux` sessions.
|
|
@@ -73,7 +73,7 @@ monthly API budget in a long weekend, or enter a retry loop that only stops when
|
|
|
73
73
|
the credit card does.
|
|
74
74
|
|
|
75
75
|
ACP is the person standing next to the fuse. It enforces launch limits,
|
|
76
|
-
reconciles outcomes before touching
|
|
76
|
+
reconciles outcomes before touching your forge, validates before it publishes, and
|
|
77
77
|
respects cooldowns instead of hammering a provider at full throttle. The agent
|
|
78
78
|
gets to be smart and fast. ACP makes sure "smart and fast" does not also mean
|
|
79
79
|
"unattended and irreversible."
|
|
@@ -98,6 +98,27 @@ it had a supervisor."
|
|
|
98
98
|
| Run reproducible agent research cheaply | Cost-controlled execution harness for studying agent behavior, output quality, or prompting strategies |
|
|
99
99
|
| Enforce safety by architecture, not by hope | Launch limits, reconcile gates, and cooldowns that are built into the runtime, not left to chance |
|
|
100
100
|
|
|
101
|
+
## Why Gitea Local-First
|
|
102
|
+
|
|
103
|
+
ACP started GitHub-first, but a local-first Gitea loop is often a better daily
|
|
104
|
+
working setup:
|
|
105
|
+
|
|
106
|
+
- It reduces dependence on GitHub API rate limits for routine issue and PR work.
|
|
107
|
+
- It lets agents collaborate against a local forge while you keep GitHub as the
|
|
108
|
+
public mirror or release boundary.
|
|
109
|
+
- It gives you a safer place to let ACP iterate quickly, because local Gitea is
|
|
110
|
+
cheaper to reset, inspect, and isolate than a live hosted repo.
|
|
111
|
+
- It matches how ACP now works best operationally: local runtime state,
|
|
112
|
+
local worktrees, local dashboard, and a forge that can live on the same
|
|
113
|
+
machine.
|
|
114
|
+
|
|
115
|
+
The intended model is:
|
|
116
|
+
|
|
117
|
+
- `Gitea main` is the working mainline ACP automates day to day.
|
|
118
|
+
- Your local source checkout auto-syncs from that forge mainline.
|
|
119
|
+
- GitHub becomes the publish/release mirror once the codebase is stable enough
|
|
120
|
+
to push outward.
|
|
121
|
+
|
|
101
122
|
## Use Cases
|
|
102
123
|
|
|
103
124
|
Teams and solo builders usually reach for ACP when one of these starts to feel
|
|
@@ -211,7 +232,7 @@ flowchart LR
|
|
|
211
232
|
Scheduler --> Workers["issue / PR worker launchers"]
|
|
212
233
|
Workers --> Backends["codex / claude / openclaw / ollama / pi / opencode / kilo"]
|
|
213
234
|
Backends --> Reconcile["reconcile issue / PR session"]
|
|
214
|
-
Reconcile -->
|
|
235
|
+
Reconcile --> Forge["issues / PRs / labels / comments"]
|
|
215
236
|
Scheduler --> State["runs + state + history"]
|
|
216
237
|
State --> Dashboard["dashboard snapshot + UI"]
|
|
217
238
|
```
|
|
@@ -227,7 +248,7 @@ sequenceDiagram
|
|
|
227
248
|
participant Heartbeat
|
|
228
249
|
participant Worker
|
|
229
250
|
participant Reconcile
|
|
230
|
-
participant
|
|
251
|
+
participant Forge
|
|
231
252
|
|
|
232
253
|
Operator->>RuntimeCtl: runtime start --profile-id <id>
|
|
233
254
|
RuntimeCtl->>Supervisor: keep runtime alive
|
|
@@ -236,7 +257,7 @@ sequenceDiagram
|
|
|
236
257
|
Bootstrap->>Heartbeat: invoke published heartbeat
|
|
237
258
|
Heartbeat->>Worker: launch eligible issue/PR flow
|
|
238
259
|
Worker->>Reconcile: emit result artifacts
|
|
239
|
-
Reconcile->>
|
|
260
|
+
Reconcile->>Forge: labels, comments, PR actions
|
|
240
261
|
end
|
|
241
262
|
```
|
|
242
263
|
|
|
@@ -267,9 +288,9 @@ system.
|
|
|
267
288
|
| --- | --- | --- | --- |
|
|
268
289
|
| Node.js `>= 18` | yes | Runs the npm package entrypoint and `npx` wrapper. | CI runs on Node `22`. Node `20` or `22` both work fine. |
|
|
269
290
|
| `bash` | yes | All runtime, profile, and worker orchestration scripts are Bash. | Your login shell can be `zsh`; `bash` just needs to be on `PATH`. |
|
|
270
|
-
| `git` | yes | Manages worktrees, checks branch state, and coordinates repo automation. | Required even if you interact only through
|
|
271
|
-
| `gh` |
|
|
272
|
-
| `jq` | yes | Parses JSON from `gh` output and worker metadata throughout. | Missing `jq` will break GitHub-heavy and runtime flows. |
|
|
291
|
+
| `git` | yes | Manages worktrees, checks branch state, and coordinates repo automation. | Required even if you interact only through forge issues and PRs. |
|
|
292
|
+
| `gh` | for GitHub-first setups | GitHub CLI auth and API access for issues, PRs, labels, and metadata. | Run `gh auth login` before first use when `--forge-provider github`. |
|
|
293
|
+
| `jq` | yes | Parses JSON from `gh` output and worker metadata throughout. | Missing `jq` will break GitHub-heavy and Gitea-heavy runtime flows. |
|
|
273
294
|
| `python3` | yes | Powers the dashboard server, snapshot renderer, and config helpers. | Required for both dashboard use and several internal scripts. |
|
|
274
295
|
| `tmux` | yes | Runs long-lived worker sessions and captures their status. | Missing `tmux` means background worker workflows will not launch. |
|
|
275
296
|
| Worker CLI (backend-specific) | depends on backend | The coding agent for a profile. Supported: `codex`, `claude`, `openclaw` (production); `ollama`, `pi`, `opencode`, `kilo` (experimental). | Install and authenticate your chosen backend before starting background runs. |
|
|
@@ -279,8 +300,10 @@ system.
|
|
|
279
300
|
| `kilo` CLI | for `kilo` backend | TypeScript coding agent ([kilocode/cli](https://github.com/Kilo-Org/kilocode)). | Install via `npm i -g @kilocode/cli`. |
|
|
280
301
|
| Bundled `codex-quota` + ACP quota manager | automatic for Codex | Quota-aware failover and health signals for Codex profiles. | Bundled by default. Override with `ACP_CODEX_QUOTA_BIN` only if you have a custom setup. |
|
|
281
302
|
|
|
282
|
-
Make sure
|
|
283
|
-
|
|
303
|
+
Make sure your chosen worker backend is authenticated for the same OS user
|
|
304
|
+
before starting any background runtime. For GitHub-first setups, authenticate
|
|
305
|
+
`gh`. For Gitea local-first setups, provide `--gitea-base-url` plus a token or
|
|
306
|
+
username/password during setup so ACP can write issues, PR comments, and labels.
|
|
284
307
|
|
|
285
308
|
## Install
|
|
286
309
|
|
|
@@ -313,7 +336,7 @@ npx agent-control-plane@latest setup
|
|
|
313
336
|
The wizard walks you through the full setup in one pass:
|
|
314
337
|
|
|
315
338
|
1. Detects the current repo and suggests sane defaults
|
|
316
|
-
2.
|
|
339
|
+
2. Captures forge mode (`github` or `gitea`) and the auth/settings that mode needs
|
|
317
340
|
3. Checks backend readiness (API keys for openclaw/pi, local server for ollama)
|
|
318
341
|
4. Scaffolds the profile, runs health checks, starts the runtime
|
|
319
342
|
5. Launches the monitoring dashboard in the background
|
|
@@ -343,16 +366,41 @@ npx agent-control-plane@latest setup \
|
|
|
343
366
|
With `--json`, ACP emits a single structured object on `stdout` and sends
|
|
344
367
|
progress logs to `stderr`, which keeps parsing stable.
|
|
345
368
|
|
|
369
|
+
Example: local-first Gitea setup
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npx agent-control-plane@latest setup \
|
|
373
|
+
--forge-provider gitea \
|
|
374
|
+
--repo-slug acp-admin/my-repo \
|
|
375
|
+
--gitea-base-url http://127.0.0.1:3000 \
|
|
376
|
+
--gitea-token <token> \
|
|
377
|
+
--start-runtime \
|
|
378
|
+
--start-dashboard
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
This writes the forge settings into the profile `runtime.env`, so later
|
|
382
|
+
heartbeat, reconcile, and publish steps keep talking to the same Gitea
|
|
383
|
+
instance without extra shell exports.
|
|
384
|
+
|
|
346
385
|
### Option B — Manual setup
|
|
347
386
|
|
|
348
387
|
If you prefer explicit control over each step:
|
|
349
388
|
|
|
350
|
-
**1. Authenticate
|
|
389
|
+
**1. Authenticate the working forge**
|
|
351
390
|
|
|
352
391
|
```bash
|
|
353
392
|
gh auth login
|
|
354
393
|
```
|
|
355
394
|
|
|
395
|
+
For Gitea local-first, skip `gh auth login` and pass Gitea settings directly to
|
|
396
|
+
`setup` or `init`:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
--forge-provider gitea \
|
|
400
|
+
--gitea-base-url http://127.0.0.1:3000 \
|
|
401
|
+
--gitea-token <token>
|
|
402
|
+
```
|
|
403
|
+
|
|
356
404
|
**2. Install the packaged runtime**
|
|
357
405
|
|
|
358
406
|
```bash
|
|
@@ -368,6 +416,7 @@ re-run after upgrades.
|
|
|
368
416
|
npx agent-control-plane@latest init \
|
|
369
417
|
--profile-id my-repo \
|
|
370
418
|
--repo-slug owner/my-repo \
|
|
419
|
+
--forge-provider github \
|
|
371
420
|
--repo-root ~/src/my-repo \
|
|
372
421
|
--agent-root ~/.agent-runtime/projects/my-repo \
|
|
373
422
|
--worktree-root ~/src/my-repo-worktrees \
|
|
@@ -377,7 +426,9 @@ npx agent-control-plane@latest init \
|
|
|
377
426
|
| Flag | Purpose |
|
|
378
427
|
| --- | --- |
|
|
379
428
|
| `--profile-id` | Short name used in all ACP commands |
|
|
380
|
-
| `--repo-slug` |
|
|
429
|
+
| `--repo-slug` | Forge repo ACP should track |
|
|
430
|
+
| `--forge-provider` | Which forge ACP should automate (`github` or `gitea`) |
|
|
431
|
+
| `--gitea-base-url` | Base URL when `--forge-provider gitea` |
|
|
381
432
|
| `--repo-root` | Path to your local checkout |
|
|
382
433
|
| `--agent-root` | Where ACP keeps per-project runtime state |
|
|
383
434
|
| `--worktree-root` | Where ACP places repo worktrees |
|
|
@@ -407,9 +458,8 @@ grouped and inspectable without digging through scattered temp files.
|
|
|
407
458
|
## Starter Issues
|
|
408
459
|
|
|
409
460
|
The setup wizard can create a set of recurring `agent-keep-open` issues on your
|
|
410
|
-
repo so ACP starts working immediately after installation.
|
|
411
|
-
|
|
412
|
-
heartbeat cycle.
|
|
461
|
+
repo so ACP starts working immediately after installation. ACP picks them up on
|
|
462
|
+
its next heartbeat cycle without requiring a separate readiness label.
|
|
413
463
|
|
|
414
464
|
Built-in templates:
|
|
415
465
|
|
|
@@ -421,9 +471,9 @@ Built-in templates:
|
|
|
421
471
|
| Dependency audit | Fix vulnerabilities and update safe patches |
|
|
422
472
|
| Refactoring sweep | Reduce complexity and duplication |
|
|
423
473
|
|
|
424
|
-
You can also create your own recurring issues
|
|
425
|
-
`agent-keep-open`
|
|
426
|
-
continuously.
|
|
474
|
+
You can also create your own recurring issues by adding the
|
|
475
|
+
`agent-keep-open` label to any GitHub issue. ACP will keep revisiting that
|
|
476
|
+
issue continuously unless it is blocked or already claimed by an open agent PR.
|
|
427
477
|
|
|
428
478
|
To skip this step during setup, pass `--no-create-starter-issues`.
|
|
429
479
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{
|
|
8
8
|
"id": "issue-implementation",
|
|
9
9
|
"kind": "issue",
|
|
10
|
-
"trigger": "Open issue
|
|
10
|
+
"trigger": "Open issue without agent-running, without an open agent PR claim, and not blocked by retry policy",
|
|
11
11
|
"entrypoint": "tools/bin/start-issue-worker.sh",
|
|
12
12
|
"summary": "Primary implementation loop for focused issues that should end in a PR or a blocked report."
|
|
13
13
|
},
|
package/bin/pr-risk.sh
CHANGED
|
@@ -26,10 +26,10 @@ fi
|
|
|
26
26
|
gh_api_json_matching_or_fallback() {
|
|
27
27
|
local fallback="${1:?fallback required}"
|
|
28
28
|
local jq_filter="${2:?jq filter required}"
|
|
29
|
-
|
|
29
|
+
local route="${3:?route required}"
|
|
30
30
|
local output=""
|
|
31
31
|
|
|
32
|
-
output="$(
|
|
32
|
+
output="$(flow_github_api_repo "${REPO_SLUG}" "${route}" 2>/dev/null || true)"
|
|
33
33
|
if jq -e "${jq_filter}" >/dev/null 2>&1 <<<"${output}"; then
|
|
34
34
|
printf '%s\n' "${output}"
|
|
35
35
|
return 0
|
|
@@ -38,17 +38,32 @@ gh_api_json_matching_or_fallback() {
|
|
|
38
38
|
printf '%s\n' "${fallback}"
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
PR_JSON="$(
|
|
42
|
-
|
|
41
|
+
PR_JSON="$(flow_github_pr_view_json "$REPO_SLUG" "$PR_NUMBER" 2>/dev/null || true)"
|
|
42
|
+
if ! jq -e '.number? != null' >/dev/null 2>&1 <<<"${PR_JSON:-}"; then
|
|
43
|
+
if flow_using_gitea; then
|
|
44
|
+
printf 'pr-risk: forge PR view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
PR_JSON="$(gh pr view "$PR_NUMBER" -R "$REPO_SLUG" --json number,title,url,body,isDraft,headRefName,headRefOid,baseRefName,labels,files,mergeStateStatus,reviewDecision,reviewRequests,statusCheckRollup,comments 2>/dev/null)" \
|
|
48
|
+
|| { printf 'pr-risk: forge PR view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2; exit 1; }
|
|
49
|
+
fi
|
|
43
50
|
PR_HEAD_SHA="$(jq -r '.headRefOid // ""' <<<"$PR_JSON")"
|
|
44
51
|
PR_HEAD_COMMITTED_AT=""
|
|
45
52
|
if [[ -n "${PR_HEAD_SHA}" ]]; then
|
|
46
|
-
PR_HEAD_COMMITTED_AT="$(
|
|
53
|
+
PR_HEAD_COMMITTED_AT="$(
|
|
54
|
+
flow_github_api_repo "${REPO_SLUG}" "commits/${PR_HEAD_SHA}" 2>/dev/null \
|
|
55
|
+
| jq -r '.commit.committer.date // .commit.author.date // .created // .timestamp // ""' 2>/dev/null \
|
|
56
|
+
|| true
|
|
57
|
+
)"
|
|
58
|
+
fi
|
|
59
|
+
if flow_using_gitea; then
|
|
60
|
+
REVIEW_COMMENTS_JSON='[]'
|
|
61
|
+
else
|
|
62
|
+
REVIEW_COMMENTS_JSON="$(gh_api_json_matching_or_fallback '[]' 'type == "array"' "pulls/${PR_NUMBER}/comments")"
|
|
47
63
|
fi
|
|
48
|
-
REVIEW_COMMENTS_JSON="$(gh_api_json_matching_or_fallback '[]' 'type == "array"' "repos/${REPO_SLUG}/pulls/${PR_NUMBER}/comments")"
|
|
49
64
|
CHECK_RUNS_JSON='{"check_runs":[]}'
|
|
50
65
|
if [[ -n "${PR_HEAD_SHA}" ]]; then
|
|
51
|
-
CHECK_RUNS_JSON="$(gh_api_json_matching_or_fallback '{"check_runs":[]}' 'type == "object" and ((.check_runs // []) | type == "array")' "
|
|
66
|
+
CHECK_RUNS_JSON="$(gh_api_json_matching_or_fallback '{"check_runs":[]}' 'type == "object" and ((.check_runs // []) | type == "array")' "commits/${PR_HEAD_SHA}/check-runs")"
|
|
52
67
|
fi
|
|
53
68
|
|
|
54
69
|
PR_JSON="$PR_JSON" PR_HEAD_SHA="$PR_HEAD_SHA" PR_HEAD_COMMITTED_AT="$PR_HEAD_COMMITTED_AT" REVIEW_COMMENTS_JSON="$REVIEW_COMMENTS_JSON" CHECK_RUNS_JSON="$CHECK_RUNS_JSON" PR_LANE_OVERRIDE="${PR_LANE_OVERRIDE:-}" MANAGED_PR_PREFIXES_JSON="$MANAGED_PR_PREFIXES_JSON" MANAGED_PR_ISSUE_CAPTURE_REGEX="$MANAGED_PR_ISSUE_CAPTURE_REGEX" ALLOW_INFRA_CI_BYPASS="$ALLOW_INFRA_CI_BYPASS" LOCAL_FIRST_PR_POLICY="$LOCAL_FIRST_PR_POLICY" node <<'EOF'
|
package/bin/sync-pr-labels.sh
CHANGED
|
@@ -100,7 +100,7 @@ fi
|
|
|
100
100
|
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$PR_NUMBER" "${args[@]}" >/dev/null
|
|
101
101
|
|
|
102
102
|
if [[ -n "$linked_issue_id" ]]; then
|
|
103
|
-
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$linked_issue_id" --remove agent-
|
|
103
|
+
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$linked_issue_id" --remove agent-running >/dev/null || true
|
|
104
104
|
fi
|
|
105
105
|
|
|
106
106
|
printf 'PR_NUMBER=%s\n' "$PR_NUMBER"
|
package/hooks/heartbeat-hooks.sh
CHANGED
|
@@ -36,13 +36,115 @@ HEARTBEAT_SNAPSHOT_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-snapshot.$$"
|
|
|
36
36
|
# pipes), so in-memory variables would be lost immediately. We rely exclusively
|
|
37
37
|
# on the PID-scoped disk cache under HEARTBEAT_SNAPSHOT_CACHE_DIR.
|
|
38
38
|
|
|
39
|
+
heartbeat_github_mirror_dir() {
|
|
40
|
+
[[ -n "${STATE_ROOT:-}" ]] || return 1
|
|
41
|
+
printf '%s/github-mirror/heartbeat\n' "${STATE_ROOT}"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
heartbeat_github_mirror_file() {
|
|
45
|
+
local mirror_key="${1:?mirror key required}"
|
|
46
|
+
local mirror_dir=""
|
|
47
|
+
mirror_dir="$(heartbeat_github_mirror_dir)" || return 1
|
|
48
|
+
printf '%s/%s.json\n' "${mirror_dir}" "${mirror_key}"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
heartbeat_json_matches_kind() {
|
|
52
|
+
local payload="${1:-}"
|
|
53
|
+
local expected_kind="${2:-}"
|
|
54
|
+
|
|
55
|
+
[[ -n "${payload}" && -n "${expected_kind}" ]] || return 1
|
|
56
|
+
jq -e --arg kind "${expected_kind}" 'type == $kind' >/dev/null <<<"${payload}"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
heartbeat_write_json_mirror() {
|
|
60
|
+
local mirror_key="${1:?mirror key required}"
|
|
61
|
+
local payload="${2:-}"
|
|
62
|
+
local expected_kind="${3:?expected kind required}"
|
|
63
|
+
local source_mode="${4:-live}"
|
|
64
|
+
local mirror_file=""
|
|
65
|
+
local mirror_dir=""
|
|
66
|
+
local meta_file=""
|
|
67
|
+
local tmp_file=""
|
|
68
|
+
local meta_tmp_file=""
|
|
69
|
+
local updated_at=""
|
|
70
|
+
|
|
71
|
+
heartbeat_json_matches_kind "${payload}" "${expected_kind}" || return 1
|
|
72
|
+
mirror_dir="$(heartbeat_github_mirror_dir)" || return 1
|
|
73
|
+
mirror_file="${mirror_dir}/${mirror_key}.json"
|
|
74
|
+
meta_file="${mirror_dir}/${mirror_key}.env"
|
|
75
|
+
mkdir -p "${mirror_dir}"
|
|
76
|
+
updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
77
|
+
tmp_file="${mirror_file}.tmp.$$"
|
|
78
|
+
meta_tmp_file="${meta_file}.tmp.$$"
|
|
79
|
+
printf '%s' "${payload}" >"${tmp_file}"
|
|
80
|
+
{
|
|
81
|
+
printf 'MIRROR_KEY=%s\n' "${mirror_key}"
|
|
82
|
+
printf 'JSON_KIND=%s\n' "${expected_kind}"
|
|
83
|
+
printf 'SOURCE=%s\n' "${source_mode}"
|
|
84
|
+
printf 'UPDATED_AT=%s\n' "${updated_at}"
|
|
85
|
+
} >"${meta_tmp_file}"
|
|
86
|
+
mv "${tmp_file}" "${mirror_file}"
|
|
87
|
+
mv "${meta_tmp_file}" "${meta_file}"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
heartbeat_read_json_mirror() {
|
|
91
|
+
local mirror_key="${1:?mirror key required}"
|
|
92
|
+
local expected_kind="${2:?expected kind required}"
|
|
93
|
+
local default_json="${3:-}"
|
|
94
|
+
local mirror_file=""
|
|
95
|
+
local payload=""
|
|
96
|
+
|
|
97
|
+
mirror_file="$(heartbeat_github_mirror_file "${mirror_key}" 2>/dev/null || true)"
|
|
98
|
+
if [[ -n "${mirror_file}" && -f "${mirror_file}" ]]; then
|
|
99
|
+
payload="$(cat "${mirror_file}" 2>/dev/null || true)"
|
|
100
|
+
if heartbeat_json_matches_kind "${payload}" "${expected_kind}"; then
|
|
101
|
+
printf '%s\n' "${payload}"
|
|
102
|
+
return 0
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
printf '%s\n' "${default_json}"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
heartbeat_cached_json_with_local_mirror() {
|
|
110
|
+
local cache_file="${1:?cache file required}"
|
|
111
|
+
local mirror_key="${2:?mirror key required}"
|
|
112
|
+
local expected_kind="${3:?expected kind required}"
|
|
113
|
+
local default_json="${4:-}"
|
|
114
|
+
local live_fetch_fn="${5:?live fetch function required}"
|
|
115
|
+
local payload=""
|
|
116
|
+
|
|
117
|
+
shift 5
|
|
118
|
+
|
|
119
|
+
if [[ -f "${cache_file}" ]]; then
|
|
120
|
+
cat "${cache_file}"
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if payload="$("${live_fetch_fn}" "$@" 2>/dev/null)" && heartbeat_json_matches_kind "${payload}" "${expected_kind}"; then
|
|
125
|
+
heartbeat_write_json_mirror "${mirror_key}" "${payload}" "${expected_kind}" "live" || true
|
|
126
|
+
else
|
|
127
|
+
payload="$(heartbeat_read_json_mirror "${mirror_key}" "${expected_kind}" "${default_json}")"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
printf '%s' "${payload}" >"${cache_file}"
|
|
131
|
+
printf '%s\n' "${payload}"
|
|
132
|
+
}
|
|
133
|
+
|
|
39
134
|
heartbeat_cached_issue_list_json() {
|
|
40
135
|
mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
|
|
41
136
|
local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/issues.json"
|
|
42
137
|
if [[ ! -f "${cache_file}" ]]; then
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
138
|
+
heartbeat_cached_json_with_local_mirror \
|
|
139
|
+
"${cache_file}" \
|
|
140
|
+
"issues-open-100" \
|
|
141
|
+
"array" \
|
|
142
|
+
'[]' \
|
|
143
|
+
flow_github_issue_list_json_live \
|
|
144
|
+
"$REPO_SLUG" \
|
|
145
|
+
open \
|
|
146
|
+
100
|
|
147
|
+
return 0
|
|
46
148
|
fi
|
|
47
149
|
cat "${cache_file}"
|
|
48
150
|
}
|
|
@@ -51,9 +153,16 @@ heartbeat_cached_pr_list_json() {
|
|
|
51
153
|
mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
|
|
52
154
|
local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/prs.json"
|
|
53
155
|
if [[ ! -f "${cache_file}" ]]; then
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
156
|
+
heartbeat_cached_json_with_local_mirror \
|
|
157
|
+
"${cache_file}" \
|
|
158
|
+
"prs-open-100" \
|
|
159
|
+
"array" \
|
|
160
|
+
'[]' \
|
|
161
|
+
flow_github_pr_list_json_live \
|
|
162
|
+
"$REPO_SLUG" \
|
|
163
|
+
open \
|
|
164
|
+
100
|
|
165
|
+
return 0
|
|
57
166
|
fi
|
|
58
167
|
cat "${cache_file}"
|
|
59
168
|
}
|
|
@@ -111,7 +220,6 @@ heartbeat_retry_reason_is_baseline_blocked() {
|
|
|
111
220
|
heartbeat_issue_json_cached() {
|
|
112
221
|
local issue_id="${1:?issue id required}"
|
|
113
222
|
local cache_file=""
|
|
114
|
-
local issue_json=""
|
|
115
223
|
|
|
116
224
|
if [[ ! -d "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}" ]]; then
|
|
117
225
|
mkdir -p "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}"
|
|
@@ -123,9 +231,14 @@ heartbeat_issue_json_cached() {
|
|
|
123
231
|
return 0
|
|
124
232
|
fi
|
|
125
233
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
234
|
+
heartbeat_cached_json_with_local_mirror \
|
|
235
|
+
"${cache_file}" \
|
|
236
|
+
"issue-${issue_id}" \
|
|
237
|
+
"object" \
|
|
238
|
+
'{}' \
|
|
239
|
+
flow_github_issue_view_json_live \
|
|
240
|
+
"$REPO_SLUG" \
|
|
241
|
+
"$issue_id"
|
|
129
242
|
}
|
|
130
243
|
|
|
131
244
|
heartbeat_open_agent_pr_issue_ids() {
|
|
@@ -483,9 +596,9 @@ heartbeat_mark_issue_running() {
|
|
|
483
596
|
local cached_json
|
|
484
597
|
cached_json="$(heartbeat_issue_json_cached "$issue_id" 2>/dev/null || true)"
|
|
485
598
|
if [[ "$is_heavy" == "yes" ]]; then
|
|
486
|
-
ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-
|
|
599
|
+
ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null || true
|
|
487
600
|
else
|
|
488
|
-
ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-
|
|
601
|
+
ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-blocked --add agent-running >/dev/null || true
|
|
489
602
|
fi
|
|
490
603
|
}
|
|
491
604
|
|
|
@@ -218,7 +218,7 @@ issue_publish_extra_args() {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
issue_remove_running() {
|
|
221
|
-
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "${REPO_SLUG}" --number "$ISSUE_ID" --remove agent-
|
|
221
|
+
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "${REPO_SLUG}" --number "$ISSUE_ID" --remove agent-running --remove agent-blocked >/dev/null || true
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
issue_mark_blocked() {
|
|
@@ -75,7 +75,7 @@ pr_cleanup_linked_issue_session() {
|
|
|
75
75
|
|
|
76
76
|
local should_close
|
|
77
77
|
should_close="$(pr_linked_issue_should_close "$issue_id")"
|
|
78
|
-
update_args=(--remove agent-
|
|
78
|
+
update_args=(--remove agent-running --remove agent-blocked --remove agent-e2e-heavy --remove agent-automerge --remove agent-exclusive)
|
|
79
79
|
pr_best_effort_update_labels --repo-slug "${REPO_SLUG}" --number "$issue_id" "${update_args[@]}"
|
|
80
80
|
|
|
81
81
|
local issue_session="${ISSUE_SESSION_PREFIX}${issue_id}"
|