Commitf8f6bd0
fix(agent-journey): self-heal orphaned probe repos across runs
fix(agent-journey): self-heal orphaned probe repos across runs journey-20260715034933-nte1 was found still live on the dashboard a day after its run — the step-10 cleanup only runs inside this process's try/finally, which can't protect against the process itself dying mid-run (killed terminal, crashed script, host restart). A hard kill skips `finally` entirely, same as any JS runtime, regardless of what caused that particular run to die. Rather than chase the one-off cause, added a step 1b: before creating this run's scratch repo, list the owner's repos and delete any journey-* probe older than 1 hour (grace period avoids racing a concurrently-running journey). WARN-only — a sweep failure must never block the merge-lifecycle certification that is the actual point of this script. Self-healing across runs instead of depending on any single run completing cleanly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 file changed+36−0f8f6bd0ace151bd02c6650c6c39277bf6d1f7308
1 changed file+36−0
Modifiedscripts/agent-journey.ts+36−0View fileUnifiedSplit
@@ -5,6 +5,9 @@
55// platforms can open and merge PRs here".
66//
77// 1. identity GET /api/v2/user
8// 1b. sweep delete any journey-* repos >1h old left behind by a
9// prior run that never reached its own step 10 (e.g.
10// the process was killed mid-run) — WARN-only
811// 2. create repo POST /api/v2/repos (no MCP create-repo tool exists — platform gap, noted)
912// 3. git push smart-HTTP receive-pack with PAT basic auth
1013// 4. branch + edit gluecron_create_branch + gluecron_write_file
@@ -122,6 +125,39 @@ async function main() {
122125 owner = user!.username;
123126 if (`${owner}/${REPO}`.toLowerCase() === "ccantynz/gluecron.com") throw new Error("safety: refusing to touch the self-host repo");
124127
128 // 1b. sweep leftover probes from earlier runs — WARN-only, best-effort.
129 // The step-10 cleanup below only runs inside this process's try/finally,
130 // which can't protect against the process itself dying mid-run (killed
131 // terminal, crashed script, host restart) — a hard kill skips `finally`
132 // entirely, same as any JS runtime. Rather than chase every possible
133 // cause of a mid-run death, sweep for orphaned `journey-*` repos here so
134 // cleanup is self-healing across runs instead of depending on any single
135 // run completing cleanly. 1-hour grace period avoids racing a
136 // concurrently-running journey.
137 await step("1b. sweep leftover journey-* probes", async () => {
138 const r = await api("GET", `/api/v2/users/${encodeURIComponent(owner)}/repos`);
139 if (r.status !== 200 || !Array.isArray(r.json)) throw new Error(`HTTP ${r.status}: ${r.text.slice(0, 200)}`);
140 const cutoff = Date.now() - 60 * 60 * 1000;
141 const orphans = r.json.filter((repo: any) =>
142 /^journey-\d{14}-[a-z0-9]{4}$/.test(repo.name) &&
143 repo.name !== REPO &&
144 new Date(repo.createdAt).getTime() < cutoff
145 );
146 if (orphans.length === 0) return;
147 let swept = 0;
148 const failures: string[] = [];
149 for (const repo of orphans) {
150 try {
151 await mcp("gluecron_delete_repo", { owner, repo: repo.name });
152 swept++;
153 } catch (err) {
154 failures.push(`${repo.name}: ${(err as Error).message}`);
155 }
156 }
157 console.log(` swept ${swept}/${orphans.length} orphaned probe repo(s)`);
158 if (failures.length > 0) throw new Error(`${failures.length} orphan(s) failed to delete: ${failures.join("; ").slice(0, 300)}`);
159 }, { warnOnly: true });
160
125161 // 2. create scratch repo
126162 await step("2. create scratch repo (POST /api/v2/repos)", async () => {
127163 const r = await api("POST", "/api/v2/repos", { name: REPO, description: "agent-journey probe — auto-deleted", private: false });
128164