unic / unic/unic-agents-plugins
bug(unic-archon-dlc): open-pr precedes its gate, and a config-value gate has no safe middle
@orioltf is already working on this.
Since Sep 17, 2026.
- Dominant language
- JavaScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 16h 43m
- Merged PRs (30d)
- 19
Description
Amended 2026-09-04 after a grilling with the maintainer. The criteria below replace the
original body's two questions. Class ruling, under #456
§ Notes: this is a second variable — it changes what/buildproduces and what/tickets
creates, and neither effect separates from the mechanism checks run 3 measures. So #439 has left
#457's blockers and belongs to run 4's change set. The maintainer's words: "ok, espera a run 3".
What this is
Two claims were filed here. Both were measured during the grilling and neither survived as written.
The first claim, that open-pr running before its gate is what let run 2 ship a pull request. In
run 2 gates.build was afk, so build-pr-gate was skipped by its own when condition wherever it
sat in the DAG. Reversing the two nodes would have changed nothing about run 2. The ordering and the
run-2 outcome are unrelated.
The second claim, that a reject at /pr-review's review-gate destroys every finding. It does
not. synthesize writes the findings and the summary sections, and reconcile writes the composed
summary and the thread plan, all four to the Session dir and all four before the gate pauses. The
findings are on disk when a human rejects. What destroys them is archon complete <branch> --force
deleting the worktree, which is a cleanup defect and not a gate defect.
What is left, and it is the real defect: a build that fails its own acceptance criteria opens a
pull request. evidence withholds evidence.json on a red verdict, the engine refuses terminal
completed, and the run ends failed — with a complete, mergeable pull request already open. That
is ADR-0034
§ Decision item 4's deferred trade, reopened: "If that cost is judged too high, gate open-pr on the
verdict; that is a separate decision with its own blast radius."
And a second defect fell out of the measurement. The one artefact the engine demands is the only
one that carries no information. evidence_policy checks presence, never content; and the file,
when it exists, is { slug, verification_failures: [], goals_check_failures: [] } with both arrays
empty by construction, because the node returns before writing whenever either is non-empty. It can
never say what failed.
The decision
evidence writes always and reports a verdict. A red verdict stops the run before any pull request
exists, and the report reaches a human through a grouping ticket that /tickets now guarantees.
Acceptance criteria
-
evidencewritesevidence.jsonon every verdict, and the file carries the verdict. The node
no longer returns early on red. The artefact names which verdict it recorded and lists the failures
that produced it, so a reader learns what failed from the file the engine demanded. -
evidenceprints its verdict as JSON on stdout, and a downstreamwhenreads it. This is not
a style choice. Ascript:node'soutput_formatis ignored — Archon 0.7.0 returns the node's
raw stdout, and a field reference against a non-JSON stdout fails the run before the referencing
node executes, withOutputRefError. Measured on 2026-09-04 in a throwaway repository. So the
script prints a JSON object or nothing downstream can branch on the verdict. -
The verdict is a value a
whenexpression can compare, andunresolvedis not green. Archon
0.7.0'swhenparser accepts only$nodeId.output <op> 'VALUE'and&&/||compounds; a bare
literal such astrueis unparseable and fails closed, skipping the node with a warning.
Measured in the same probe. #430 lands
first and gives the verdict its third state, so the comparison this ticket writes treats
unresolvedexactly as it treats red. -
A red verdict opens no pull request, and the run ends
cancelled. Acancel:node fires on
the red verdict. Measured: acancel:node sets terminal statuscancelled, which is distinct
fromfailed, is recorded in the run row, and exits the CLI non-zero; the DAG stops between layers
so nothing downstream runs, whatever trigger rule those nodes carry. -
The node that opens the pull request carries
trigger_rule: all_done. Without it a green
build opens no pull request at all: the skippedcancel:node does not count as success under the
default rule, so its dependent is skipped too. Measured both ways in the probe — skipped without
the rule, executed with it. This is the criterion most easily satisfied wrongly, because the red
path looks correct while the green path silently produces nothing. -
Committing and pushing is its own node, and it runs on both verdicts. Today the node that
opens the pull request also stages, commits and pushes, so a red run that never reaches it commits
nothing and loses the whole build. Splitting them puts the durable output — the implementation
commits and the session artefacts — on the remote before the branch can be judged, on either
verdict. Opening the pull request stays a separate act, reached only on green. -
On a red verdict, the grouping ticket receives a pointer, not the report. A comment names the
branch and the path of the report inside it. It does not carry the report body: a report is
30 712 characters in the only run that has produced one, and the comment limits across hosts run
from roughly 32 000 upward, so a larger run would not fit. The pointer is appended, never edited in
place, so a slug built three times leaves three attempts on record. Amended 2026-09-04 21:40 by the
orchestrator, four-reads pass: the node that appends the pointer is its own node, it depends on the
commit-and-push node of criterion 6 (the pointer names a branch that must already be on the remote), and
thecancel:node of criterion 4 depends on it. Criterion 4 says thecancel:node stops the DAG
between layers so nothing downstream runs; a pointer node placed downstream of, or beside,cancel:never
runs, and the red path then satisfies criteria 4 to 6 while criterion 7 fails silently. Same shape as the
gate #441 shipped that could not fire. -
On a green verdict nothing is written to the grouping ticket, and the body says so. The pull
request already links the report and the slices, so a second copy would be a surface to keep in
sync. A reader of this ticket should not have to guess whether the omission was deliberate. -
/ticketscreates one grouping ticket per run and records it in the baton. It carries the
type rolefeatureand no state role. The tracker contract's own reasoning is why: every state
role writes something, and none of them is true of a ticket nobody is meant to pick up; leaving all
of them unwritten lands the ticket in whatever the host's creation default is, which is where a
grouping ticket belongs. -
The plugin names the grouping relationship and never its mechanism.
/ticketsasks the
tracker contract how this repository expresses a relationship between a group and its members,
and uses what it names — parenthood where the host has it, any other relation where it does not.
No Box asks the tracker about hierarchy at run time:/buildreads the grouping ticket's id from
the baton. -
/ticketsstops when the contract names no grouping mechanism. Same shape as its existing
contract check in Step 1: print a message naming what is missing, and stop. A grouping ticket that
groups nothing is worse than none —/buildwould find it in the baton, publish the report there,
and no one reading a slice would ever reach it. -
A re-entering
/ticketsreuses the grouping ticket the baton names, and creates a second one
for the same slug never./ticketsalso accepts the grouping ticket as an optional argument — a
number or a link. Given one it uses that ticket and creates nothing; given none it creates one. The
argument exists because the work often already lives under a ticket a human made: this repository's
own effort is four stories under one Azure DevOps feature, made by hand before any Box ran. -
The baton becomes an object, and every surface that asserts its shape changes with it. The
grouping ticket's id belongs to the run, not to a slice, so the file gains a top-level field
beside the slice array. Nine sentences across six files assert the current shape — the build Box's
header, its loop comment, its loop constants block, and the two loop instructions that call it an
ordered array and walk it in order;/tickets' own overview, its write step and its summary line;
and the build Box's operator doc.CONTEXT.md,README.mdand/improve-architecturename the
file without asserting its shape and are checked rather than assumed. A reader can confirm no
surface still calls it an array. -
The three sentences that describe the old mechanism are rewritten, and they are named by what
they claim rather than by where they sit. Bothverification's andgoals-check's prompts tell
their reader whatevidencedoes with their output and whatevidence_policythen gates on; the
evidencenode's own comment block says the same. All three go false with this change. #430 lands
first and rewrites the first half of that sentence for the third state, so write these against the
tree #430 leaves, not against today's. Amended 2026-09-04 21:40 by the orchestrator, four-reads pass:
a fourth claim goes false with criterion 6 and is named by what it says: thatbuild-state.jsonis
committed once, at the node that opens the pull request, and never in a loop iteration. The plugin
AGENTS.md§ Plugin doctrines staging bullet anddocs/boxes/unic-dlc-build.md'sopen-prstep both
say it. After criterion 6 the once-and-never-in-a-loop half stays true and the node moves; both surfaces
change in the same PR. They change differently, and that is the point.AGENTS.mdkeeps the rule
and its reason and stops naming the node: the rule survives any node rename, the node name does
not, which is why criterion 6 falsified it at all. The Box operator doc describes nodes for a reader
running them, so it names the new one. -
The
evidencenode's comment block keeps its warning about dollar-prefixed tokens. The DAG
loader reads every$node.outputtoken in that script body, comments included, as a real edge, so
a placeholder node id written into a comment makes the whole Box fail to load. Nothing guards this.
Rewriting the block is where it would be lost. -
The tracker contract template
/setupwrites gains the grouping section, and the delivery is
named as ending inside each Consumer. The contract is tenant-owned:/setupwrites it on the
first run and thereafter only reports. So this change is inert until a human writes the section
into an existing Consumer's contract by hand. ForDXP-DesignSystemthe maintainer does that
himself. A criterion that ended at "the template gains a section" would be satisfied by a change
no run ever sees. -
An ADR records the decision. It carries the always-write rule, the verdict as the branch
signal,evidence_policy's demotion to a backstop, the grouping ticket as a named need with a
tenant-owned mechanism, and the three Archon behaviours criteria 2 to 5 depend on — with the date
of the probe that measured them, because nothing in this repository can re-derive them. -
/qacloses the grouping ticket when it merges the pull request, and reads the baton to find
it./qareads no baton today — only/builddoes — so it gains that read inbootstrap, where
/buildalready has it, rather than inmerge, so the irreversible node never discovers a missing
value halfway through. Merging is where a run ends, and a grouping ticket that outlives every run it
grouped is a ticket nobody closes. -
CONTEXT.mdgains an entry for Baton. The word appears 26 times across 15 files — all four Box
YAMLs, four ADRs, three Box operator docs,AGENTS.md,README.mdand two commands — and
CONTEXT.mddefines it nowhere. It is the plugin's most-used structural term and its most-used
undefined one. The entry says what a baton is (the artefact one stage writes to disk for the next to
read, so nothing travels in session memory) and names the four that exist. This criterion sits here
rather than in the vocabulary spin-out because this ticket changes what the/buildbaton is.
Its_Avoid_line names handoff contract and handoff artefact:AGENTS.mdalready calls the
/explore→/specsbaton "the tightened handoff contract", which is the same relationship under a
second name in a file that usesbatonelsewhere. The entry must also say that/handoffand
HANDOFF.mdare not this term and keep their names — the first is one of Matt's skills and the
second a retired file, and a sweep that reads the_Avoid_line without this sentence renames a
third party's skill.
Two decisions that are not criteria
Both are satisfied by changing nothing, so writing them as criteria would buy coverage and cost an
audit round to discover.
- The order of
open-prandbuild-pr-gatedoes not change. The title of this ticket says it
should, and the measurement says otherwise: once a red verdict opens no pull request, the gate is no
longer authorising an effect that should not have happened — it is reviewing work that met its
criteria. Recorded as a decision so the next reader does not reopen it from the title. evidence_policy: { required: true }stays. With always-write it can no longer refuse a red
run; it now covers only the case thecancel:node cannot see, which isevidenceitself failing
before it writes. Say that in the ADR, or the next reader finds a config key that never fires and
deletes it.
What is still decided by a config value, after this
gates.build still decides whether a human sees a green pull request before the run ends. That is
intended and is not what this ticket set out to fix: on green the pull request is legitimate, so
skipping the pause is a tenant's choice rather than a hole.
What this did not decide
Amended 2026-09-04 22:45. Four of the five original items were ruled by the maintainer in a second
grilling pass and have moved into the criteria above or into a spin-out. What is left is one item, and
one that only a run can answer.
- Whether
/qa's grouping-ticket close should itself be gated. Criterion 18 closes the ticket at
merge, unconditionally, inside a node that already runs behindgates.qa. Whether closing deserves
its own pause is untested and nobody has wanted one yet. - What a Consumer whose tracker expresses no relationship at all should do. Criterion 11 stops
/ticketswhen the contract names no mechanism, which is correct and is also untested: no such
tracker has been met. The first one that turns up decides whether stopping is right or whether an
unrelated grouping ticket beats none.
Ruled since the first pass, recorded so nobody re-opens them as gaps:
- Who closes the grouping ticket → criterion 18,
/qaat merge. - Whether
/specsshould know the grouping ticket exists → no; instead/ticketstakes it as an
optional argument, criterion 12. cleanup.prune_slug_dirs's default, and whether that pruning wants its ownhitl/afkmode and
gate → #459.- What word names the thing a tracker holds →
ticket, ruled by the maintainer.
#460 carries the sweep and the argument
CONTEXT.mdneeds, since its Issues JSON entry liststicketsunder_Avoid_today. The reason
to record:work itemis Azure DevOps's word,issueis GitHub's and Jira's, andticketis the one
no host in front of us claims. Renaming the baton file rides with it, 74 occurrences in 16 files. - What
/qadoes with anunresolvedverdict → #430's,
unchanged.
Related
- #430 — lands first. Its criteria 8 and
9 are reworded around the verdict; this ticket then changes what reads the verdict. Write these
criteria against the tree it leaves. Two shared edit sites, both in the same direction. Its
criteria 8 and 9 rewrite whatevidencereports, which criteria 1 to 3 here then change again; and
its criterion 3 rewrites/qa'sbootstrapoutput schema, which criterion 18 here extends with the
baton read. #430's version of each node is the one this ticket builds on. - #437 — the
/specsgate approves an
artefact nobody reads. A command gate, not an Archonapproval:node. Sibling, not part. - #458 —
/qa's three unrecorded node
outputs. Outside run 3 and not absorbed here. - #440 — owns the re-entry idiom. Same rule.
- Two spin-outs approved in the grilling, not yet filed. One takes
/pr-review's missing
on_rejectand the unresolved Session paths in its gate message, plus theprune_slug_dirs
question. The other takes the vocabulary ruling in What this did not decide item 3, including the
maintainer's own reason: "utilizoticketporque WorkItem es propio de ADO y en este repositorio no
tenemos en cuenta la tecnología que utilizan los consumidores."
ADRs this revises
Plugin-local, in apps/claude-code/unic-archon-dlc/docs/adr/, revised inline by slug:
0034-evidence-gate-deterministic-writer— § Decision item 4 in full.0023-build-generic-red-green-refactor-loop— the sentence saying the pull request is opened anyway
inafk.0022-tickets-slice-to-build— the grouping ticket, and the baton becoming an object.0016-dlc-thin-process-layer— no change of substance; it is where the rule this ticket applies
already lives, that the plugin names the need and the Consumer the mechanism.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.