Skip to content

Commit 7467eea

Browse files
committed
create-extractor: use the scheduler's status labels; drop add-live-case
Retires this pipeline's two invented labels for the scheduler's own vocabulary. `extractor-request` stays for the issue's whole life — it marks what the issue IS — and its STATE is now: extractor-in-progress -> agent-running extractor-blocked-needs-human -> needs-human The scheduler guarantees both labels exist before any preprocessing runs, so the worker's own label-creation goes away entirely. `ready-for-agent` is deliberately never applied to a request: it is the executor's trigger. A request stays claimed while its PR is in review, which outlives the executor's 3h stale-claim sweep — that sweep is now scoped to dispatch issues, so this pipeline releases its own stale claims instead: a claimed request with no open `claude/extractor/*` PR naming it, quiet past a 6h grace period, is released with a comment and picked up again next pass. A run that dies between claiming and delivering no longer strands its request silently, which the invented label never handled either. Also removes the add-live-case skill. What it uniquely held — the record-a-page procedure and the gardening flow — is four lines now that the record-page task acts on a committed `.url`, and it belongs beside the live-test contract it depends on, so it moves into testing-guide. The rest of the skill was already duplicated there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016NgBmWQmxLFPybrhSTz8i3
1 parent e53f368 commit 7467eea

14 files changed

Lines changed: 180 additions & 125 deletions

File tree

.claudinite/local/packs/gcec/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fingerprinted or seeded.
1717
| Prose | [RULES.md](RULES.md) — working rules, owner commands, testing invariants, codebase gotchas, workflow-failure classification, architecture rules of the road, the capture policy |
1818
| Checks | [test-offline-list-sync](test-offline-list-sync.mjs) (+ red-first fixtures in [pack.test.mjs](pack.test.mjs), run by this repo's own `npm test`) |
1919
| Daily tasks | none |
20-
| Skills | [snapshot-approval](skills/snapshot-approval/SKILL.md) · [merge-and-ci](skills/merge-and-ci/SKILL.md) · [testing-guide](skills/testing-guide/SKILL.md) · [add-live-case](skills/add-live-case/SKILL.md) |
20+
| Skills | [snapshot-approval](skills/snapshot-approval/SKILL.md) · [merge-and-ci](skills/merge-and-ci/SKILL.md) · [testing-guide](skills/testing-guide/SKILL.md) |
2121

2222
## Rules
2323

.claudinite/local/packs/gcec/RULES.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The project's standing working rules — injected at session start while this pack
44
is declared. Activity-scoped procedures live in this pack's skills
5-
(snapshot-approval, merge-and-ci, testing-guide, add-live-case) and surface on
5+
(snapshot-approval, merge-and-ci, testing-guide) and surface on
66
demand; the extractor-automation domain's standing rules are the "Extractor
77
pipeline" section below, and its three scheduled tasks live under `tasks/`.
88

@@ -147,8 +147,8 @@ Standing rules for the extractor-automation domain — the three gcec pack
147147
[`tasks/`](tasks/): **create-extractor** (an `extractor-request` issue → a PR
148148
adding site support), **record-page** (records a committed `.url`'s cached page),
149149
and the weekly **fallback-extractor-improvements** (read a spec only when working
150-
on that pipeline). Adding a cached live case is the
151-
[add-live-case](skills/add-live-case/SKILL.md) skill.
150+
on that pipeline). Adding or refreshing a cached live case by hand is the
151+
[testing-guide](skills/testing-guide/SKILL.md) skill.
152152

153153
- **All page fetching goes through [`scraperapi.mjs`](scraperapi.mjs), from a
154154
task's preprocessing worker and nowhere else.** A rendered fetch through
@@ -184,7 +184,7 @@ on that pipeline). Adding a cached live case is the
184184
`.` / `-` separators are always day-first regardless (#686).
185185
- **An unrecordable page is a dead end, not a failed run.** When a fetch can't
186186
produce a page (bot wall, dead URL, empty render), create-extractor's
187-
preprocessing labels the request `extractor-blocked-needs-human` with the reason
187+
preprocessing labels the request `needs-human` with the reason
188188
and exits **0** — a task failure would converge to a `needs-human` dispatch
189189
issue as well, duplicating the signal and implying the pipeline broke when it
190190
correctly declined. Same for record-page: the pages that did record still land.

.claudinite/local/packs/gcec/pack.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export default {
1818
marker: null,
1919
prose: 'RULES.md',
2020
rules: [testOfflineListSync],
21-
skills: ['snapshot-approval', 'merge-and-ci', 'testing-guide', 'add-live-case'],
21+
skills: ['snapshot-approval', 'merge-and-ci', 'testing-guide'],
2222
run_daily: [],
2323
};

.claudinite/local/packs/gcec/skills/add-live-case/SKILL.md

Lines changed: 0 additions & 53 deletions
This file was deleted.

.claudinite/local/packs/gcec/skills/testing-guide/SKILL.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ npm test # everything above except e2e
1818
The invariants (mirror tree, reviewed-contract cases, `REFERENCE_NOW` floor,
1919
refusal-test discipline) are always-loaded in the gcec pack's RULES.md. General
2020
test discipline (see-it-fail, green-twice, high-watermark gating) is canon.
21-
Adding a cached live case is the gcec pack's **add-live-case**
22-
skill; the fallback-coverage gate's invariants are in that pack's RULES.md.
21+
Adding a cached live case is covered under "Live integration tests" below; the
22+
fallback-coverage gate's invariants are in the gcec pack's RULES.md.
2323

2424
## The requirements model — every leaf has exactly one case
2525

@@ -49,11 +49,24 @@ is *claimed*, not every leaf *faithfully* verified (#435; the banner in
4949
`expected/`: `expected.events` is the **complete, exact** array the extractor
5050
produces — deep-equal on `title`/`start`/`end`/`location`/`ctz`/`details`, no
5151
matchers, array length included. The page URL lives **only** in
52-
`data/server-fetched/<name>.url` (single source of truth — the fetch workflow
53-
and the test both read it; never in the case file). Tests run offline against
54-
the committed cached HTML, loaded into a DOM at the `.url`'s URL so hostname
55-
detection behaves exactly as in Chrome. `test.yml` never fetches — recording
56-
goes through the record-page task (see add-live-case).
52+
`data/server-fetched/<name>.url` (single source of truth — the recorder and the
53+
test both read it; never in the case file). Tests run offline against the
54+
committed cached HTML, loaded into a DOM at the `.url`'s URL so hostname
55+
detection behaves exactly as in Chrome. `test.yml` never fetches.
56+
57+
**Adding one.** Usually you don't: file an `extractor-request` issue with the
58+
event URL and the `create-extractor` task records the page, writes the
59+
extractor, and opens the PR. To add a case by hand, land the `.url` alone first
60+
— a committed `.url` with no sibling `.html` *is* the request the `record-page`
61+
task acts on, so it fetches the page (ScraperAPI; this sandbox is bot-blocked)
62+
and opens a PR carrying it. Merge that, then run `npm run test:live` and paste
63+
the printed values into `expected/<name>.json` — never hand-write them.
64+
65+
**Gardening.** When an event page is taken down, point `<name>.url` at a newer
66+
event **and delete the stale `<name>.html`** in the same commit; that is the
67+
whole re-record request. Until an HTML file exists, `test:live` fails with
68+
`Missing cached HTML for "<name>"`, so land the refresh before (or with) the
69+
case's updated `expected`.
5770

5871
## UI snapshots
5972

.claudinite/local/packs/gcec/tasks/create-extractor/prepare.mjs

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
// 2. triage each eligible one oldest-first (triage.js, over the committed
1313
// sources' real matches() + the popup's classifyHost) and CLOSE every
1414
// deny/allow/duplicate with its canned message — no agent, no branch;
15-
// 3. for the first request that genuinely needs an extractor: claim it, branch,
16-
// scaffold, prove the offline baseline green, and commit;
15+
// 3. for the first request that genuinely needs an extractor: claim it
16+
// (`agent-running`, the scheduler's own label — no custom vocabulary here),
17+
// branch, scaffold, prove the offline baseline green, and commit;
1718
// 4. RECORD THE PAGE through ScraperAPI directly. Preprocessing runs inside
1819
// Actions, where SCRAPER_API_KEY already lives (the task names it in
1920
// `required_secrets`, which the wiring converge stamps into the workflow).
@@ -48,22 +49,31 @@ const { runTriage } = require('./triage.js');
4849
const { addSample } = require('./attach-sample-url.js');
4950

5051
const API = 'https://api.github.com';
52+
// `extractor-request` marks the issue KIND (the form applies it; it stays for the
53+
// issue's life). Its STATE uses the scheduler's own labels rather than any
54+
// invented here — the scheduler guarantees they exist before preprocessing runs,
55+
// so nothing below has to create them:
56+
// agent-running — this pipeline owns the request (from claim until its PR lands)
57+
// needs-human — handed over; the pipeline will not pick it up again
58+
// `ready-for-agent` is never applied to a request: it is the executor's trigger.
5159
const REQUEST_LABEL = 'extractor-request';
52-
const BLOCKED_LABEL = 'extractor-blocked-needs-human';
53-
// The claim label. It is what makes the precondition's cheap gate correct across
54-
// hours: the scheduler runs this task hourly and does NOT know a previous run is
55-
// still in flight (preprocessing runs before, and independently of, the dispatch
56-
// issue's at-most-one-open guard). A claimed request drops out of the precondition's
57-
// eligible set, so an in-flight extractor is never scaffolded twice.
58-
const CLAIMED_LABEL = 'extractor-in-progress';
60+
const CLAIMED_LABEL = 'agent-running';
61+
const BLOCKED_LABEL = 'needs-human';
62+
const BRANCH_PREFIX = 'claude/extractor/';
5963
const DATA_DIR = 'dev/requirements/extractor/data/server-fetched';
6064

65+
// How long a claimed request with nothing to show for it may sit before this
66+
// pipeline releases it. The executor's stale-`agent-running` sweep covers dispatch
67+
// issues only (it cannot know that a request stays claimed while its PR is in
68+
// review), so releasing our OWN stale claims is this worker's job.
69+
const STALE_CLAIM_MS = 6 * 60 * 60 * 1000;
70+
6171
// --- pure helpers (unit-tested; no I/O) --------------------------------------
6272

6373
// The requests this run may act on, oldest first. Mirrors the precondition's
64-
// eligibility exactly (task.mjs `eligibleRequests`) so the gate and the worker can
65-
// never disagree about what "eligible" means — the worker re-derives it from full
66-
// issue objects rather than trusting the gate's older snapshot.
74+
// eligibility exactly so the gate and the worker can never disagree about what
75+
// "eligible" means — the worker re-derives it from full issue objects rather than
76+
// trusting the gate's older snapshot.
6777
export function eligible(issues) {
6878
const labelsOf = (i) => (i.labels ?? []).map((l) => (typeof l === 'string' ? l : l?.name ?? ''));
6979
return (issues ?? [])
@@ -77,6 +87,24 @@ export function eligible(issues) {
7787
.sort((a, b) => a.number - b.number);
7888
}
7989

90+
// Claimed requests whose claim is stale: nothing this pipeline produced is still
91+
// open for them (no `claude/extractor/*` PR says `Closes #N`) and the issue has
92+
// been quiet past the grace period — so a run died between claiming and delivering.
93+
// Releasing the claim makes them eligible again next hour. `openPrs` is the repo's
94+
// open PRs; `now` is passed in so this is pure.
95+
export function staleClaims(issues, openPrs, now) {
96+
const claimedByPr = new Set(
97+
(openPrs ?? [])
98+
.filter((pr) => String(pr?.head?.ref ?? '').startsWith(BRANCH_PREFIX))
99+
.flatMap((pr) => [...String(pr?.body ?? '').matchAll(/\bCloses #(\d+)/gi)].map((m) => Number(m[1]))),
100+
);
101+
return (issues ?? [])
102+
.filter((i) => (i.labels ?? []).some((l) => (typeof l === 'string' ? l : l?.name) === CLAIMED_LABEL))
103+
.filter((i) => !claimedByPr.has(i.number))
104+
.filter((i) => now - new Date(i.updated_at ?? 0).getTime() > STALE_CLAIM_MS)
105+
.map((i) => i.number);
106+
}
107+
80108
// The PR title for a finished run, by mode — the same two phrasings the routine
81109
// spec used to ask the agent to write by hand.
82110
export function prTitle(mode, host, sourceBase) {
@@ -115,14 +143,6 @@ async function gh(path, { method = 'GET', body } = {}) {
115143

116144
const comment = (n, text) => gh(`/repos/${REPO}/issues/${n}/comments`, { method: 'POST', body: { body: text } });
117145

118-
// GitHub 422s when an unknown label is applied and never creates one on demand, so
119-
// the thing that assigns a label guarantees it first (idempotent; self-healing if
120-
// someone deletes it).
121-
async function ensureLabel(name, color, description) {
122-
const { status } = await gh(`/repos/${REPO}/labels`, { method: 'POST', body: { name, color, description } });
123-
if (status !== 201 && status !== 422) console.log(`! could not ensure label "${name}": ${status}`);
124-
}
125-
126146
const addLabel = (n, name) => gh(`/repos/${REPO}/issues/${n}/labels`, { method: 'POST', body: { labels: [name] } });
127147
const removeLabel = (n, name) => gh(`/repos/${REPO}/issues/${n}/labels/${encodeURIComponent(name)}`, { method: 'DELETE' });
128148
const closeNotPlanned = (n) => gh(`/repos/${REPO}/issues/${n}`, { method: 'PATCH', body: { state: 'closed', state_reason: 'not_planned' } });
@@ -163,7 +183,6 @@ async function closeSkipped(issue, decision) {
163183
// release the claim. Never a task failure — the pipeline correctly declining a page
164184
// is a normal outcome.
165185
async function handToHuman(number, why) {
166-
await ensureLabel(BLOCKED_LABEL, 'D93F0B', 'An automated extractor run stopped here and needs a maintainer');
167186
await comment(number, why);
168187
await addLabel(number, BLOCKED_LABEL);
169188
await removeLabel(number, CLAIMED_LABEL);
@@ -236,9 +255,22 @@ export async function main() {
236255
if (!REPO || !TOKEN) throw new Error('no CLAUDINITE_REPO/GITHUB_TOKEN — not in an Actions context');
237256
if (!scraperKey) throw new Error('SCRAPER_API_KEY is not set');
238257

239-
const requests = eligible(await openRequests());
258+
const allRequests = await openRequests();
259+
260+
// Release our own stale claims first (the executor's sweep covers dispatch
261+
// issues only). A request claimed by a run that died before delivering becomes
262+
// eligible again; one whose PR is still open stays claimed however long review
263+
// takes.
264+
const { json: openPrs } = await gh(`/repos/${REPO}/pulls?state=open&per_page=100`);
265+
for (const number of staleClaims(allRequests, Array.isArray(openPrs) ? openPrs : [], Date.now())) {
266+
await removeLabel(number, CLAIMED_LABEL);
267+
await comment(number, 'Releasing this request: an automated run claimed it but never delivered a pull request. The pipeline will pick it up again on its next pass.');
268+
console.log(`create-extractor: released a stale claim on #${number}`);
269+
}
270+
271+
const requests = eligible(allRequests);
240272
if (!requests.length) {
241-
console.log('create-extractor: no eligible request (the precondition\'s snapshot is stale) — nothing to do');
273+
console.log('create-extractor: nothing eligible to act on this run');
242274
return;
243275
}
244276

@@ -253,7 +285,7 @@ export async function main() {
253285
const d = await runTriage({ body: issue.body ?? '', title: issue.title, number: issue.number }, undefined, peers);
254286
if (d.skipAgent) { await closeSkipped(issue, d); continue; }
255287
if (!d.url || !d.branch) {
256-
await handToHuman(issue.number, 'This request has no parseable event URL, so the pipeline could not act on it. Edit the issue to include the event page URL and remove the `extractor-blocked-needs-human` label to retry.');
288+
await handToHuman(issue.number, 'This request has no parseable event URL, so the pipeline could not act on it. Edit the issue to include the event page URL and remove the `needs-human` label to retry.');
257289
continue;
258290
}
259291
target = issue;
@@ -267,7 +299,6 @@ export async function main() {
267299

268300
// Claim it before any expensive work, so the next hourly run's precondition sees
269301
// it as in flight even if this run dies partway.
270-
await ensureLabel(CLAIMED_LABEL, '0E8A16', 'An automated extractor run is working on this request');
271302
await addLabel(target.number, CLAIMED_LABEL);
272303
console.log(`create-extractor: #${target.number}${decision.host} (${decision.mode} mode, case ${decision.caseName})`);
273304

.claudinite/local/packs/gcec/tasks/create-extractor/task.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ triggers `test.yml` on the PR; one green run suffices.
8585
**Bail** (the page was not one usable event, or the postcondition failed): leave
8686
the case's `events` empty, make no source change, **close the draft PR**, comment
8787
a one-sentence diagnosis of what the page actually is on the request issue, and
88-
label it `extractor-blocked-needs-human` (removing `extractor-in-progress`). No
89-
PR, no merge.
88+
swap its `agent-running` label for `needs-human`. No PR, no merge.
89+
90+
Either way the request keeps `extractor-request` — that marks what the issue *is*,
91+
for its whole life; `agent-running` / `needs-human` carry its state. On success
92+
leave `agent-running` in place: it is what stops the pipeline re-scaffolding the
93+
request while its PR is in review, and merging the PR closes the issue.
9094

9195
---
9296

@@ -118,10 +122,10 @@ venue's zone — it neither supplies the `ctz` nor vetoes deriving one from the
118122

119123
## Manual fallback
120124

121-
When the pipeline hands an issue to a human (`extractor-blocked-needs-human`), or
122-
to add a source by hand: same shape — add `custom/<site>.js`, `npm run index`,
125+
When the pipeline hands an issue to a human (`needs-human`), or to add a source by
126+
hand: same shape — add `custom/<site>.js`, `npm run index`,
123127
register the host in `supportedDomains` (`extension/fallback-lists.json`), add a
124-
reviewed case (the [add-live-case](../../skills/add-live-case/SKILL.md) skill), and
128+
reviewed case (the [testing-guide](../../skills/testing-guide/SKILL.md) skill), and
125129
record the host as an extractor-support requirement leaf in
126130
`dev/requirements/requirements.md` §11 (see
127131
[`dev/requirements/README.md`](../../../../../../dev/requirements/README.md)).

0 commit comments

Comments
 (0)