From c53e2770be6968659f97265645555c7c65f30a40 Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 16 Oct 2026 15:36:41 +0200 Subject: [PATCH 1/4] =?UTF-8?q?docs(plan-321):=20record=20the=20shepherd's?= =?UTF-8?q?=20four=20measured=20P0=20inputs=20in=20=C2=A75?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule-to-skill-input path (code read at a7939a74), GitHub rate budget (gh api rate_limit live probe, 5000 req/h core), failing-job log sizes (gh api .../actions/jobs//logs on CI run 37460214524 — Test job 923,330 bytes / 5,955 lines, audit job 32,310 bytes / 421 lines; a last-300-lines, under-60 KB tail bound carried the failure for both job kinds), and the attention list's fields (no branch, PR or repo). P0's exit is met. --- docs/plans/321-the-loop-recovers-from-ci.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/plans/321-the-loop-recovers-from-ci.md b/docs/plans/321-the-loop-recovers-from-ci.md index 37c49379..20199fd9 100644 --- a/docs/plans/321-the-loop-recovers-from-ci.md +++ b/docs/plans/321-the-loop-recovers-from-ci.md @@ -69,7 +69,12 @@ Every pass leaves the handoff current and one run in the ledger. A `fix` also le ## 5. Phases -**P0 — Measure.** How a fired schedule starts a skill run and what input reaches it (`scheduleCrud`); GitHub's rate budget against the polling interval; the size of a failing job's log and the excerpt that carries the failure; whether `workflow.run.list_attention` can name a branch. _Exit:_ each answer in this plan with the command that produced it. +**P0 — Measure.** How a fired schedule starts a skill run and what input reaches it (`scheduleCrud`); GitHub's rate budget against the polling interval; the size of a failing job's log and the excerpt that carries the failure; whether `workflow.run.list_attention` can name a branch. _Exit:_ each answer in this plan with the command that produced it — met. + +1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`ScheduleEvaluator.ts`, `inputResolution.ts`, `startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim and unchecked at creation (`scheduleCrud.ts:152-160`, `:499`; `operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`prShepherd.ts:136-141`, `158-165`). +2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token, 2026-10-16 12:47 GMT; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out, token expires 2026-10-25 09:47 UTC. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`execution.ts:74-163`, `config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`api-errors.ts:92`, `:157`). +3. **The size of a failing job's log.** _Commands:_ `gh api repos/aflowai/aflow/actions/runs/37460214524/attempts/1/jobs` to name the failing jobs, then `gh api repos/aflowai/aflow/actions/jobs//logs` — GitHub's `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs`, which answers with a redirect to a signed URL — for CI run 37460214524 on `aflowai/aflow` (PR #94's first run). _Answer:_ the Test job (112257622723) is 5,955 lines, 923,330 bytes; Vitest's "Failed Tests" block begins at line 5,676, so the last 280 lines carry the failure and its assertion. The Dependency audit job (112257622741) is 421 lines, 32,310 bytes; the advisories first appear at line 232 and the final attempt's block begins at line 387, so the last 35 lines carry every advisory once. An excerpt bounded at the last 300 lines, under 60 KB, carried the failure for both job kinds observed; a bound needs no endpoint binding to hold, and the D5 endpoints are P1's. +4. **Whether the attention list can name a branch.** _Command:_ `workflow.run.list_attention` (pending and consumed items). _Answer:_ no. Items carry `id`, `kind`, `relatedRunId`, `relatedResource` (`workflow_run:`), `payload.terminalStatus`, `priority`, `createdAt`, `consumedAt`, `own` — no branch, no PR, no repo field; learning what a run produced costs a follow-up run-detail read per item. **P1 — One pass, by hand.** The skill with `read-*`, `decide`, `handoff` and `release`, fired manually against a pull request with a planted failing test; `fix` and `publish` land it. _Exit:_ the planted failure is fixed and pushed within one pass without a hand; a second pass on the same check run is `wait`. From 087d86c6ccddd5b263c02e94723732762078e86a Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 16 Oct 2026 15:44:27 +0200 Subject: [PATCH 2/4] docs(plan-321): the P0 measurements change the decisions they contradict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D1/D2 now say a schedule starts an agent run and the skill runs because that agent calls workflow.run.start with the inputs its template carries (the agent target and the folder binding named as static JSON). D3.4 names the mechanism replacing the attention list's missing branch — a bounded read of the space's runs by workflow and input field — and §4 turns its conditional into the packages/schemas change that read needs, with the P2 exit following. The polling interval is derived: one pass for N open PRs costs the list + one check-runs read per PR + one jobs read per failing run + one log read per failing job, against the 5,000-an-hour core budget, with the default interval written as the number and the arithmetic. Status: P0 measured, P1 next. --- docs/plans/321-the-loop-recovers-from-ci.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plans/321-the-loop-recovers-from-ci.md b/docs/plans/321-the-loop-recovers-from-ci.md index 20199fd9..ae565278 100644 --- a/docs/plans/321-the-loop-recovers-from-ci.md +++ b/docs/plans/321-the-loop-recovers-from-ci.md @@ -1,6 +1,6 @@ # Plan 321 — The loop recovers from CI -**Status:** 📋 planned — nothing built; start at P0 +**Status:** 📋 P0 measured (§5); P1 is next — nothing built **Builds on:** Plan 315 (the loop: commission → review → publish; `mergeFrom`; the push posture), the catalog's `pr-shepherd` skill (the hosted code lane's shape of the same job), the scheduler (`agent.schedule.create`), and the `github` connector. @@ -14,16 +14,16 @@ What the hosted `pr-shepherd` already settles: one invocation is describe → re ## 2. Decisions -**D1 — The shepherd is a skill, run by the Runner, and every decision it makes is read from data.** `local-pr-shepherd` is a catalog skill on the host lane: it finds the pull requests the loop opened, reads each one's live state, decides `fix | wait | abandon | leave`, and for `fix` commissions the coding agent on the pull request's branch with `base: ` and `mergeFrom: origin/` and publishes the result appended, through Local Publish, under the folder's posture. No step of it is a Helmsman conversation. _Rejected:_ a Helmsman prompt that says "check the pull requests" — a cold session with prose instructions is the shape that commissions duplicate work; what a cold run needs is the state as inputs to its tasks (D3), not a paragraph about being careful. +**D1 — The shepherd is a skill, run by the Runner, started by an agent that decides nothing, and every decision it makes is read from data.** A schedule does not start a skill: it starts an agent run, and the skill runs because that agent calls `workflow.run.start` (§5 P0.1). The schedule therefore targets the Helmsman (`{ kind: 'platform-role', systemRole: 'helmsman' }`), the platform agent that holds `workflow.run.start`, and its one act in that run is to call it for `local-pr-shepherd` with the inputs the schedule's template carries. Those inputs are static JSON written into the template when the schedule is created: the skill's slug and the folder binding, `bindingId`, as literal values — no `$memoryRef`, which fails the firing, and nothing derived at fire time, which a template cannot carry. `local-pr-shepherd` is a catalog skill on the host lane: it finds the pull requests the loop opened, reads each one's live state, decides `fix | wait | abandon | leave`, and for `fix` commissions the coding agent on the pull request's branch with `base: ` and `mergeFrom: origin/` and publishes the result appended, through Local Publish, under the folder's posture. No step of the skill is a Helmsman conversation: the Helmsman's run starts it and reads nothing about pull requests. _Rejected:_ a Helmsman prompt that says "check the pull requests" — a cold session with prose instructions is the shape that commissions duplicate work; what a cold run needs is the state as inputs to its tasks (D3), not a paragraph about being careful. -**D2 — The trigger is a schedule on the local edition, and a webhook later where there is a public address.** A recurring schedule (`agent.schedule.create`, `cron`) fires the skill every few minutes while the executor is paired; the interval is an operator knob with a default measured in P0 against GitHub's rate budget, not a constant chosen here. The firing carries nothing but the connected folder: the skill discovers its pull requests itself. The appliance's own webhook ingest is the hosted edition's trigger, where `api.aflow.ai` can receive GitHub's `check_suite` events; a laptop cannot, so it polls. A recurring schedule's `maxFirings` is finite by design; the skill's last task renews the schedule when it is near its cap, so the shepherd does not expire silently. +**D2 — The trigger is a schedule on the local edition, and a webhook later where there is a public address.** A recurring schedule (`agent.schedule.create`, `action: 'start_run'`, `cron`) starts a Helmsman run every five minutes while the executor is paired, and that run starts the skill (D1); the interval is an operator knob whose default, `*/5 * * * *`, is derived from GitHub's rate budget in §5 P0.2. The schedule's target is the Helmsman, and its template is static JSON naming the skill's slug and the folder binding (`bindingId`), checked at creation only for non-emptiness. The firing carries nothing but that: the skill discovers its pull requests itself, because a template carries no live value and no cursor across firings. The appliance's own webhook ingest is the hosted edition's trigger, where `api.aflow.ai` can receive GitHub's `check_suite` events; a laptop cannot, so it polls. A recurring schedule's `maxFirings` is finite by design; the skill's last task renews the schedule when it is near its cap, with the same target and template, so the shepherd does not expire silently. **D3 — A cold run reads four things before it acts, and acts on none it did not read.** 1. **The pull requests that are its to tend**: open pull requests whose head branch is under the folder's publish prefix and whose body carries Local Publish's marker. A pull request a person opened, or one labelled `hands-off`, is left alone. 2. **The head's checks**: `listCheckRuns` on the head sha. The decision reads the conclusions, not the names. 3. **The handoff**: one comment per pull request, ``, carrying `attemptCount`, the `checkRunId` of the last failure acted on, the head sha it acted on, and the run id of the fix it commissioned. Written by the shepherd alone. -4. **The space's in-flight work on that branch**: a lease in the memory store, `shepherd///`, taken for the duration of a fix and released at its end, holding the run id and the time; and the space's attention list (`workflow.run.list_attention`) for a paused publication or commission naming that branch. A branch with a live lease or a paused run on it is `wait`, whoever started that run — a person's conversation included. +4. **The space's in-flight work on that branch**: a lease in the memory store, `shepherd///`, taken for the duration of a fix and released at its end, holding the run id and the time; and a bounded read of the space's runs by workflow and input field — `workflow.run.list`, space-wide and one page at most, for the non-terminal runs, paused included, of `publish-local-changes` whose `branch` input is the branch and of `commission-change` whose `base` input is the branch. The attention list cannot answer this: its items name a run, never a branch (§5 P0.4). A branch with a live lease or a non-terminal run on it is `wait`, whoever started that run — a person's conversation included. The four reads are tasks with typed outputs, and the deciding step receives them as its inputs. A read that fails ends the run as `wait` with the reason, never as a guess. @@ -63,7 +63,7 @@ Every pass leaves the handoff current and one run in the ledger. A `fix` also le ## 4. What it affects - `packages/platform-artifacts` — the `local-pr-shepherd` skill and its bundle; `listWorkflowRunJobs` and `getJobLogs` on the `github` connector; Local Publish's body marker. -- `packages/schemas` — nothing new unless P0 finds the attention list cannot name a branch, in which case a bounded read of a space's runs by workflow and input field. +- `packages/schemas` — `workflow.run.list`, the bounded read D3.4 needs, because the attention list cannot name a branch (§5 P0.4): its input is a workflow slug, one input field and the value it must equal, a status filter defaulting to the non-terminal statuses with `paused` among them, and a `limit` (default 25, max 100); its output is each matching run's id, status, start time and `own`, space-wide. It is registered in the `workflow.run` group beside `list_attention` (`packages/schemas/src/operations/workflow/registrations.ts`), a group the capability profiles already carry. It lands before P2, whose exit — a branch with a paused publication is `wait` — is proven through it. - `packages/web-product` — the shepherd's recent decisions in the space context (D7). - `docs/dev/driving-work-through-the-loop.md` — a stream that wants a branch left alone labels its pull request `hands-off`. @@ -71,8 +71,8 @@ Every pass leaves the handoff current and one run in the ledger. A `fix` also le **P0 — Measure.** How a fired schedule starts a skill run and what input reaches it (`scheduleCrud`); GitHub's rate budget against the polling interval; the size of a failing job's log and the excerpt that carries the failure; whether `workflow.run.list_attention` can name a branch. _Exit:_ each answer in this plan with the command that produced it — met. -1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`ScheduleEvaluator.ts`, `inputResolution.ts`, `startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim and unchecked at creation (`scheduleCrud.ts:152-160`, `:499`; `operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`prShepherd.ts:136-141`, `158-165`). -2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token, 2026-10-16 12:47 GMT; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out, token expires 2026-10-25 09:47 UTC. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`execution.ts:74-163`, `config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`api-errors.ts:92`, `:157`). +1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts`, `packages/schemas/src/schedules/inputResolution.ts`, `apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim, checked at creation only for non-emptiness (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/scheduleCrud.ts:152-160`, `:499`; `packages/schemas/src/schedules/operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`packages/schemas/src/schedules/inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`packages/schemas/src/runtime/agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/workflowCrud/run/start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`packages/platform-artifacts/src/skillCatalog/prShepherd.ts:136-141`, `:158-165`). +2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`apps/aflow-executor-api/src/lib/header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`apps/aflow-executor-api/src/handlers/api/execution.ts:74-163`, `apps/aflow-executor-api/src/handlers/api/config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`apps/aflow-executor-api/src/lib/api-errors.ts:92`, `:157`). _The interval it gives:_ one pass over N open pull requests costs 1 list + N check-runs reads + R jobs reads (one per failing workflow run) + J log reads (one per failing job), so 1 + N + R + J requests. With N = 10, every pull request red on one run with three failing jobs: 1 + 10 + 10 + 30 = 51 requests a pass. One pass every five minutes is 12 an hour: 12 × 51 = 612 requests an hour, 612 / 5,000 ≈ 12% of the budget; with every GET retried twice, the generic retry's ceiling, 3 × 612 = 1,836, ≈ 37%. Five minutes spends the whole budget only at 5,000 / 12 ≈ 416 requests a pass — for pull requests each red on one failing job, 1 + 3N = 416, N ≈ 138. The default interval is five minutes, `*/5 * * * *`, and the rest of the hour's budget stays with the commission, Local Publish and the operator's own use of the same token. 3. **The size of a failing job's log.** _Commands:_ `gh api repos/aflowai/aflow/actions/runs/37460214524/attempts/1/jobs` to name the failing jobs, then `gh api repos/aflowai/aflow/actions/jobs//logs` — GitHub's `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs`, which answers with a redirect to a signed URL — for CI run 37460214524 on `aflowai/aflow` (PR #94's first run). _Answer:_ the Test job (112257622723) is 5,955 lines, 923,330 bytes; Vitest's "Failed Tests" block begins at line 5,676, so the last 280 lines carry the failure and its assertion. The Dependency audit job (112257622741) is 421 lines, 32,310 bytes; the advisories first appear at line 232 and the final attempt's block begins at line 387, so the last 35 lines carry every advisory once. An excerpt bounded at the last 300 lines, under 60 KB, carried the failure for both job kinds observed; a bound needs no endpoint binding to hold, and the D5 endpoints are P1's. 4. **Whether the attention list can name a branch.** _Command:_ `workflow.run.list_attention` (pending and consumed items). _Answer:_ no. Items carry `id`, `kind`, `relatedRunId`, `relatedResource` (`workflow_run:`), `payload.terminalStatus`, `priority`, `createdAt`, `consumedAt`, `own` — no branch, no PR, no repo field; learning what a run produced costs a follow-up run-detail read per item. From 587ddb4d30dbf094b370e0e560554f835a5ec8b4 Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 16 Oct 2026 15:49:30 +0200 Subject: [PATCH 3/4] docs(plan-321): D1/D2 name the custom step-graph agent the schedule targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no platform role 'helmsman' — the Helmsman's systemRole is cybernetic-helmsman — and a Helmsman run acting on a template is the prompt D1 rejects. A schedule carries only a persistent target, platform-role or custom-agent, never an inline agent (scheduleCrud.ts, comment near :388). The agent that decides nothing is a custom agent: an operator-authored step-graph agent (agent.manage.create, packages/schemas/src/runtime/agentTarget.ts custom-agent) whose definition is one operation step, workflow.run.start of local-pr-shepherd with bindingId as a literal, and no model turn at all — no ai.agent.turn, no prompt, nothing to drift. The schedule targets that agent by slug; the Local PR Shepherd bundle installs the agent beside the skill so the operator creates nothing by hand; D2 names the same agent. Every systemRole: 'helmsman' is replaced; the P0.1 $memoryRef citation names its own file by repository-relative path. --- docs/plans/321-the-loop-recovers-from-ci.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plans/321-the-loop-recovers-from-ci.md b/docs/plans/321-the-loop-recovers-from-ci.md index ae565278..be67b80b 100644 --- a/docs/plans/321-the-loop-recovers-from-ci.md +++ b/docs/plans/321-the-loop-recovers-from-ci.md @@ -14,9 +14,9 @@ What the hosted `pr-shepherd` already settles: one invocation is describe → re ## 2. Decisions -**D1 — The shepherd is a skill, run by the Runner, started by an agent that decides nothing, and every decision it makes is read from data.** A schedule does not start a skill: it starts an agent run, and the skill runs because that agent calls `workflow.run.start` (§5 P0.1). The schedule therefore targets the Helmsman (`{ kind: 'platform-role', systemRole: 'helmsman' }`), the platform agent that holds `workflow.run.start`, and its one act in that run is to call it for `local-pr-shepherd` with the inputs the schedule's template carries. Those inputs are static JSON written into the template when the schedule is created: the skill's slug and the folder binding, `bindingId`, as literal values — no `$memoryRef`, which fails the firing, and nothing derived at fire time, which a template cannot carry. `local-pr-shepherd` is a catalog skill on the host lane: it finds the pull requests the loop opened, reads each one's live state, decides `fix | wait | abandon | leave`, and for `fix` commissions the coding agent on the pull request's branch with `base: ` and `mergeFrom: origin/` and publishes the result appended, through Local Publish, under the folder's posture. No step of the skill is a Helmsman conversation: the Helmsman's run starts it and reads nothing about pull requests. _Rejected:_ a Helmsman prompt that says "check the pull requests" — a cold session with prose instructions is the shape that commissions duplicate work; what a cold run needs is the state as inputs to its tasks (D3), not a paragraph about being careful. +**D1 — The shepherd is a skill, run by the Runner, started by an agent that decides nothing, and every decision it makes is read from data.** A schedule does not start a skill: it starts an agent run, and the skill runs because that agent calls `workflow.run.start` (§5 P0.1). A schedule carries only a persistent target, `platform-role` or `custom-agent`, never an inline agent (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/scheduleCrud.ts:388`), and the platform agent that holds `workflow.run.start`, the Helmsman (`cybernetic-helmsman`), is a model acting on a template — the prompt rejected below. The agent that decides nothing is therefore a custom agent (`custom-agent`, `packages/schemas/src/runtime/agentTarget.ts`): a step-graph agent, of the kind `agent.manage.create` writes, whose definition is one operation step — `workflow.run.start` of `local-pr-shepherd`, with the skill's slug and the folder binding, `bindingId`, as literal values — and no model turn at all: no `ai.agent.turn`, no prompt, nothing to drift. The step's inputs are fixed when the agent is installed — no `$memoryRef`, which fails the firing, and nothing derived at fire time, which a template cannot carry. The Local PR Shepherd bundle installs the agent beside the skill, under its slug, so the operator creates nothing by hand, and the schedule targets that agent by its slug: the target it stores is `{ kind: 'custom-agent', agentId }`, the id of the agent the slug names. `local-pr-shepherd` is a catalog skill on the host lane: it finds the pull requests the loop opened, reads each one's live state, decides `fix | wait | abandon | leave`, and for `fix` commissions the coding agent on the pull request's branch with `base: ` and `mergeFrom: origin/` and publishes the result appended, through Local Publish, under the folder's posture. No step of the skill is a Helmsman conversation, and the agent's run that starts it reads nothing about pull requests. _Rejected:_ a Helmsman prompt that says "check the pull requests" — a cold session with prose instructions is the shape that commissions duplicate work; what a cold run needs is the state as inputs to its tasks (D3), not a paragraph about being careful. -**D2 — The trigger is a schedule on the local edition, and a webhook later where there is a public address.** A recurring schedule (`agent.schedule.create`, `action: 'start_run'`, `cron`) starts a Helmsman run every five minutes while the executor is paired, and that run starts the skill (D1); the interval is an operator knob whose default, `*/5 * * * *`, is derived from GitHub's rate budget in §5 P0.2. The schedule's target is the Helmsman, and its template is static JSON naming the skill's slug and the folder binding (`bindingId`), checked at creation only for non-emptiness. The firing carries nothing but that: the skill discovers its pull requests itself, because a template carries no live value and no cursor across firings. The appliance's own webhook ingest is the hosted edition's trigger, where `api.aflow.ai` can receive GitHub's `check_suite` events; a laptop cannot, so it polls. A recurring schedule's `maxFirings` is finite by design; the skill's last task renews the schedule when it is near its cap, with the same target and template, so the shepherd does not expire silently. +**D2 — The trigger is a schedule on the local edition, and a webhook later where there is a public address.** A recurring schedule (`agent.schedule.create`, `action: 'start_run'`, `cron`) starts a run of the shepherd's custom agent every five minutes while the executor is paired, and that agent's one operation step starts the skill (D1); the interval is an operator knob whose default, `*/5 * * * *`, is derived from GitHub's rate budget in §5 P0.2. The schedule's target is the custom agent the Local PR Shepherd bundle installs, named by its slug; its template is static JSON, checked at creation only for non-emptiness, and the step reads none of it — the skill's slug and `bindingId` are literals in the agent's definition. The firing carries nothing else: the skill discovers its pull requests itself, because a template carries no live value and no cursor across firings. The appliance's own webhook ingest is the hosted edition's trigger, where `api.aflow.ai` can receive GitHub's `check_suite` events; a laptop cannot, so it polls. A recurring schedule's `maxFirings` is finite by design; the skill's last task renews the schedule when it is near its cap, with the same target and template, so the shepherd does not expire silently. **D3 — A cold run reads four things before it acts, and acts on none it did not read.** @@ -71,7 +71,7 @@ Every pass leaves the handoff current and one run in the ledger. A `fix` also le **P0 — Measure.** How a fired schedule starts a skill run and what input reaches it (`scheduleCrud`); GitHub's rate budget against the polling interval; the size of a failing job's log and the excerpt that carries the failure; whether `workflow.run.list_attention` can name a branch. _Exit:_ each answer in this plan with the command that produced it — met. -1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts`, `packages/schemas/src/schedules/inputResolution.ts`, `apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim, checked at creation only for non-emptiness (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/scheduleCrud.ts:152-160`, `:499`; `packages/schemas/src/schedules/operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`packages/schemas/src/schedules/inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`packages/schemas/src/runtime/agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/workflowCrud/run/start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`packages/platform-artifacts/src/skillCatalog/prShepherd.ts:136-141`, `:158-165`). +1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts`, `packages/schemas/src/schedules/inputResolution.ts`, `apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim, checked at creation only for non-emptiness (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/scheduleCrud.ts:152-160`, `:499`; `packages/schemas/src/schedules/operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`packages/schemas/src/schedules/inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`packages/schemas/src/runtime/agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/workflowCrud/run/start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`packages/platform-artifacts/src/skillCatalog/prShepherd.ts:136-141`, `:158-165`). 2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`apps/aflow-executor-api/src/lib/header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`apps/aflow-executor-api/src/handlers/api/execution.ts:74-163`, `apps/aflow-executor-api/src/handlers/api/config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`apps/aflow-executor-api/src/lib/api-errors.ts:92`, `:157`). _The interval it gives:_ one pass over N open pull requests costs 1 list + N check-runs reads + R jobs reads (one per failing workflow run) + J log reads (one per failing job), so 1 + N + R + J requests. With N = 10, every pull request red on one run with three failing jobs: 1 + 10 + 10 + 30 = 51 requests a pass. One pass every five minutes is 12 an hour: 12 × 51 = 612 requests an hour, 612 / 5,000 ≈ 12% of the budget; with every GET retried twice, the generic retry's ceiling, 3 × 612 = 1,836, ≈ 37%. Five minutes spends the whole budget only at 5,000 / 12 ≈ 416 requests a pass — for pull requests each red on one failing job, 1 + 3N = 416, N ≈ 138. The default interval is five minutes, `*/5 * * * *`, and the rest of the hour's budget stays with the commission, Local Publish and the operator's own use of the same token. 3. **The size of a failing job's log.** _Commands:_ `gh api repos/aflowai/aflow/actions/runs/37460214524/attempts/1/jobs` to name the failing jobs, then `gh api repos/aflowai/aflow/actions/jobs//logs` — GitHub's `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs`, which answers with a redirect to a signed URL — for CI run 37460214524 on `aflowai/aflow` (PR #94's first run). _Answer:_ the Test job (112257622723) is 5,955 lines, 923,330 bytes; Vitest's "Failed Tests" block begins at line 5,676, so the last 280 lines carry the failure and its assertion. The Dependency audit job (112257622741) is 421 lines, 32,310 bytes; the advisories first appear at line 232 and the final attempt's block begins at line 387, so the last 35 lines carry every advisory once. An excerpt bounded at the last 300 lines, under 60 KB, carried the failure for both job kinds observed; a bound needs no endpoint binding to hold, and the D5 endpoints are P1's. 4. **Whether the attention list can name a branch.** _Command:_ `workflow.run.list_attention` (pending and consumed items). _Answer:_ no. Items carry `id`, `kind`, `relatedRunId`, `relatedResource` (`workflow_run:`), `payload.terminalStatus`, `priority`, `createdAt`, `consumedAt`, `own` — no branch, no PR, no repo field; learning what a run produced costs a follow-up run-detail read per item. From b9bf461192cee9ed3f61e4ce8cff40f7d4359003 Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 16 Oct 2026 15:55:25 +0200 Subject: [PATCH 4/4] =?UTF-8?q?docs(plan-321):=20the=20three=20minors=20?= =?UTF-8?q?=E2=80=94=20the=20task=20table,=20the=20arithmetic,=20the=20dec?= =?UTF-8?q?ide=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (a) The §3.1 task table's read-in-flight reads workflow.run.list by workflow and input field, as D3.4 says — not workflow.run.list_attention. (b) P0.2's arithmetic is recomputed from what the plan's own tasks cost: three reads and one write per pull request per pass — the check runs, the jobs of a failing run, the log of a failing job, and the handoff comment — against the 5,000-an-hour core budget, with the per-pass cost, the budget share and the break-even N written as arithmetic. (c) §3.1 states the decide rule the measurements answer: the four inputs decide fix, wait, abandon and leave by rule, and a model is reached for only to read a log excerpt a rule cannot. The open question closes. --- docs/plans/321-the-loop-recovers-from-ci.md | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/plans/321-the-loop-recovers-from-ci.md b/docs/plans/321-the-loop-recovers-from-ci.md index be67b80b..1779f783 100644 --- a/docs/plans/321-the-loop-recovers-from-ci.md +++ b/docs/plans/321-the-loop-recovers-from-ci.md @@ -39,22 +39,22 @@ The four reads are tasks with typed outputs, and the deciding step receives them ### 3.1 Tasks -| Task | Operation | Reads / does | -| ---------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -| `read-folder` | `host.binding.inspect` | the branch prefix, the posture, `origin` | -| `list-prs` | `api.http.call` `listPullRequests` | open pull requests, filtered to the prefix and the marker, minus `hands-off` | -| `read-pr` | per pull request: `getPullRequest`, `listCheckRuns`, `listIssueComments` | head sha, checks, the handoff | -| `read-in-flight` | `memory.store.get`, `workflow.run.list_attention` | the lease and any paused run on the branch | -| `decide` | `ai.generate_json` or a pure rule | `fix \| wait \| abandon \| leave`, with the reason; a rule where the inputs decide it alone | -| `read-failure` | `listWorkflowRunJobs`, `getJobLogs` | failing jobs and bounded log excerpts, as a payload | -| `lease` | `memory.store.put` | the lease, written before the commission | -| `fix` | `host.harness.run` | `base: `, `mergeFrom: origin/`, the assembled task | -| `publish` | `workflow.run.start` Local Publish | appended onto the branch, `wait: 'until_complete'` | -| `handoff` | `createIssueComment` / edit | the updated record, every pass | -| `release` | `memory.store.delete` | the lease, on every terminal path | -| `renew` | `agent.schedule.create` | the next schedule when this one nears `maxFirings` | - -Whether `decide` needs a model at all is a P0 question: the inputs above decide the four outcomes without one, and a model is reached for only to read a log excerpt a rule cannot. +| Task | Operation | Reads / does | +| ---------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `read-folder` | `host.binding.inspect` | the branch prefix, the posture, `origin` | +| `list-prs` | `api.http.call` `listPullRequests` | open pull requests, filtered to the prefix and the marker, minus `hands-off` | +| `read-pr` | per pull request: `getPullRequest`, `listCheckRuns`, `listIssueComments` | head sha, checks, the handoff | +| `read-in-flight` | `memory.store.get`, `workflow.run.list` | the lease and the non-terminal runs, paused included, whose branch input is the branch | +| `decide` | a pure rule; `ai.generate_json` for a log excerpt only | `fix \| wait \| abandon \| leave`, with the reason | +| `read-failure` | `listWorkflowRunJobs`, `getJobLogs` | failing jobs and bounded log excerpts, as a payload | +| `lease` | `memory.store.put` | the lease, written before the commission | +| `fix` | `host.harness.run` | `base: `, `mergeFrom: origin/`, the assembled task | +| `publish` | `workflow.run.start` Local Publish | appended onto the branch, `wait: 'until_complete'` | +| `handoff` | `createIssueComment` / edit | the updated record, every pass | +| `release` | `memory.store.delete` | the lease, on every terminal path | +| `renew` | `agent.schedule.create` | the next schedule when this one nears `maxFirings` | + +`decide` is a rule. The four reads of D3 decide `fix`, `wait`, `abandon` and `leave` without a model; a model, `ai.generate_json`, is reached for only to read a log excerpt a rule cannot. ### 3.2 What is recorded @@ -72,7 +72,7 @@ Every pass leaves the handoff current and one run in the ledger. A `fix` also le **P0 — Measure.** How a fired schedule starts a skill run and what input reaches it (`scheduleCrud`); GitHub's rate budget against the polling interval; the size of a failing job's log and the excerpt that carries the failure; whether `workflow.run.list_attention` can name a branch. _Exit:_ each answer in this plan with the command that produced it — met. 1. **The schedule-to-skill-input path.** _Command:_ a code read of the scheduler path (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts`, `packages/schemas/src/schedules/inputResolution.ts`, `apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts` and their callers) by the coding agent at base `a7939a74`. _Answer:_ a schedule never calls `workflow.run.start`; it starts an agent run, and a skill runs only if that agent then calls `workflow.run.start` with inputs it picks itself. The template is stored verbatim, checked at creation only for non-emptiness (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/scheduleCrud.ts:152-160`, `:499`; `packages/schemas/src/schedules/operations.ts:145-153` — no skill-slug field exists). Firing resolves it with ctx `{now}` (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:513`); `on_completion` adds `{sourceRun, now}` where `sourceRun.output` is a `PayloadRef` string, not the dereferenced output, and `input` is never set (`:889-892`, `:937-940`). Substitutions are `$now`, `$sourceRef`, `$memoryRef` (`packages/schemas/src/schedules/inputResolution.ts:68-129`); no caller supplies a memory resolver, so any `$memoryRef` fails the firing (`apps/aflow-orchestrator/src/services/ScheduleEvaluator.ts:514-522`). Dispatch is inline base64 JSON on a `start_run` control message (`:737-757`, `:1090-1093`), trigger `'schedule'`, unattended turn (`packages/schemas/src/runtime/agentPolicies.ts:23-25`). The agent's input contract is checked only at run start (`apps/aflow-orchestrator/src/services/SessionOrchestrator/lifecycle/startRun.ts:117-139`). A scheduled run can carry: static JSON (a branch name, an owner/repo, a SHA range as literal text), `$now` timestamps, and for `on_completion` the source `runId`/`status`/`outputRef`. It cannot carry: live or derived values, any cross-firing cursor (only `firing_count` advances), a skill slug or typed skill inputs as first-class fields, or a repo coordinate — a coding skill's repo is campaign config (`apps/aflow-orchestrator/src/services/SessionOrchestrator/handlers/inlineOps/workflowCrud/run/start.ts:446`) and `pr-shepherd`'s only run input is `prNumber` (`packages/platform-artifacts/src/skillCatalog/prShepherd.ts:136-141`, `:158-165`). -2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`apps/aflow-executor-api/src/lib/header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`apps/aflow-executor-api/src/handlers/api/execution.ts:74-163`, `apps/aflow-executor-api/src/handlers/api/config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`apps/aflow-executor-api/src/lib/api-errors.ts:92`, `:157`). _The interval it gives:_ one pass over N open pull requests costs 1 list + N check-runs reads + R jobs reads (one per failing workflow run) + J log reads (one per failing job), so 1 + N + R + J requests. With N = 10, every pull request red on one run with three failing jobs: 1 + 10 + 10 + 30 = 51 requests a pass. One pass every five minutes is 12 an hour: 12 × 51 = 612 requests an hour, 612 / 5,000 ≈ 12% of the budget; with every GET retried twice, the generic retry's ceiling, 3 × 612 = 1,836, ≈ 37%. Five minutes spends the whole budget only at 5,000 / 12 ≈ 416 requests a pass — for pull requests each red on one failing job, 1 + 3N = 416, N ≈ 138. The default interval is five minutes, `*/5 * * * *`, and the rest of the hour's budget stays with the commission, Local Publish and the operator's own use of the same token. +2. **GitHub's rate budget.** _Command:_ `gh api rate_limit` (live probe of the binding's token; `x-ratelimit-*` headers read off a bound `GET /repos/aflowai/aflow`). _Answer:_ core limit 5,000 requests/hour; 5 used at probe, window reset ~38 minutes out. `x-ratelimit-*` and `Retry-After` reach the calling agent unparsed (`apps/aflow-executor-api/src/lib/header-redaction.ts:9-20`). In code there is no GitHub-specific handling: only the generic retry — 2 retries, exponential backoff 1s→30s, on 429/502/503/504, idempotent methods only (`apps/aflow-executor-api/src/handlers/api/execution.ts:74-163`, `apps/aflow-executor-api/src/handlers/api/config.ts:26-40`); it never reads `Retry-After` or `x-ratelimit-*`; the exhausted-limit 403 is not retried; POST/PATCH calls are never retried on 429; `API_RATE_LIMITED` is declared but never raised (`apps/aflow-executor-api/src/lib/api-errors.ts:92`, `:157`). _The interval it gives:_ one pass costs one `listPullRequests` read, then for each pull request the requests of the §3.1 tasks: `read-pr`'s three reads (`getPullRequest`, `listCheckRuns`, `listIssueComments`), `read-failure`'s jobs read for the failing run and log read for the failing job, and `handoff`'s one write — five reads and one write, 6 requests, for a pull request red on one failing job; each further failing job adds one log read. A pass over N such pull requests is 1 + 6N requests. With N = 10: 1 + 60 = 61 requests a pass. One pass every five minutes is 12 an hour: 12 × 61 = 732 requests an hour, 732 / 5,000 ≈ 15% of the budget. With every GET retried twice, the generic retry's ceiling, and the write not retried: 3 × (1 + 50) + 10 = 163 a pass, 12 × 163 = 1,956 an hour, ≈ 39%. Five minutes spends the whole budget at 5,000 / 12 ≈ 416 requests a pass: 1 + 6N ≤ 416 gives N = 69 (12 × 415 = 4,980), and at the retry ceiling 3 × (1 + 5N) + N = 3 + 16N ≤ 416 gives N = 25 (12 × 403 = 4,836). The default interval is five minutes, `*/5 * * * *`, and the rest of the hour's budget stays with the commission, Local Publish and the operator's own use of the same token. 3. **The size of a failing job's log.** _Commands:_ `gh api repos/aflowai/aflow/actions/runs/37460214524/attempts/1/jobs` to name the failing jobs, then `gh api repos/aflowai/aflow/actions/jobs//logs` — GitHub's `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs`, which answers with a redirect to a signed URL — for CI run 37460214524 on `aflowai/aflow` (PR #94's first run). _Answer:_ the Test job (112257622723) is 5,955 lines, 923,330 bytes; Vitest's "Failed Tests" block begins at line 5,676, so the last 280 lines carry the failure and its assertion. The Dependency audit job (112257622741) is 421 lines, 32,310 bytes; the advisories first appear at line 232 and the final attempt's block begins at line 387, so the last 35 lines carry every advisory once. An excerpt bounded at the last 300 lines, under 60 KB, carried the failure for both job kinds observed; a bound needs no endpoint binding to hold, and the D5 endpoints are P1's. 4. **Whether the attention list can name a branch.** _Command:_ `workflow.run.list_attention` (pending and consumed items). _Answer:_ no. Items carry `id`, `kind`, `relatedRunId`, `relatedResource` (`workflow_run:`), `payload.terminalStatus`, `priority`, `createdAt`, `consumedAt`, `own` — no branch, no PR, no repo field; learning what a run produced costs a follow-up run-detail read per item.