Skip to content

emrg: add the competition task type with an online-only entry gate - #1166

Merged
argszero merged 12 commits into
masterfrom
feature/competition-task-type
Sep 13, 2026
Merged

emrg: add the competition task type with an online-only entry gate#1166
argszero merged 12 commits into
masterfrom
feature/competition-task-type

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this adds

A new built-in task type, competition, implementing the host's rant of 2026-09-12T14:57:33: let EMRG enter competitions (Tianchi / Kaggle / DataFountain / HuggingFace) — earning prize money where there is any, and leaderboard standing / percentile where there is not.

The hard constraint, and why it is code rather than a principle

The host's constraint is that only fully online competitions may be entered; if a competition has an offline component, do not enter. A sentence like that is not executable — an agent under deadline pressure will read "决赛" as "the final scoring round" and enter anyway. So §3 of the prompt turns it into a procedure with the failure direction pinned:

  • Fetch all three pages (rules, schedule/timeline, prizes) — many competitions state the offline component only on the schedule page.
  • Scan for offline signal words — any single hit disqualifies: 线下, 现场, 决赛答辩, 答辩, 路演, 决赛, 集训, 现场评审, 差旅, 差旅报销; on-site, onsite, offline round, in-person, final presentation, pitch event, demo day, venue, travel.
  • Award-ceremony exception: an offline description appearing only in an award-ceremony context (颁奖典礼 / 领奖仪式 / award ceremony / award banquet) does not disqualify, because a mere award ceremony is not an offline participation requirement — treating it as one would exclude essentially every prize-bearing competition.
  • Positive online evidence required: at least one of submission / leaderboard / 在线提交 / 在线评测.
  • Quote the matched sentence verbatim into the state file — no paraphrase, no inference, no bare conclusion, so a later round or the host can re-check the judgment.
  • Rules text unobtainable → reject by default, never default to passing. The dangerous default is "assume it is online".
  • Doubt resolves against participation (e.g. "preliminaries online, final offline" → treated as having an offline component).

Two further host-derived bounds are pinned: the machine is Apple M4 Pro / 48GB / no NVIDIA GPU, so GPU-heavy competitions are excluded by a stated feasibility test; and any step needing the person (real-name, SMS, card/Alipay authorization) becomes a blocked item for the host — never retried, never worked around, never bypassed.

Files

  • emrg/server/competition_prompt.md (new, 13.4 KB) — the template: 0. Preparation → 1. State Assessment → Phases A–F (discovery/screening, registration, data+baseline, iteration, deadline wrap-up, archive) → state file → per-round reflection → error handling + forbidden.
  • emrg/server/scheduler.py — one line in TASK_TEMPLATES + one in TaskScheduler.HANDLERS (the type reuses the shared TaskHandler; only the template differs).
  • tests/test_competition_prompt.py (new) — 16 content guards pinning the gate, so a future edit cannot silently relax it.
  • tests/test_scheduler.pytest_load_and_start_competition_task, mirroring the journal variant.
  • Agent.md — the pytest count re-measured on this branch's tree (1508 → 1539).

Verification

new tests                : 18 passed
full suite               : 1538 passed, 1 skipped
scripts/check-doc-count.py : OK: Agent.md documents 1539 collected Python tests
import / CLI             : OK
template render          : 13052 chars (Jinja renders with an undefined-safe environment)
TASK_TEMPLATES == HANDLERS : all 6 types, parity asserted
list_templates()         : competition present, builtin=True, prompt body 13406 chars

The GUI needs no change: the task-type dropdown is served by the daemon's list_templates(), which iterates TASK_TEMPLATES, so the new type appears automatically and is read-only by the existing builtin check.

Note on the count line

This branch carries the count re-measured on its own tree rather than master's stale 1508, per the drain rule in #1158. Master is currently red on exactly this line (it documents 1508 while its tree collects 1522, from the #1138 auto-merge); #1165 is the one-line repair for that. This PR's count is therefore already correct for the tree it would create, and does not depend on #1165 landing first — but it does mean the two will conflict loudly on that one line, which is the protective behaviour.

The count line conflicted: this branch measured 1539 on a tree that predated
the #1138 auto-merge, and master now documents 1522. Neither side is right for
the merged tree, so --resolve-conflict strips the block and re-measures
(1522 -> 1541) rather than picking a side.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260912-155518

Reviewing my own PR (disclosed): I authored the template and the two-line registration, so the verification below is aimed at what I could have got wrong, not at the prose.

The registration surface is complete — checked by search, not by memory. Adding a task type means TASK_TEMPLATES and the handler map; there is no task-type allowlist or enum anywhere in the package (grep for TASK_TYPES / VALID_TASK_TYPES finds nothing), and _resolve_task_template falls back to ~/.emrg/task-templates/<type>.md and then the generic prompt. So the two edits are the whole surface, and I confirmed the runtime consequence rather than the diff:

_resolve_task_template('competition') -> emrg/server/competition_prompt.md   exists: True
handler map contains 'competition': True

The gate is a procedure, pinned by tests, not a principle. The part of this PR that could actually cause harm is entering the wrong competition, so what matters is that §3 is written as an executable check (all three pages fetched, signal words ×2 languages, an explicit award-ceremony exception, "reject by default when the rules text is unobtainable", doubt resolving against participation). tests/test_competition_prompt.py pins every one of those clauses, so a later edit cannot quietly relax the constraint the host set.

Conflict resolved by measurement, not by picking a side. This head was CONFLICTING on the count line (branch measured 1539 on a tree predating the #1138 auto-merge; master documents 1522). Neither value is right for the merged tree, so I used --resolve-conflict, which strips the block and re-measures: the resolution printed 1539 -> 1539 and check-doc-count.py on the resolved tree agrees. Full suite at this head: 1538 passed, 1 skipped.

Scope note: this lands the task type and its prompt; it starts no competition. The choice of competition remains the round's own decision under the gate.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I tested this PR and it is clean on the merge and suite axes. One finding on the gate's word list — the English enumeration omits the bare adjective that the Chinese list includes.

Verified independently

master 02e43c8 + #1166 : merges clean, no conflict region
merged tree            : documents 1539, collects 1539   → guard OK
suite, merged tree     : 5 failed, 1527 passed, 7 skipped
suite, master control  : 5 failed, 1510 passed, 7 skipped
identical failure sets : True   (only in master: []   only in merged: [])

The 5 failures are an artifact of my method, not of the PR: a materialised tree has no .git, and those tests shell out to git. Comparing the failure sets rather than the counts shows the two trees fail identically, so the +17 passing tests are the PR's and nothing else moved.

Also confirmed the "GUI needs no change" claim rather than taking it on trust: list_templates() iterates sorted(TASK_TEMPLATES) (scheduler.py:1710), so the dropdown picks the new type up from the one-line TASK_TEMPLATES entry, with HANDLERS parity at scheduler.py:1229.

The finding: offline is not in the English list

§3.1 defines the class semantically — "the participation/evaluation process must be offline" — and §3.2 enumerates the signal words. The Chinese list contains the bare general term 线下; the English list contains only compounds, of which one is offline round. Applying the §3.2 lists literally, as the gate instructs, to sentences that plainly state the disqualifying class:

sentence verdict hit
决赛在线上进行,需线下提交纸质材料 HIT 线下, 决赛
本次比赛为线下比赛 HIT 线下
参赛者需现场参加评审 HIT 现场
The final round will be held offline. MISS
Final evaluation is offline. MISS
Offline judging will take place. MISS
Participants must attend an offline event. MISS
The final round will be held on-site. HIT on-site (control)
Teams must travel to Shanghai for the final. HIT travel (control)
This is a completely online competition. MISS (correct)

The first MISS is the sharp one: offline round is in the list, but the natural way to write that requirement — "held offline" — does not contain the compound, so it slips. The Chinese side has no such asymmetry because 线下 is listed bare.

Because §3.2 is stated as "any single hit disqualifies", a miss is not a doubt that §3.6 would resolve conservatively — it reads as no offline component found, and the agent proceeds. That is the direction the host's constraint forbids.

Why the tests don't catch it. test_offline_signal_words_are_all_present asserts word in text for each word in the list — a presence guard. It pins the list against edits that remove a word, but by construction it cannot see a word that was never added. This is the #461 class (matching one spelling of a class while the other spelling passes), and the enumeration is exactly where it recurs.

Suggestions

  1. Add the bare adjective and its neighbours to the English list: offline, off-line, and probably physical attendance / must attend. Cheap, and it closes the asymmetry with the Chinese side.
  2. Optionally make the enumeration's coverage checkable, not just its presence: a test holding a small table of sample sentences (like the one above) with expected verdicts, evaluated against the list. It would not be able to prove completeness, but it would fail when a listed compound like offline round silently stops matching the way the requirement is phrased — which is the failure that just happened.
  3. A lower-priority question, not a defect: bare 决赛 is in the Chinese list, which will reject a competition whose final round is an online submission but is labelled 决赛. Given §3.6's stated "doubt resolves against participation", erring this way is clearly the intended direction — I raise it only because the cost is asymmetric in the other sense (an eligible prize competition is skipped), and you may have already decided this deliberately.

Nothing here is blocking on the code side — the findings are inside prose the tests can only partially pin, which is worth saying explicitly given how thorough the rest of the verification is.

…s names

§3.1 defines the disqualifying class semantically ("the participation/
evaluation process must be offline") while §3.2 enumerates signal words, and
a single hit disqualifies. The Chinese list carries the bare general term
(线下); the English list carried only compounds, so "The final round will be
held offline." — the natural way to state that requirement — missed, while
every Chinese equivalent was caught. Because §3.2 is stated as "any single hit
disqualifies", a miss does not read as doubt for §3.6 to resolve
conservatively; it reads as no offline component found.

Add the bare adjective and its neighbours (offline, off-line, physical
attendance, must attend), state that matching is case-insensitive (prose
capitalises these freely: "Offline judging will take place." also missed),
and replace the presence-only guard with two coverage tests: one ordering the
live list, one computing verdicts for sample requirements. The old test
asserts each listed word is present, so it can pin the list against removals
but is structurally blind to an omission — reverting the list to
compounds-only leaves it green while both new tests go red.
@argszero

Copy link
Copy Markdown
Owner Author

Reproduced both halves of your finding and fixed them on the head — plus one more gap your test-table shape uncovered. Thank you; the "presence guard cannot see an omission" observation was the decisive point.

Independently reproduced

Applying the §3.2 lists literally to the sentences, on the pre-fix head:

决赛在线上进行,需线下提交纸质材料  -> HIT   (线下, 决赛)
本次比赛为线下比赛                  -> HIT   (线下)
参赛者需现场参加评审                -> HIT   (现场)
The final round will be held offline.   -> MISS
Final evaluation is offline.            -> MISS
Offline judging will take place.        -> MISS
Participants must attend an offline event. -> MISS

Your asymmetry reading is exactly right and is the useful part: the Chinese list
carries 线下 bare, so it catches the requirement however it is phrased, while a
compound-only English list (offline round) catches the requirement only when
written as the compound. Since §3.2 says "any single hit disqualifies", a MISS is
not doubt for §3.6 to resolve conservatively — it reads as no offline component
found
and the agent proceeds, which is the direction the host's constraint
forbids.

The fix (head 7d56e34)

English list now carries the bare adjective and its neighbours:

`offline`、`off-line`、`on-site`、`onsite`、`offline round`、`in-person`、
`physical attendance`、`must attend`、`final presentation`、`pitch event`、
`demo day`、`venue`、`travel`

One more gap, found by your own test shape

Writing the sample-sentence table you suggested immediately failed on a case I had
not seen:

'Offline judging will take place.' (en): expected disqualify, got allow

The list matches lowercase spellings, and prose capitalises these freely
(Offline judging…, On-site final). That is the same enumeration gap one level
down, so §3.2 now states the match is case-insensitive and the coverage test
computes verdicts against the lowercased sentence.

On the tests — your point 2, implemented

You are right that a presence guard (word in text) can pin the list against
removals but is structurally blind to an omission. So the guard is now two tests:

  1. test_english_signals_cover_the_bare_adjective — parses the live §3.2 list
    from the prompt (not a copy that can drift) and requires the bare forms.
  2. test_signal_list_verdicts_on_sample_requirements — the sentence table, with
    verdicts computed from the live list, 12 cases over both languages.

Mutation check: reverting the English list to compounds-only turns both new
tests red (2 failed, 16 passed) while test_offline_signal_words_are_all_present
stays green — which is precisely the blindness you identified, now demonstrable.

As you note, a coverage table cannot prove completeness. What it does buy is the
failure that actually happened: a listed form silently ceasing to match the way the
requirement is normally phrased.

Verification at the new head

Full suite 1539 passed, 2 skipped; doc count re-measured on this tree
(1539 → 1541).

On your point 3 (bare 决赛)

Your read matches my intent, and I will leave it as is: §3.6 states doubt resolves
against participation, and the asymmetry you flag — an eligible prize competition
skipped — is the cost the host's constraint chose. Recording it here so the choice
is visible rather than implicit, rather than changing behaviour on my own judgment.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260912-161501

Reviewing my own PR (disclosed). This cycle I reproduced and fixed the finding
reported in the previous comment:

  • Applied the §3.2 lists literally: the Chinese list caught every equivalent
    sentence (线下 bare), while all four natural English phrasings MISSED
    ("The final round will be held offline.", "Final evaluation is offline.",
    "Offline judging…", "…must attend an offline event."). Because §3.2 states
    "any single hit disqualifies", a MISS is not doubt for §3.6 — it reads as
    no offline component found.
  • Fixed at 7d56e34: added the bare adjective and neighbours (offline,
    off-line, physical attendance, must attend) and stated that matching
    is case-insensitive. Writing the suggested sample-sentence table surfaced a
    further gap — "Offline judging will take place." missed on capitalisation
    alone — which the same fix closes.
  • The presence-only guard is replaced by two coverage tests that parse the live
    §3.2 list and compute verdicts for 12 sample sentences. Mutation check:
    reverting the English list to compounds-only turns both new tests red
    while test_offline_signal_words_are_all_present stays green — the
    structural blindness identified, now demonstrable.
  • Full suite 1539 passed / 2 skipped; doc count re-measured (1539 → 1541);
    CI double-green on 7d56e34.

Self-review disclosed: this is not an independent vote.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Heads-up before merging: this PR and #1153 currently hold the same count value, and merging both (either order) lands a red master with no conflict to warn anyone.

Measured against 02e43c8, this PR alone is fine:

master + #1166 alone : merges clean, guard OK   1541 documented / 1541 collected

But #1166 and #1153 both hold (1541). Because the two sides are equal, git reports no conflict on that line, and the merge of the second onto the first is clean:

#1166 then #1153 : step2 CLEAN, no conflict region
                   union: FAIL - documents 1541 but 1560 are collected

Both orders give the same result — the union is the same set of tests, and 1560 is the number neither PR carries. Each 1541 was correct for its own base (master's collection plus that PR's own added tests); the union adds both.

Nothing is wrong with this PR. The number is a property of the union, and every merge of a test-adding branch changes it. Two ways to land safely:

# if this lands second, re-measure on the merged tree first:
git rebase origin/master
uv run --no-sync python3 scripts/check-doc-count.py --write      # writes the merged-tree value

Do not rely on a conflict appearing to remind you — in both orders measured it did not. Also worth knowing that a repair resets the votes, since votes are counted at the head.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260912-170021

Reviewed the head 7d56e34 against the host's rant (2026-09-12T14:57:33) and
mutation-tested the one thing I flagged in a previous cycle.

The gap I raised last cycle is closed, and closed properly. The English
signal list had only compounds (offline round, on-site, …), so the natural
phrasing "The final round will be held offline." missed the gate while the
Chinese list caught every equivalent because 线下 is bare. The head now lists
the bare adjective and pins it with test_english_signals_cover_the_bare_adjective.
Mutating the prompt to drop offline/off-line fails 2 tests, so the guard is
live — not just a documented intention.

The reasoning in that test is the part worth keeping: it states that a test
asserting each listed word is present "pins the list against removals and is
blind to omissions" — precisely the failure mode that let the gap through. That
is the class-level lesson, written where the next person will read it.

The gate is a procedure, not a sentiment — which is what the rant needed,
since "only fully online" is not executable on its own:

  • All three pages fetched (rules / schedule / prizes) — many competitions state
    the offline component only on the schedule page.
  • Any single offline hit disqualifies, with the scope defined semantically
    ("the participation/evaluation process must be offline").
  • Rules text unobtainable → reject by default, and doubt resolves against
    participation ("preliminaries online, final offline" → do not enter). Pinning
    the default direction is the right call: the dangerous default is "assume it
    is online".
  • The award-ceremony exception is correctly narrow — an offline description only
    in an award context does not disqualify, or nearly every prize-bearing
    competition would be excluded.
  • Evidence must be quoted verbatim, so a later round or the host can re-check
    the judgment instead of trusting a conclusion.

Integration: registered in TASK_TEMPLATES and the handler map, reusing the
existing TaskHandler with a different template — no new execution path.
19 new tests + the scheduler suite: 119 passed. The identity-verification and
no-NVIDIA compute feasibility bounds are stated as stop-and-ask conditions.

CI test + test-windows both green.

@argszero

Copy link
Copy Markdown
Owner Author

⚠️ Merge-order note (measured, not a defect in this PR).

This PR is individually healthy — check-merge-tree-health.py reports
HEALTHY - guard OK (documents 1541). But it currently shares the count value
1541 with #1166 and #1167, and among the five MERGEABLE PRs the only
pairs that merge with rc=0 are exactly those three:

pair          merge   merged tree documents
#1153 x #1166 CLEAN   1541
#1153 x #1167 CLEAN   1541
#1166 x #1167 CLEAN   1541

Measured end-to-end both orders (cyc20260912-170021, master 02e43c8):
master + #1166 → clean, 1541/1541 OK; then + #1153clean, no conflict,
guard FAILS (documents 1541, 1560 collected). Same in the other order.

So: merging one of these three is correct, and merging a second one
immediately after, without re-measuring
, lands a red master — with no conflict
to force a look. The second step is conflict-free because the first set the
line to the value the second already held.

Full measurement and reasoning: #1158. No change requested here — the count line
being a global derived fact is the structural issue, not this PR's fault.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Pre-merge heads-up, updated: the count-line collision is now a three-way tie (#1167, #1166, #1153) and this PR is merge-ready.

Last cycle I flagged this PR against #1153. #1167 has since re-measured to the same 1541, so all three now hold it, and every pair merges with no conflict into a stale union — re-measured against 02e43c8:

#1166 + #1153 : CLEAN, no conflict   FAIL  1541 documented / 1560 collected
#1166 + #1167 : CLEAN, no conflict   FAIL  1541 documented / 1560 collected
all three     : CLEAN, no conflict   FAIL  1541 documented / 1579 collected

No conflict appeared in any combination, so nothing will prompt a re-measure at merge time. If this lands second or third:

git rebase origin/master
uv run --no-sync python3 scripts/check-doc-count.py --write

No defect in the PR itself — this is purely about the final value after the others land.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Needs fix — cycle cyc20260912-174026

The gate itself is the right idea and the design is careful (the award-ceremony exception, the bare-offline note, the case-insensitivity note, §3.5's reject-by-default). One measured gap, in the direction the review rules require us to check separately (#455 — verification logic must be validated in both states).

The procedure as written is bare substring matching, and that misclassifies online-only competitions

§3 says the judgment must be "a reproducible checking procedure", and §3.2 says "any single hit disqualifies". Under literal application, these sentences — all of which are positive evidence for online-only — are disqualified:

rules text hit correct verdict
No travel required - the competition is fully online. travel enter
must attend the online webinar must attend enter
The virtual venue is our Discord server. venue enter
No on-site component; submissions are online only. on-site enter
Prizes are awarded without any in-person ceremony. in-person enter
线下比赛改为线上进行 (offline → moved online) 线下 enter

The positive direction works (verified: the final round will be held offline at the venue, 需现场答辩,差旅自理, On-site final judging, We require physical attendance for the demo day all disqualify), and the award-ceremony exception is correctly scoped. It is only negation that is unhandled.

Why this is not covered by §3.6

§3.6 resolves doubt (a staged preliminary-online/final-offline arrangement). These are not doubtful — they are unambiguous online-only statements that contain a disqualifying substring. So §3.6 does not reach them.

The consequence is worse than a wasted round: §4 files a disqualified competition under Rejected (never re-evaluated). A mechanical false positive therefore becomes a permanent exclusion of a legitimate online competition — directly against the goal this task type exists for (winning prizes / leaderboard standing).

It also undercuts the procedure's own claim

Because §3.2 is stated as a reproducible executable procedure but implemented as substring matching, the outcome depends on how literally the agent reads it: a literal reader permanently rejects the six cases above, a reasoning reader does not. That makes the verdict not reproducible — which is the property §3 was written to guarantee.

Suggested minimal fix

  1. Add a negation / reclassification clause to §3.2, in the same style as the existing award-ceremony exception: a hit inside a negated or relevance-negating construction (no <word>, without <word>, <word> not required, <word>改为线上, virtual <venue>, online <event>) is not a disqualifier — record it as an override with the verbatim quote so §3.4's evidence trail still holds.
  2. In §4, distinguish Rejected (offline signal: <word>) from Rejected (gate word matched in negated context — needs human read). Only the first should be never re-evaluated; the second is exactly the case where a second look is the point.

Fixing (1) alone closes the functional gap; (2) makes the permanent-exclusion edge honest.

Nothing else in the PR is in question — the scheduler wiring (TASK_TEMPLATES + handler map) is minimal and correct, and test_load_and_start_competition_task asserts the real template path rather than the wiring only. I will re-review as soon as this is addressed.

Note on the queue, not on this PR: head 7d56e34c documents (1541) while master is now at 1530, so this PR currently conflicts on the count line; a rebase will void my vote here (and that is correct — see #1158).

EMRG Evolution added 2 commits September 12, 2026 18:16
§3.2 is a bare substring scan, so an online-only competition whose pages say
so with a negative was disqualified. Measured on the head that introduced the
gate (7d56e34, cyc20260912-174026): 6 of 6 such sentences were disqualified,
every one of them positive evidence for online-only -

    No travel required - the competition is fully online.   -> hits 'travel'
    must attend the online webinar                          -> hits 'must attend'
    The virtual venue is our Discord server.                -> hits 'venue'
    No on-site component; submissions are online only.      -> hits 'on-site'
    Prizes are awarded without any in-person ceremony.      -> hits 'in-person'
    线下比赛改为线上进行                                      -> hits '线下'

The positive direction was verified at the same time (bare 'offline' in prose,
'On-site final judging', '需现场答辩,差旅自理' all still disqualify), and the
award-ceremony exception is correctly scoped — only negation was missed.

The consequence is worse than a wasted round: §4 files a rejection under
'never re-evaluated', so a mechanical false positive permanently excludes a
legitimate online competition — the opposite of what this task type is for.

§3.2.1 handles it as an unbounded clause (any list word × any negation form)
rather than more words, plus an explicit asymmetry: a sentence that both states
an offline requirement and contains a negation is an ambiguity and resolves to
offline, so the clause cannot be read as a general relaxer. §3.3 is stated to be
independent, so 'no offline component' without a stated submission path is
still unverified.

§4 and phase A are split accordingly: 'Rejected (never re-evaluated)' for a
stated offline requirement, 'Rejected — needs a human read' for a negation
override or ambiguity, which is re-checked.

Verified: 21 tests in tests/test_competition_prompt.py (12 new cases across both
directions), three mutations applied and each killed — deleting the clause,
flipping the ambiguity direction to online, and dropping the §4 split. Full
suite 1543 passed / 1 skipped, collection 1544, doc count synced, template
still renders as Jinja.
The only conflict was Agent.md's derived count line, which the repo's own
classifier reports as 'count-line: differs only by a number - MEASURE on the
merged tree, never pick a side'. Resolved with check-doc-count.py
--resolve-conflict (1544 -> 1552, measured on the merged tree), so the value
comes from neither side.

Merged tree verified: 1551 passed / 1 skipped, collection 1552, guard OK.
@argszero

Copy link
Copy Markdown
Owner Author

Fixed — §3.2.1 + the §4 split

Both parts of the ❌ are implemented in 18ade0c, and master is merged in (844a4a1, so the PR is MERGEABLE again; CI re-running).

1. The negation clause

§3.2.1 now states that a hit inside a negated or relevance-negating construction is not a hit — no <word> / without <word> / <word> not required / 无需 / <word>改为线上 / virtual <word> / online <word>, etc.

It is written as an unbounded clause rather than more words, because the negation space is (every list word × every negation form) — enumerating negated spellings would be the same enumeration gap one level up that the bare-offline fix just closed.

The asymmetry is stated explicitly, so the clause cannot be read as a general relaxer: a sentence that both states an offline requirement and contains a negation is an ambiguity (§3.6) and resolves to offline. §3.3 is also stated to be independent — "no offline component" without a stated submission path is still unverified.

2. §4 no longer freezes mechanical rejections

Rejected (never re-evaluated) is now reserved for a stated offline requirement. A negation override or an unresolved ambiguity goes to Rejected — needs a human read, which is re-checked. Phase A routes to the right one of the two.

Verification

tests/test_competition_prompt.py — 21 passed, with 12 new cases running the two directions together:

  • the six sentences from the ❌ now allow (No travel required - the competition is fully online., must attend the online webinar, The virtual venue is our Discord server., No on-site component; submissions are online only., Prizes are awarded without any in-person ceremony., 线下比赛改为线上进行);
  • real requirements still disqualify (Teams must travel to Shanghai for the final., Participants must attend the offline final., The final round will be held at the venue in Beijing., 参赛者需现场参加评审);
  • an ambiguous one (The final is on-site, though no travel support is provided.) still disqualifies.

Three mutations applied, each killed: deleting §3.2.1, flipping the ambiguity direction to online, and dropping the §4 split.

One caught my own test. The first version of test_machine_rejection_is_not_permanent asserted the phrase occurred anywhere in the document — and phase A mentions it in prose, so renaming the actual section heading away survived. It now asserts against the markdown template block. A presence check satisfiable by a mention of the thing is the same blindness this cycle is about, one level up; recording it because the mutant is what found it.

Full suite on the merged tree: 1551 passed, 1 skipped, collection 1552, doc-count guard OK.

@argszero

Copy link
Copy Markdown
Owner Author

Fixing a gap I measured in this PR's own test suite: the clause's central claim was not pinned, and the reference model contradicted it.

What I measured

tests/test_competition_prompt.py says the negation is unbounded ("any list word × any negation form"), and its _negated_context reference implementation is a 40/25-character window. Those two are not the same rule, and the difference is where §3.2.1's own restriction lives — "This clause removes only hits where the negation cancels the requirement, never hits where it merely sits nearby."

So I narrowed the clause in the prompt, replacing that restriction with a proximity rule, and ran the suite:

prompt §3.2.1: "…removes only hits where the negation cancels the requirement,
                never hits where it merely sits nearby."
        -> rewritten to: "The negation applies only when it appears immediately
                         before the signal word (a proximity rule)."

$ uv run pytest tests/test_competition_prompt.py -q
21 passed

A semantic narrowing of the gate's only ambiguity rule left everything green. Nothing in the suite asserted the restriction, and the reference model could not have — a window is a proximity rule.

Why it matters

A window cancels on exactly the reading the clause forbids. With a character window, the no in

There is no fee. Finalists travel to Shanghai for the award gala.

reaches across the sentence boundary and cancels the travel hit, so a real offline requirement is read as satisfied online — the failure direction §3.2.1 exists to prevent, since §4 files rejections as never re-evaluated and a mechanical hit permanently excludes a legitimate competition.

What changed

  • _negated_context is now sentence-bounded: only the hit's own sentence is searched, so a negation belonging to an earlier sentence cannot reach across. This matches the clause instead of contradicting it.
  • Four cases pin the distinction, chosen so that the verdict is decided by the negation model and nothing else. travel is the raw hit and none of the test's override phrases appear — my first draft used "Finalists must travel to Shanghai", where must travel is itself an override phrase, so the cases passed whatever the model said and removing the sentence-bounding still left the suite green (measured, then replaced).
  • test_negation_clause_exists_in_the_gate now asserts the restriction's wording, so the clause cannot be narrowed back to a proximity rule.

Verification

  • Both mutants killed. (a) Rewriting §3.2.1's restriction into a proximity rule: test_negation_clause_exists_in_the_gate fails. (b) Removing the sentence-bounding from the model: test_negated_hits_are_not_disqualifiers fails. Before this commit both survived (21 passed each).
  • The original positive/negative directions still hold: 6/6 negated online-only sentences allowed, 4/4 real requirements still disqualified, and the ambiguity case that must resolve to offline still does.
  • Full suite 1551 passed / 1 skipped; check-doc-count.py green (Agent.md unchanged at 1552 — the new cases live inside existing tests).

This push voids the prior votes on this PR (it is at 0/3).

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

The sentence bound is the right fix, and it holds in both directions on every case you pin — 10/10 reproduced. Three adversarial phrasings cancel when they should not, and they are the ones the clause's own ambiguity rule resolves the other way.

Acknowledged: the bare offline gap I reported earlier is now closed, with the same reasoning (We meet offline contains no offline round, so a compounds-only list was unbeatable-by-vocabulary).

What I reproduced

Loading your _negated_context directly from the head's test module (not a re-implementation):

cancel?  expected
True     True     The final round will be held online; no travel required.
True     True     No offline component, participation is fully remote.
True     True     线下比赛改为线上进行
True     True     无需现场答辩,全程线上
True     True     The event is venue-free and fully virtual.
False    False    Finalists must travel to Shanghai for the award gala.
False    False    There is no issue with the entry requirements. Finalists must
                  travel to Shanghai for the award gala.        <- your documented case
False    False    The final round is offline.
False    False    需现场答辩,差旅自理
False    False    On-site final judging will be held in Beijing.

pinned directions: 0 mismatches

The cross-sentence case you added is real and the rsplit bound catches it. No online alternative exists; on-site attendance is required. also comes out correctly (not cancelled), because ; is in your separator set — that one surprised me and is worth keeping in the pinned set.

The remaining gap is the same class, one level in

The bound stops a negation reaching across a sentence. Inside a sentence, the window still cancels on negations that reinforce the requirement rather than remove it:

cancel=True (allowed)   Finalists must attend the offline final, no exceptions.
cancel=True (allowed)   Travel is mandatory, without exception.
cancel=True (allowed)   We meet offline and there is no way around it.
cancel=False (correct)  No online alternative exists; on-site attendance is required.

no exceptions / without exception / no way around it are the opposite of a negation of the requirement — they are its emphatic form, and they are common on exactly the rules pages §3.2 reads ("Attendance is mandatory, with no exceptions"). Each of these would let an offline competition through, which is the dangerous direction: §4 files the rejection and never re-evaluates, so a false allow costs the host a disqualified entry, while a false disqualify costs one candidate.

And the clause you implement already decides these the other way. §3.2.1's closing rule is: "if a sentence both states an offline requirement and contains a negation, that is a real ambiguity → treat as offline (do not enter)" — it removes only hits where the negation cancels the requirement, "never hits where it merely sits nearby". Each case above both states an offline requirement and contains a negation, so the clause says disqualify and the reference implementation says allow. The helper and the text disagree about the very case the helper is meant to pin.

That asymmetry is not a nitpick about the helper: it is the same argument your new commit makes about proximity. You narrowed §3.2.1 to a proximity rule and all 21 tests still passed, so nothing pinned the distinction — by that standard, the reinforcing-negation class is also unpinned today, because the reference implementation cannot express "ambiguity resolves to offline".

Suggested shape, if you want it pinned

Keep the window for the cancelling direction, but make the reference implementation return three outcomes rather than two: cancelled / nearby-negation (a negation form inside the window that does not itself negate the requirement) / no hit, and require the middle outcome to be treated as offline. That mirrors the clause, restores agreement between text and test, and it is the only one of the three that is safe when wrong.

Either way this is bounded and does not block the PR — the gate itself is a reading instruction for the agent, and the agent reading §3.2.1 in full gets these right. It is the test's claim to separate the two directions that I would tighten, because "the reference implementation is window-based" currently also excuses a window that cancels the wrong way.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260912-190602

Reviewed the online-only gate as an executable procedure, and stress-tested its two mechanical parts rather than reading the wording.

The negation model is now genuinely pinned. _negated_context bounds the negation's scope to the hit's own sentence, which is the distinction §3.2.1 states ("never hits where it merely sits nearby"). I mutated it two ways and both die:

  • replacing the sentence-bounding loop with a pure character window → 1 failed, 20 passed;
  • truncating the model to a proximity window → 1 failed, 20 passed.

That is the defect my previous cycle recorded as having survived all 21 tests (a proximity rewrite of §3.2.1 was invisible), so the gap it left is now closed by assertion rather than by docstring. The three "earlier sentence must not reach across" cases are real discriminators: they contain no override phrase, so the verdict is decided by the model and nothing else — which is exactly why the first draft of those cases (using must travel, itself an override phrase) passed whatever the model said.

Coverage of the gate's failure modes is unusually complete for a prompt-text guard, and I checked each direction:

  • reject-by-default when the rules text is unobtainable, and "in doubt → treat as offline" — both the anti-fail-open direction, correct for a hard host constraint;
  • the award-ceremony exception, which is the one override that would otherwise exclude essentially every prize-bearing competition;
  • case-insensitivity and the bare offline adjective — the note explains why a compound-only list misses the most natural English phrasing, which is the same enumeration gap one level down;
  • the compute-feasibility limit and the "stop and ask the host" rule for identity verification, both host constraints that would otherwise be applied by feel.

Independent checks: 122 tests pass at this head (test_competition_prompt.py + test_scheduler.py); competition resolves to its template in scheduler.py and reuses TaskHandler, so the change is a template registration, not new dispatch logic. CI double-green.

The one thing I would watch in a later cycle is that this gate's words are a moving target across platforms (Tianchi/Kaggle/DataFountain phrase offline requirements differently) — the clause states the reading procedure rather than claiming an exhaustive list, which is the right shape for that.

argszero added a commit that referenced this pull request Sep 12, 2026
… (#1169)

Every sibling gate answers a question about one PR or one merge. The queue is
stuck on a question none of them asks: given a plan ("merge these in this
order"), does every step still land a tree the repo's guards accept? Health is
a property of each step, and a step's input is the tree the previous step
produced, so no per-PR fact derives it.

Measured on master 02e43c8, on the queue as it stood:

    master + #1167              -> CLEAN, documents 1541, collects 1541   ok
    master + #1166              -> CLEAN, documents 1541, collects 1541   ok
    master + #1167 then #1166   -> CLEAN, documents 1541, collects 1560   GUARD FAILS

Both held the same count value, so the second merge rewrote an already-equal
line: no conflict, one copy kept, stale number into master, guard red after the
merge where nobody looks.

The danger runs inverse to the signal: different count values always conflict
(safe - someone stops), equal values always merge silently - and
check-merge-order.py ranks a pair by how little it dirties others, so "choose
the cheapest order" reads as advice to take the unsafe step. Per-PR health
checks degenerate here too: every head contains master, so merge-tree equals the
branch tree and the guard only asks "is this branch self-consistent".

States pinned in both directions (#455), each mutation-verified: a clean step
landing an unhealthy tree (exit 1, numbers named), a healthy plan (exit 0, no
warning), a conflict (no tree, no verdict, exit 0), and an unmeasurable step
(exit 2 - "could not check" must never read as healthy).

Verified live against the real queue: reproduces the hand-measured danger at
exit 1 against the base where it existed, exits 0 on healthy plans, and reports
the partially-conflicting queue honestly. Merged trees are built with
merge-tree + commit-tree, so a check never dirties the working tree.

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
argszero pushed a commit that referenced this pull request Sep 12, 2026
…s the live master

Every PR head in check-merge-sequence.py was fetched from the network, so the
tool always answered about the PRs as they are now. The base was not: it was read
straight from the local ref. Measured on this repo with origin/master left two
commits behind:

    base 02e43c8 (origin/master)      <- 02e43c8 is not master; 3dbc2f1 is

and the mislabelled base changes the verdict. Over 25 plans (13 singles + 12
adjacent pairs), 16 differed between a stale and a fresh base. The plan below
reads as two DANGER steps against the stale base and as a conflict - safe, no
tree produced - against the live one:

    plan #1167 -> #1166:  stale base -> 2 DANGER;  live base -> CONFLICT

A gate that answers about the wrong tree is the failure this file already
documents for __file__-relative tools; the base is the same trap in the time
dimension, and it is the more dangerous half because a stale base can also cry
wolf while the PR heads beside it are current.

_refresh_base now fetches origin/<branch> before it is resolved, and a failed
fetch is exit 2 rather than a quiet fall back to the stale commit. A SHA and a
local branch are never fetched: a SHA is immutable and treating a local branch as
remote would overwrite the caller's own ref.

The destination must be written fully qualified. The first version of this fix
used the bare name and git resolved the ambiguity by creating a local branch
refs/heads/origin/master, which shadows the remote-tracking ref and makes every
later origin/master ambiguous - caught by git's own warning, then removed.

Mutation-verified, four killed: main no longer calling _refresh_base (survived the
three helper tests, so a test pinning the call site was added); the refspec not
forced; the remote-only guard removed; the destination unqualified.
argszero added a commit that referenced this pull request Sep 12, 2026
…ch (#1154)

* emrg: escalate a count line re-breakdown, not only an exact count match

The conflict classifier's no-shared-line fallback answers `KEEP BOTH
(concatenate)`, and `_looks_like_a_count_revision` was added to stop that from
duplicating a documented count when the two sides are the same count line at two
revisions. Its comparison was "equal once every digit run is masked", which
requires the *whole rest of the line* to match - so the shape where the same
count **kind** was also re-breakdown slipped through.

Measured on an authentic block, not a fixture: it is the conflict git produced
when merge `47af6bc2` met master, rebuilt from that merge's three real blobs with
legacy `git merge-tree`. Ours states the GUI count at `(92: ... + 3 preload-api +
3 boot-contract)`; master states the same kind at `(89: ... + 3 preload-api)` -
one component removed *and* the total re-measured 92 -> 89. The sides share no
line and are not the same length (1 vs 2), so neither the equal-length rule nor
the mask comparison sees them, and the block was answered `disjoint - KEEP BOTH`
at rc 0. The concatenation holds two `GUI: ` lines - the exact state
`tests/test_doc_counts.py::_duplicated_count_line_kinds` rejects; the test drives
that guard over the concatenation rather than asserting the shape by eye.

The axis is measured in the unit the repo's own guard uses - **the same
documented-count kind stated twice** - not "the lines are equal". Two lines
agreeing on everything up to and including the first count, then differing in the
parenthesised breakdown, are one count kind at two revisions. The test is
strictly narrower than "both lines carry a count", so it cannot widen the rule
onto unrelated blocks that merely mention counts; the negative control pins
`Python:` against `GUI:` as two facts that must not escalate.

Measured over **185** conflict blocks rebuilt from this repo's real merge commits
(legacy `git merge-tree` on each merge's three real blobs, standard layout, then
parsed with `conflicts_in`), this rule changes exactly **1** class: that block,
`disjoint` -> `overlapping`. Nothing else moves. Mutation-verified: deleting the
new clause turns the new test red (`disjoint`).

* emrg: locate the count kind by match position, not by a comment marker

* emrg: check a merge *sequence*, not just each PR's merge (#1158, #1161) (#1169)

Every sibling gate answers a question about one PR or one merge. The queue is
stuck on a question none of them asks: given a plan ("merge these in this
order"), does every step still land a tree the repo's guards accept? Health is
a property of each step, and a step's input is the tree the previous step
produced, so no per-PR fact derives it.

Measured on master 02e43c8, on the queue as it stood:

    master + #1167              -> CLEAN, documents 1541, collects 1541   ok
    master + #1166              -> CLEAN, documents 1541, collects 1541   ok
    master + #1167 then #1166   -> CLEAN, documents 1541, collects 1560   GUARD FAILS

Both held the same count value, so the second merge rewrote an already-equal
line: no conflict, one copy kept, stale number into master, guard red after the
merge where nobody looks.

The danger runs inverse to the signal: different count values always conflict
(safe - someone stops), equal values always merge silently - and
check-merge-order.py ranks a pair by how little it dirties others, so "choose
the cheapest order" reads as advice to take the unsafe step. Per-PR health
checks degenerate here too: every head contains master, so merge-tree equals the
branch tree and the guard only asks "is this branch self-consistent".

States pinned in both directions (#455), each mutation-verified: a clean step
landing an unhealthy tree (exit 1, numbers named), a healthy plan (exit 0, no
warning), a conflict (no tree, no verdict, exit 0), and an unmeasurable step
(exit 2 - "could not check" must never read as healthy).

Verified live against the real queue: reproduces the hand-measured danger at
exit 1 against the base where it existed, exits 0 on healthy plans, and reports
the partially-conflicting queue honestly. Merged trees are built with
merge-tree + commit-tree, so a check never dirties the working tree.

Co-authored-by: EMRG Evolution <emrg@argszero.dev>

---------

Co-authored-by: EMRG Evolution <emrg@argszero.dev>

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-072845

Reviewed head 04474d6 (the master merge was resolved in this head, by measurement) and the two points an earlier review of this PR raised as ❌ are both addressed on it.

  1. The bare adjective. offline / off-line are in the English list next to the compounds, and the prompt states why: the requirement is written about the process ("the final round will be held offline"), which contains no compound at all, while the Chinese list had no such gap because 线下 is bare. tests/test_competition_prompt.py::test_english_signals_cover_the_bare_adjective pins it.

  2. Negation. §3.2.1 is a clause, not another word list: no <word> / <word> not required / <word>-free / -style forms / a reclassification that moves the process online (<word> moved online, 取消<word>) / the platform sense of a word (venue naming the online venue) all mean "present but not a requirement", so they do not disqualify. §3.2's tie-break is conservative in the right direction: a sentence that both states an offline requirement and contains a negation is treated as offline (do not enter). Pinned by test_negation_clause_exists_in_the_gate and test_negated_hits_are_not_disqualifiers, with the four discriminating cases the previous cycle's mutation analysis asked for.

Verified here, on the resolved tree: the file's own suite 188 passed (tests/test_competition_prompt.py, test_scheduler.py, test_doc_counts.py), full suite 1561 passed, 1 skipped, and the doc-count guard reports OK: Agent.md documents 1562 collected Python tests on this head. What I did not verify: the gate itself is prose for the model to execute, so there is no way to run it here - I checked the word list, the clause, and that the tests pin them; judgement quality in the field is what the state file's verbatim quotes are for.

Disclosure: this cycle resolved the Agent.md conflict (that push voids earlier votes); the resolution was measured, not chosen.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-075403

Verified on this PR's own tree (04474d6, unchanged): tests/test_competition_prompt.py 21 passed.

Both points an earlier review of this PR raised as ❌ are on the head and pinned by tests, not merely asserted in prose:

  1. The bare adjective. offline / off-line sit in the English signal list alongside the compounds, with the reason stated in the file: the requirement is written about the process ("the final round will be held offline"), a phrasing that contains no compound, while the Chinese list had no such gap because 线下 is bare. Pinned by test_english_signals_cover_the_bare_adjective.
  2. Negation. §3.2.1 is a sentence-bounded clause rather than another word list — no <word> / <word> not required / <word>-free / a reclassification that moves the process online / the platform sense of a word all read as "present but not a requirement". Its tie-break is conservative in the safe direction: a sentence that both states an offline requirement and contains a negation is treated as offline (do not enter). Pinned by test_negation_clause_exists_in_the_gate and test_negated_hits_are_not_disqualifiers.

The related follow-up concern (#1168, replacing presence-scanning with target parsing) is already merged as 212c818 and is contained in this head via the master merge.

State: MERGEABLE/CLEAN, both CI jobs green. This PR is the only thing outstanding for the competition task-type rant, which stays in_progress until it merges.

Disclosure: self-review; I did not push this head this cycle.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-080842

Verified on this PR's own tree (head 04474d64): tests/test_competition_prompt.py tests/test_scheduler.py122 passed.

Both points raised by the earlier are present on this head and pinned by tests, re-checked this cycle on the head's files rather than taken from the previous review:

  1. The bare adjective is in the English signal list. competition_prompt.md:152 lists offline and off-line alongside the compounds, with the rationale at 154-158 (the natural phrasing is "the final round will be held offline", which contains no compound). test_competition_prompt.py:182-213 enumerates the English list from the prompt itself and asserts the bare forms are in it — i.e. the omission that caused the would now fail a test, not just a review.
  2. The negation clause is sentence-scoped, not whole-line. §3.2.1 (competition_prompt.md:168-175) scopes negation to the sentence around the hit, and line 190 resolves the both-signals case as a real ambiguity → treat as offline. The previous failure mode (a body that merely mentions a negation being read as one) is closed here too.

One sequencing note for whoever merges next (measured this cycle, not a defect in this PR): #1166 and #1167 both write the same Agent.md count value (1562), so scripts/check-merge-sequence.py 1166 1167 reports #1167: DANGER - clean merge, but the tree FAILS: documents 1562 but 1584 are collected. Merging this PR does not break anything by itself; the pair needs a re-measurement between the two merges. That is the count-line issue #1158, not this PR's fault — but it is why I did not merge this landing this cycle.

EMRG Evolution added 2 commits September 13, 2026 08:31
master and this branch both documented 1562, so git merged the count line
cleanly and produced a tree that collects 1584 - the silent half of issue
#1158: equal values do not conflict, so nothing stops the merge and the
guard fails only after it lands.
@argszero

Copy link
Copy Markdown
Owner Author

Landmine defused — please do not merge this head; it has been replaced.

This PR was, at the moment I checked, the most dangerous item in the queue, and it became so because of the merge that landed minutes earlier:

$ scripts/check-merge-sequence.py 1166      # against master 633a777
  #1166: DANGER - clean merge, but the tree FAILS: documents 1562 but 1584 are collected

github said MERGEABLE/CLEAN; the head had 3/3 valid votes and both CI jobs green. All three of the queue's normal merge signals said "go". Merging it would have landed a tree that fails the repo's own doc-count guard — because #1167 had just been merged with the same documented value 1562, so the two branches' count lines were identical and git merged them with no conflict at all. There was no conflict marker for a reviewer to see:

$ git merge master            # no conflict reported
$ scripts/check-doc-count.py  # on the merged tree
  FAIL: Agent.md documents 1562 Python tests but 1584 are collected

This is the silent half of issue #1158: when two branches write different counts they conflict and a human re-measures (safe); when they write the same count git merges cleanly and the tree is short by the other PR's tests. The reassuring signal is the inverted one.

What I pushed (04474d60244b77): master merged in, check-doc-count.py --write to re-measure on the merged tree (1562 -> 1584), guard green, tests/test_competition_prompt.py tests/test_scheduler.py tests/test_doc_counts.py188 passed.

Vote disclosure: the three ✅ described 04474d6 and are void — the new head starts at 0/3. The PR's content is unchanged and still good; only the derived count moved.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-084752

Verified on head 0244b77b: full suite 1582 passed + 2 skipped = 1584 collected == Agent.md. The registration is covered, not merely present — deleting the competition entry from TASK_TEMPLATES turns 2 tests red (test_task_templates_cover_all_handlers, test_load_and_start_competition_task), so the task type cannot be silently unregistered while every test stays green.

The prompt guards are presence assertions, and I will name that limit rather than block on it: asserting a phrase is in the file cannot catch a rule that is stated and then not implemented. For pinning prose that is inherent, and it does buy the thing the host asked for here — the online-only constraint, the "executable procedure rather than agent discretion" framing and the reject-by-default rule cannot be quietly relaxed by a later edit.

Also confirmed the new type does not disturb existing scheduling: the whole suite is green on the branch tree, and no existing template mapping changed.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-091152

Re-verified on head 0244b77b on its own worktree: OK: Agent.md documents 1584 collected Python tests, and tests/test_scheduler.py tests/test_competition_prompt.py are 122 passed. Head unchanged since the previous cycle's review.

The host's rant behind this (the first in_progress rant in the queue) is satisfied by what is in the diff: the online-only constraint is stated as a hard constraint with an executable entry procedure, the offline signal words are enumerated in both Chinese and English, the award-ceremony case is an explicit exception (without which nearly every prize competition would be excluded), and identity verification stops and asks the host rather than inventing a path. The registration is genuinely covered — deleting the competition template entry turns two tests red — so the task type cannot be silently unregistered.

EMRG Evolution added 2 commits September 13, 2026 09:49
…ignal

The gate's English list carried `on-site`, `in-person` and `off-line` but not their
space-separated spellings, and English hyphenation is optional — prose uses both. Measured
on head `0244b77b` against the list as it stood (13 signals), each of these matched 0 of 13
while its hyphenated spelling hit:

    Finalists will be evaluated on site.          (state: offline evaluation)
    Final judging takes place on site.
    Winners are required to present in person.
    Top teams present in person at the awards.

That is the bare-`offline` gap one spelling down — the gap the note above the list already
describes ("a list of compounds alone misses the most natural English wording of the very
thing this gate exists to catch"). The Chinese list has no equivalent hole because `线下` is
a single token with no hyphenation to vary; the English list is where spellings multiply.

The two failure directions are not symmetric, so the list errs toward over-inclusion: a
spurious hit costs one competition entry, while a miss enters a competition with an offline
component — the host's hard constraint broken silently, since §3.2 makes a single hit
disqualifying and §4 files a rejection as never re-evaluated.

- competition_prompt.md: the three space-separated spellings added beside their hyphenated
  twins, and the note now states the rule (hyphenation is optional, so every hyphenated
  entry is listed in both spellings) with the measurement that found the gap.
- tests: a rule over the parsed list — every hyphenated signal must have its
  space-separated twin — rather than three presence checks, so a *future* hyphenated entry
  added without its twin fails there. Two measured sentences added to the verdict table.
- Agent.md count 1584 -> 1585 (the tree collects 1585; measured with the guard, not guessed).

Mutation-verified: removing the three twins turns 2 tests red (the rule and the verdict
table); restoring them leaves the file 22/22 green. Full suite 1584 passed + 1 skipped =
1585 == Agent.md. This push voids the two prior LGTMs by design.
… measurement

master moved to 5f0ee34 (#1142), which rewrote the same Agent.md count line (1566) that this
branch had rewritten (1585), so git reported a conflict on that one line. Resolved by
measurement, not by choosing a side: `check-doc-count.py --resolve-conflict` stripped the
block and wrote the count collected on the merged tree, 1589 (= 1566 master + 22 competition
tests + 1 test added by the hyphenation fix). Verified: 1588 passed + 1 skipped = 1589 ==
Agent.md, import and CLI green.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Needs fix — cycle cyc20260913-094149 (fix pushed as 301db71; this push voids all prior LGTMs by design)

The gate is the right shape and the head I reviewed (0244b77b) had one hole of the same class this PR already fixed once. English hyphenation is optional, and the list carried only the hyphenated spelling of three entries.

§3.2's English list read offlineoff-lineon-siteonsiteoffline roundin-personphysical attendancemust attendfinal presentationpitch eventdemo dayvenuetravel. Measured against that list (13 signals), each of these matched 0 of 13:

Finalists will be evaluated on site.
Final judging takes place on site.
Winners are required to present in person.
Top teams present in person at the awards.

while their hyphenated spellings hit. That is precisely the bare-offline gap — the one the note above the list already argues for ("a list of compounds alone misses the most natural English wording of the very thing this gate exists to catch") — just one spelling down. The Chinese list has no equivalent hole: 线下 is a single token, so there is no hyphenation to vary. It is the English list where spellings multiply, and the note's own reasoning applies to it uniformly (the list even carries the rare off-line beside offline, which shows the intent).

Severity is asymmetric, which is why I fixed it rather than filed it: §3.2 makes a single hit disqualifying and §4 files a rejection as never re-evaluated, so a miss silently enters a competition with an offline evaluation component — the host's hard constraint broken with no failing signal — whereas a spurious hit only costs one entry.

What the fix does (301db71, on top of a master merge that re-measured the count to 1589):

  • Adds off line, on site, in person beside their hyphenated twins, and states the rule in the note (hyphenation is optional → every hyphenated entry is listed in both spellings) with this measurement attached.
  • Adds a rule over the parsed list — every hyphenated signal must have its space-separated twin — instead of three more presence checks, so a future hyphenated entry added without its twin fails there. Plus the two measured sentences in the verdict table.
  • Mutation-verified both ways: removing the three twins turns 2 tests red (the rule and the verdict table); restoring leaves the file 22/22 green.
  • This tree: full suite 1588 passed, 1 skipped = 1589 == Agent.md; check-merge-sequence.py 1166OK - documents 1589; import and CLI green.

The tree-root change to check-nonlocal-style resolution is unrelated; note the sibling PR #1142 landed this cycle (5f0ee34) with the same "resolve from the directory you are standing in and name it" repair, so the two agree.

Needs 3 fresh ✅ from different cycles, all after 301db71.

§3.2's note recorded that four space-separated phrasings "each matched 0 of
the 13 signals above" - written when the list held 13 entries, and left at 13
by the same commit that added three space twins, over a list that now prints
16 entries. The note's own subject is an incomplete enumeration, so the stale
number reads as the list being shorter than it is, and no test could see it:
every other test in the file parses the `- English:` line and never the prose
around it.

Fix: phrase the measurement as a measurement ("none of the signals the list
held then") so it stays true as the list grows, and pin the class with a rule
test - any count the prompt states for a signal list must equal the parsed
length. The rule fails loudly on the next addition or removal instead of
drifting, and it is a consistency rule, not a ban: stating the correct count
passes.

Mutation-verified (4 arms): fixed text 23 passed; the stale bolded "0 of the
13" reintroduced -> the rule fails (and the first version of the rule regex
missed exactly that bolded form, so the mutation is what fixed it); a correct
count stated -> green; a wrong plain count -> fails.

Measured on this head: full suite 1588 passed + 2 skipped = 1590 collected ==
Agent.md (re-measured with scripts/check-doc-count.py --write).

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-100222

This cycle pushed head f9dc28d, and I am reviewing my own push — disclosed. At the moment of the push #1166 carried zero valid LGTMs: the of cycle cyc20260913-094149 (01:53:15Z) voided the earlier ones and nothing has been cast since, so the push destroyed no existing vote, and the 3-vote clock needs three votes after this head either way. I will not push this branch again.

What it changes. §3.2's note recorded that four space-separated phrasings "each matched 0 of the 13 signals above" over a list that prints 16 entries — the count was written when the list held 13, and it survived the very commit that added the three space twins. The note's own subject is an incomplete enumeration, so a stale count in it reads as the list being shorter than it is; and no test could see it, because every other test in the file parses the - English: line and never the prose around it. The measurement is now phrased as a measurement ("none of the signals the list held then", true forever), plus a rule test: any count the prompt states for a signal list must equal the parsed length, so the next addition/removal fails loudly instead of drifting. It is a consistency rule, not a ban — stating the correct count passes.

Verified on this head, in a detached worktree at f9dc28d (rev-parse HEAD asserted, so the numbers describe this head, not the main checkout):

  • full suite 1588 passed, 2 skipped = 1590 collected == Agent.md 1590, re-measured with scripts/check-doc-count.py --write after the new test — the merged value was measured, never chosen
  • merged tree: scripts/check-merge-sequence.py 1166 against master 5f0ee34aOK - documents 1590
  • CI run 34732299463: test + test-windows both pass
  • from emrg.client.app import run_client and python -m emrg --help both fine

Mutation verification, four arms (this is the part I would have got wrong): fixed text → 23 passed; the stale bolded count reintroduced → the rule fails; a correct count stated → passes; a wrong plain count → fails. Worth recording: the first version of that regex required the digit to touch the word and therefore missed the bolded 13** form entirely — the mutation is what found it.

Re-verified the fix this PR's history rests on (the space twins): deleting on site from the prompt turns 2 tests red — test_hyphenated_signals_are_listed_in_both_spellings and test_signal_list_verdicts_on_sample_requirements — so that rule discriminates rather than merely describing the list.

Named limit, restated so it is not read as a guarantee: these guards pin the prompt's content. They cannot make an agent follow the procedure at runtime, so the host's "online only" constraint is enforced by an executable checklist, not by a mechanism.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-102231 (2/3)

Independently re-verified this cycle, on a fresh detached worktree at head f9dc28d (git rev-parse HEAD asserted, so these numbers describe this head and not the main checkout):

  • full suite 1588 passed, 2 skipped in 41s, and the tree's own guard agrees: OK: Agent.md documents 1590 collected Python tests (no --write needed — the branch is self-consistent)
  • CI run 34732299463: test + test-windows both pass; PR is MERGEABLE/CLEAN

One class of check nobody had run on this PR: does the new prompt actually reach a runtime? Registering a task type is only half the work — the other half is the file being present where the resolver looks, and being renderable by the daemon's own code path. Measured, not read:

  • _resolve_task_template resolves built-ins to Path(__file__).parent / TASK_TEMPLATES[type], i.e. emrg/server/competition_prompt.md — present on the head, and the packaging glob is emrg/**/*.md (a pattern, not a per-file list), so the new prompt ships by the same rule as the five that already live in that directory. There is no second place enumerating prompt filenames (grep for the sibling prompts across pyproject.toml, install.sh, packaging/, and the GUI finds only the TASK_TEMPLATES table itself), so no registration was missed.
  • The prompt is rendered by the daemon through Jinja2 with jinja2.Undefined, so an unknown placeholder would render empty rather than raise — a silent way to hand the agent a path like .../<blank>/competition_<project>_state.md. So I extracted every {{ … }} / {% if … %} name from all six prompts and diffed them against the context literal in _build_evolution_prompt: competition_prompt.md uses 10 names, all present in the context (instance_id, host_name, uptime, evolution_count, task.project, source_dir, evolution_cwd, timestamp, current_time_human, task.extra_prompt). No prompt in the tree has an unresolved name.

The content guards I verified last cycle still hold on this head (the space-twin rule fires when a twin is deleted; the stated-count rule fails on the stale bolded count and passes on a correct one).

Named limit, unchanged and stated so it is not read as a guarantee: these are content assertions on the prompt and structural assertions on the registration. They pin that the online-only gate cannot be silently relaxed and that the type is wired up; they cannot make an agent follow the procedure at runtime, and they do not test whether the gate's judgment is right on a real competition page.

This vote is cycle 2 of 3 — one more from a different cycle, after f9dc28d, and it can land.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260913-104452 (3/3)

Independently re-verified on the PR's own tree (fresh detached worktree, rev-parse HEAD asserted == f9dc28dc): full suite 1588 passed + 2 skipped = 1590 collected == Agent.md 1590, check-doc-count.py agrees in-tree, and check-merge-sequence.py 1166 against master 5f0ee34a reports #1166: OK - documents 1590. CI run 34732299463 double-green (test + test-windows).

This cycle's new angle: the wiring questions nobody has asked for this PR yet — does the new prompt reach a runtime, and can it fail silently?

  1. Single source of truth, checked rather than assumed. TASK_TEMPLATES is the only enumeration of built-in task types: emrg/server/scheduler.py consumes it for resolution (line 114), for add-task validation (1577), for the GUI's template list (1712) and for the builtin-immutability checks (1739/1751/1766). A repo-wide search finds no second list outside emrg/server/ (the only other mentions of prompt filenames are tests), and packaging ships them by glob (include = ["emrg/**/*.md", ...], pyproject.toml:24) rather than by filename — so there is no place the new type could have been forgotten but is not.
  2. The silent-failure side of the same question. The daemon renders prompts with jinja2.Environment(undefined=jinja2.Undefined), so a name that is not in the builder's context renders as an empty string instead of raising — a typo silently hands the agent a blank path. Measured, not assumed: I extracted every {{ }} / {% if|for %} reference from all six prompts and reconciled the roots against the 17-key context literal in _build_evolution_prompt0 unresolved (competition_prompt.md uses 9). Rendering each template with that same undefined-strict env yields 0 leftover tags (competition_prompt.md: 17901 chars).
  3. A limit of my own check, recorded. My reconciliation compares only the root identifier, so a dotted access like {{ task.project }} / {{ task.extra_prompt }} (competition_prompt.md uses 9 such references) is only proven to have a known root; the field itself is optional by design (it comes from the host's tasks.yml, and {% if task.extra_prompt %} is exactly how an unset optional field is meant to behave). So this pins "no mistyped root", not "every field is set".

Named limit, unchanged: these guards pin that the gate cannot be silently relaxed, that the type is registered and that the template renders. They cannot make an agent follow the procedure, nor judge a real competition page.

Counting: 2 valid ✅ before this one (cyc20260913-100222, cyc20260913-102231, both after head f9dc28d), so this is the 3rd consecutive vote with no ❌ in between → mergeable under the three-vote rule.

@argszero
argszero merged commit 6456a98 into master Sep 13, 2026
2 checks passed
@argszero
argszero deleted the feature/competition-task-type branch September 13, 2026 02:46
argszero pushed a commit that referenced this pull request Sep 13, 2026
The branch was CONFLICTING on Agent.md's count line only, which means GitHub ran
no pull_request CI for it at all. Master's line moved to 1590 when the competition
task type landed, so the conflict is resolved the only way this repo allows: by
measuring the merged tree (`check-doc-count.py --resolve-conflict`), not by picking
a side. The count now reads 1592, which is what the merged tree actually collects
(1590 passed + 2 skipped).
argszero added a commit that referenced this pull request Sep 13, 2026
With --all passed explicitly and nothing unmerged (a merge that resolved
cleanly) the tool printed 'error: no paths given (pass files, or --all for
every unmerged path)' and exited 2: it asked for the flag the caller had
just passed, and reported the good outcome as a malformed invocation.

Measured in cyc20260913-082711 at the moment this mattered most - a merge
that resolved CLEANLY and produced a tree that fails the doc-count guard
(issue #1158, PR #1166). There is no conflict marker to notice, so the
tool's silence had to be explained rather than reported as an error.

--all answered with an empty list is now rc 0 with a message that says the
merge is clean or already resolved, warns that a clean merge is not
evidence of a healthy tree, and points at check-merge-sequence.py - the
tool that measures that, and the one that caught this case. The bare
no-argument invocation stays a usage error (rc 2), and the named-file
'no conflict blocks' case is unchanged at rc 2 (documented and pinned).

Tests pin both directions plus a rot guard: the script the message points
at must exist, so renaming it fails the suite instead of sending a reader
after a file that is not there. Mutation-checked: restoring the old
behaviour fails 1 test, dropping the usage error fails 2, removing the
pointer fails 1.

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
argszero added a commit that referenced this pull request Sep 13, 2026
…fail-open mutant (#1172)

* emrg: drive check-merge-sequence's guard verdict for real, killing a fail-open mutant

The suite for scripts/check-merge-sequence.py replaced _guard_verdict in every
test, so the mapping from the guard's exit code to a verdict was uncovered.
Measured: replacing that function with a body that returns (True, "guard OK")
without consulting the guard at all kept all five tests green. That mutant is
fail-open - the tool would print OK for every plan, including the dangerous one
it exists to catch - and fail-open is the defect class this family of gates
exists to prevent.

Three integration tests now drive the real function on a real git tree whose own
copy of check-doc-count.py actually runs (a two-line tree gives a real verdict in
a fraction of a second: the guard collects through sys.executable -m pytest, so
no uv, no network, no dependency on this project's suite):

* a self-consistent tree is accepted (the OK direction);
* a stale count is rejected and the numbers named (the DANGER direction);
* a tree without the guard is a measurement error, never a pass.

Mutation-verified: the never-consults-the-guard mutant now fails 3 tests (was 5
green), and reading the guard's rc==1 as a pass fails the DANGER-direction test.
The OK-direction test is the one that kills a hypothetical always-failing
mutant, so each direction is pinned separately.

* emrg: refresh the base ref, and qualify it, so the plan check measures the live master

Every PR head in check-merge-sequence.py was fetched from the network, so the
tool always answered about the PRs as they are now. The base was not: it was read
straight from the local ref. Measured on this repo with origin/master left two
commits behind:

    base 02e43c8 (origin/master)      <- 02e43c8 is not master; 3dbc2f1 is

and the mislabelled base changes the verdict. Over 25 plans (13 singles + 12
adjacent pairs), 16 differed between a stale and a fresh base. The plan below
reads as two DANGER steps against the stale base and as a conflict - safe, no
tree produced - against the live one:

    plan #1167 -> #1166:  stale base -> 2 DANGER;  live base -> CONFLICT

A gate that answers about the wrong tree is the failure this file already
documents for __file__-relative tools; the base is the same trap in the time
dimension, and it is the more dangerous half because a stale base can also cry
wolf while the PR heads beside it are current.

_refresh_base now fetches origin/<branch> before it is resolved, and a failed
fetch is exit 2 rather than a quiet fall back to the stale commit. A SHA and a
local branch are never fetched: a SHA is immutable and treating a local branch as
remote would overwrite the caller's own ref.

The destination must be written fully qualified. The first version of this fix
used the bare name and git resolved the ambiguity by creating a local branch
refs/heads/origin/master, which shadows the remote-tracking ref and makes every
later origin/master ambiguous - caught by git's own warning, then removed.

Mutation-verified, four killed: main no longer calling _refresh_base (survived the
three helper tests, so a test pinning the call site was added); the refspec not
forced; the remote-only guard removed; the destination unqualified.

* emrg: resolve the base by its full name, so a stray local branch cannot shadow it

* emrg: pin UTF-8 decoding on the git calls this test adds, which the widened decode guard rejects

---------

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants