unic / unic/unic-agents-plugins

bug(unic-archon-dlc): open-pr precedes its gate, and a config-value gate has no safe middle

Open
#439 6 comments 0 reactions 1 assignee View on GitHub

@orioltf is already working on this.

Since Sep 17, 2026.

app:unic-archon-dlc bug p1 ready-for-agent wayfinder:grilling
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 /build produces 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

  1. evidence writes evidence.json on 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.

  2. evidence prints its verdict as JSON on stdout, and a downstream when reads it. This is not
    a style choice. A script: node's output_format is 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, with OutputRefError. Measured on 2026-09-04 in a throwaway repository. So the
    script prints a JSON object or nothing downstream can branch on the verdict.

  3. The verdict is a value a when expression can compare, and unresolved is not green. Archon
    0.7.0's when parser accepts only $nodeId.output <op> 'VALUE' and && / || compounds; a bare
    literal such as true is 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
    unresolved exactly as it treats red.

  4. A red verdict opens no pull request, and the run ends cancelled. A cancel: node fires on
    the red verdict. Measured: a cancel: node sets terminal status cancelled, which is distinct
    from failed, 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.

  5. The node that opens the pull request carries trigger_rule: all_done. Without it a green
    build opens no pull request at all: the skipped cancel: 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.

  6. 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.

  7. 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
    the cancel: node of criterion 4 depends on it. Criterion 4 says the cancel: 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.

  8. 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.

  9. /tickets creates one grouping ticket per run and records it in the baton. It carries the
    type role feature and 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.

  10. The plugin names the grouping relationship and never its mechanism. /tickets asks 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: /build reads the grouping ticket's id from
    the baton.

  11. /tickets stops 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 — /build would find it in the baton, publish the report there,
    and no one reading a slice would ever reach it.

  12. A re-entering /tickets reuses the grouping ticket the baton names, and creates a second one
    for the same slug never. /tickets also 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.

  13. 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.md and /improve-architecture name the
    file without asserting its shape and are checked rather than assumed. A reader can confirm no
    surface still calls it an array.

  14. The three sentences that describe the old mechanism are rewritten, and they are named by what
    they claim rather than by where they sit.
    Both verification's and goals-check's prompts tell
    their reader what evidence does with their output and what evidence_policy then gates on; the
    evidence node'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: that build-state.json is
    committed once, at the node that opens the pull request, and never in a loop iteration. The plugin
    AGENTS.md § Plugin doctrines staging bullet and docs/boxes/unic-dlc-build.md's open-pr step 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.md keeps 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.

  15. The evidence node's comment block keeps its warning about dollar-prefixed tokens. The DAG
    loader reads every $node.output token 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.

  16. The tracker contract template /setup writes gains the grouping section, and the delivery is
    named as ending inside each Consumer.
    The contract is tenant-owned: /setup writes 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. For DXP-DesignSystem the maintainer does that
    himself. A criterion that ended at "the template gains a section" would be satisfied by a change
    no run ever sees.

  17. 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.

  18. /qa closes the grouping ticket when it merges the pull request, and reads the baton to find
    it.
    /qa reads no baton today — only /build does — so it gains that read in bootstrap, where
    /build already has it, rather than in merge, 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.

  19. CONTEXT.md gains 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.md and two commands — and
    CONTEXT.md defines 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 /build baton is.
    Its _Avoid_ line names handoff contract and handoff artefact: AGENTS.md already calls the
    /explore/specs baton "the tightened handoff contract", which is the same relationship under a
    second name in a file that uses baton elsewhere. The entry must also say that /handoff and
    HANDOFF.md are 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-pr and build-pr-gate does 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 the cancel: node cannot see, which is evidence itself 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.

  1. Whether /qa's grouping-ticket close should itself be gated. Criterion 18 closes the ticket at
    merge, unconditionally, inside a node that already runs behind gates.qa. Whether closing deserves
    its own pause is untested and nobody has wanted one yet.
  2. What a Consumer whose tracker expresses no relationship at all should do. Criterion 11 stops
    /tickets when 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, /qa at merge.
  • Whether /specs should know the grouping ticket exists → no; instead /tickets takes it as an
    optional argument, criterion 12.
  • cleanup.prune_slug_dirs's default, and whether that pruning wants its own hitl/afk mode and
    gate → #459.
  • What word names the thing a tracker holdsticket, ruled by the maintainer.
    #460 carries the sweep and the argument
    CONTEXT.md needs, since its Issues JSON entry lists tickets under _Avoid_ today. The reason
    to record: work item is Azure DevOps's word, issue is GitHub's and Jira's, and ticket is the one
    no host in front of us claims. Renaming the baton file rides with it, 74 occurrences in 16 files.
  • What /qa does with an unresolved verdict#430's,
    unchanged.

Related

  • #430lands 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 what evidence reports, which criteria 1 to 3 here then change again; and
    its criterion 3 rewrites /qa's bootstrap output 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 /specs gate approves an
    artefact nobody reads. A command gate, not an Archon approval: 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_reject and the unresolved Session paths in its gate message, plus the prune_slug_dirs
    question. The other takes the vocabulary ruling in What this did not decide item 3, including the
    maintainer's own reason: "utilizo ticket porque 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
    in afk.
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.