From 26b84eca9d57b8525a99311cb61a0aad7df424a5 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 15:28:19 +0700 Subject: [PATCH 1/5] fix(queen): the Linux copy of the policy had drifted, and its gate ran nowhere Eleven policy files exist twice. `rings/SR-00` is compiled by the app; `agent-server/queen-core/Sources/QueenCore` is the copy the Docker build compiles for Linux, because the build context cannot reach up out of the agent-server directory. `make queen-core-sync` compares them byte for byte and its comment states the stakes: "a policy that differs between them is two arbiters of the same rule." IT APPEARED IN NO WORKFLOW. Measured 2026-09-06 across all thirteen files: twelve identical, one not. `QueenLocalisation.swift` gained a string-aware literal view in the ring on 2026-09-05 (#1176). The copy that ships to Linux was last touched 2026-08-29. The fix never crossed, and it is not a comment: the copy is missing `var inString` and the whole escape-and-quote branch, so on Linux the literal view is still the version the ring's own comment describes being burnt by - a `/*` inside a quoted branch glob (`"No empty queen/* branch ..."`, line 6864 of ChatViewModel.swift) opening a phantom block comment and blanking the file from there to the end. 6,726 lines of evidence invisible, two candidates tied at zero, and a range answered in the middle of a function the spec never named. `QueenLocalisation` is reached from `QueenEvidencePolicy` inside queen-core, so this is in the artifact Linux runs, not a stray file. TWO CHANGES. The copy is synced, and the gate now runs on ubuntu in `trios-logic.yml` - it is `cmp`, it costs seconds, and it does not need the compiler that consumes its result. The workflow's paths gain `trios/agent-server/queen-core/**` so a hand-edit of EITHER side wakes it; watching only the ring would leave the same hole facing the other way. Proven both directions before wiring: with the drift in place the gate prints `[FAIL] ... DIFFERS QueenLocalisation.swift` and exits 1; after the sync it prints `[OK] ... byte-identical`. Co-Authored-By: Claude Opus 5 --- .github/workflows/trios-logic.yml | 29 ++ .../Sources/QueenCore/QueenLocalisation.swift | 264 ++++++++++++------ 2 files changed, 212 insertions(+), 81 deletions(-) diff --git a/.github/workflows/trios-logic.yml b/.github/workflows/trios-logic.yml index f3d6b83a32..31d2213f72 100644 --- a/.github/workflows/trios-logic.yml +++ b/.github/workflows/trios-logic.yml @@ -10,12 +10,41 @@ on: paths: - 'trios/rings/**' - 'trios/tests/**' + # The Linux copy of the policy, so a change to EITHER side of the + # duplication wakes the gate below. Watching only the ring would let a + # hand-edit of the copy drift in unwatched, which is the same hole in the + # other direction. + - 'trios/agent-server/queen-core/**' - 'trios/agent-server/apps/server/src/agent/**' - 'trios/Makefile' - 'trios/tests/fixtures/**' - '.github/workflows/trios-logic.yml' jobs: + # ELEVEN POLICY FILES EXIST TWICE AND THE GATE FOR IT RAN NOWHERE. + # + # `make queen-core-sync` compares rings/SR-00 with the copy the Docker build + # compiles for Linux, byte for byte, and its own comment says why: "the copy + # is compiled by the Linux stage and the original by the app, and a policy + # that differs between them is two arbiters of the same rule." + # + # It appeared in no workflow. Measured 2026-09-06: QueenLocalisation.swift had + # already drifted - the ring gained a string-aware literal view on 2026-09-05 + # (#1176, a `/*` inside a quoted branch glob opened a phantom comment and + # blanked 6,726 lines of evidence) and the copy that ships to Linux was last + # touched on 2026-08-29. The fix never crossed. Twelve of thirteen files were + # identical; one was not, and nothing was looking. + # + # Ubuntu and no Swift: this is `cmp`, and it costs seconds. The comparison + # does not need the compiler that consumes its result. + queen-core-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: The Linux copy of the policy matches the ring, byte for byte + working-directory: trios + run: make queen-core-sync + server-units: runs-on: ubuntu-latest steps: diff --git a/trios/agent-server/queen-core/Sources/QueenCore/QueenLocalisation.swift b/trios/agent-server/queen-core/Sources/QueenCore/QueenLocalisation.swift index 5a49a4a052..a90962c3a2 100644 --- a/trios/agent-server/queen-core/Sources/QueenCore/QueenLocalisation.swift +++ b/trios/agent-server/queen-core/Sources/QueenCore/QueenLocalisation.swift @@ -30,6 +30,16 @@ import Foundation /// No rule answers → `nil`. A confidently wrong range is worse than no range: /// it sends the bee to read the wrong place with authority (#1175). /// +/// ## The finishing check (#1176) +/// +/// Whichever rule answers, the answer is checked before it leaves: the first +/// line of the returned range must declare a function — and for the name rule, +/// exactly the name it matched. The measurement that forced this: #1158, whose +/// spec names `autoAcceptIfUnambiguous`, was answered with a range beginning +/// in the middle of `handleWorkerFinished` — a function that spec never +/// named — because a window capped around a hit deep inside a wide body +/// starts nowhere in particular. Beginning nowhere in particular is silence. +/// /// This is pure static plumbing: source in, range out, no state, no side effects. public enum QueenLocalisation { @@ -199,6 +209,15 @@ public enum QueenLocalisation { /// Newlines are preserved. Event names ("queen.review.verdicts") are /// evidence, not decoration (#1174) — this view exists so rule 2 can see /// them while every other rule keeps working on code only. + /// + /// String-aware, because the literal view once was not and the file + /// measured it (#1176, 2026-09-02): a branch glob quoted inside a string — + /// `"No empty queen/* branch …"`, line 6864 of ChatViewModel.swift — + /// opened a phantom block comment, and the literal view went blank from + /// there to the end of the file. 6 726 lines of evidence invisible: + /// corroboration saw no mentions past the glob, so #1158's two candidates + /// tied at zero and rule 4 answered with a mid-body window of the wrong + /// function. A comment opener inside a string is prose, not a comment. private static func maskComments(_ source: String) -> String { var output = [Character]() output.reserveCapacity(source.count) @@ -206,12 +225,26 @@ public enum QueenLocalisation { let chars = Array(source) var i = 0 var blockDepth = 0 + var inString = false while i < chars.count { let c = chars[i] let next: Character? = i + 1 < chars.count ? chars[i + 1] : nil - if blockDepth > 0 { + if inString { + if c == "\\", let escaped = next { + output.append(c) + output.append(escaped) + i += 2 + } else if c == "\"" { + inString = false + output.append(c) + i += 1 + } else { + output.append(c) + i += 1 + } + } else if blockDepth > 0 { if c == "/", next == "*" { blockDepth += 1 output.append(" "); output.append(" ") @@ -233,6 +266,10 @@ public enum QueenLocalisation { blockDepth = 1 output.append(" "); output.append(" ") i += 2 + } else if c == "\"" { + inString = true + output.append(c) + i += 1 } else { output.append(c) i += 1 @@ -386,6 +423,41 @@ public enum QueenLocalisation { return start...end } + // MARK: - The finishing check + + /// Caps a rule's answer to `maxRegionWidth` around `hitLine`, then applies + /// the #1176 self-check: the first line of the returned range must declare + /// a function — and the name rule, which passes `naming`, must land on + /// exactly the name it matched. Capping around a hit deep inside a wide + /// body slides the window off the declaration line, and a range that + /// begins mid-body begins "not at the named function" just as surely as + /// one that begins at the neighbour. Either failure is silence, not a + /// range. + /// + /// This is the one place an answer is either returned or silenced. Delete + /// the two guards and the замер goes red on the `queen.review.verdicts` + /// case — that flip is the proof the check carries weight (#1176, + /// criterion 4). + private static func finished( + _ extent: ClosedRange, + around hitLine: Int, + in codeLines: [String], + naming requiredName: String? = nil + ) -> ClosedRange? { + let capped = capToWidth(extent, around: hitLine) + + // Self-check: the range must begin at a line that declares a function + // — the very name the rule matched, when it matched one. + guard let startName = declarationName(on: codeLines[capped.lowerBound]) else { + return nil + } + if let requiredName, startName != requiredName { + return nil + } + + return (capped.lowerBound + 1)...(capped.upperBound + 1) + } + // MARK: - Rule 1: declaration name /// Returns the 1-indexed range of the declaration whose name matches one @@ -432,7 +504,9 @@ public enum QueenLocalisation { } guard !tie else { return nil } } - return finished(chosen, lines: lines) + return finished( + chosen.extent, around: chosen.idx, in: lines, naming: chosen.name + ) } /// The 0-based extent of the declaration starting at `declLine`: @@ -467,21 +541,6 @@ public enum QueenLocalisation { return idx...end } - /// Caps a name-match candidate and applies the #1176 self-check: the - /// first line of the returned range must still declare the matched name. - /// If capping or any other step shifted the start, the range points at - /// the wrong function — silence it rather than mislead. - private static func finished( - _ candidate: (idx: Int, name: String, extent: ClosedRange), - lines: [String] - ) -> ClosedRange? { - let capped = capToWidth(candidate.extent, around: candidate.idx) - guard declarationName(on: lines[capped.lowerBound]) == candidate.name else { - return nil - } - return (capped.lowerBound + 1)...(capped.upperBound + 1) - } - // MARK: - Rule 2: dotted name in a string literal /// A dotted identifier (`queen.review.verdicts`) found verbatim inside a @@ -509,8 +568,7 @@ public enum QueenLocalisation { guard let enclosing = enclosingDeclaration( hitLine: idx, depths: depths, lines: codeLines ) else { continue } - let capped = capToWidth(enclosing, around: idx) - return (capped.lowerBound + 1)...(capped.upperBound + 1) + return finished(enclosing, around: idx, in: codeLines) } } return nil @@ -580,8 +638,11 @@ public enum QueenLocalisation { } let targets = Set(clean.keys) guard targets.count == 1, let idx = targets.first else { return nil } - let capped = capToWidth(declarationExtent(declLine: idx, depths: depths), around: idx) - return (capped.lowerBound + 1)...(capped.upperBound + 1) + return finished( + declarationExtent(declLine: idx, depths: depths), + around: idx, + in: lines + ) } // MARK: - Rule 4: identifier mentioned exactly once @@ -618,8 +679,7 @@ public enum QueenLocalisation { guard extents.count == 1, let first = anchored.first else { return nil } let hit = anchored.map(\.line).min() ?? first.line - let capped = capToWidth(first.extent, around: hit) - return (capped.lowerBound + 1)...(capped.upperBound + 1) + return finished(first.extent, around: hit, in: codeLines) } // MARK: - Name extraction @@ -643,7 +703,7 @@ public enum QueenLocalisation { return nil } - // MARK: - Замер (#1173) + // MARK: - Замер (#1173, #1176) /// One case of the #1173 measurement: the identifiers an issue body /// yields through `ChatViewModel.identifiers(from:)`, recorded from the @@ -654,72 +714,98 @@ public enum QueenLocalisation { public let expected: Expected public enum Expected: Equatable { - /// The range must lie inside this function's declaration. + /// The range must begin at this function's declaration line and + /// stay inside its extent. Not "somewhere inside" — a range that + /// begins mid-body begins "not at the named function" (#1176). case declaration(String) + /// The issue's own acceptance, verbatim (#1176): begin at this + /// function's declaration, or say nothing at all. A range that + /// begins anywhere else — another function, mid-body, the wrong + /// end — is a FAIL. + case declarationOrSilence(String) /// No range at all — a wrong range is worse than none (#1175). case silence } } - /// The замер of #1173, repeated and recorded 2026-08-19 — bodies of the - /// four issues fetched live, identifiers extracted exactly as - /// `ChatViewModel.identifiers(from:)` does, `region` run against - /// `rings/SR-02/ChatViewModel.swift` (10 062 lines at recording; the - /// boundary file moves under concurrent work, so the functions are the - /// contract and the line numbers are the snapshot): + /// The замер of #1173, re-recorded 2026-09-02 for #1176 — bodies of the + /// issues unchanged (identifiers are what `ChatViewModel.identifiers(from:)` + /// extracts from them), `region` run against `rings/SR-02/ChatViewModel.swift` + /// as it stands (13 590 lines at recording; the boundary file moves under + /// concurrent work, so the functions are the contract and the line numbers + /// are the snapshot). Two things the re-recording found: /// - /// | case | chose, before | chose, after | the human named | - /// |---|---|---|---| - /// | #1156 | silence | 4968-5101 `handleWorkerFinished` ✓ | `handleWorkerFinished` | - /// | #1158 | 6263-6439 `acceptanceBlockReasonDistinguishingEmptyAnswers` ✗ | 6644-6862 `autoAcceptIfUnambiguous` ✓ | `autoAcceptIfUnambiguous` | - /// | #1165 | silence | silence ✗ | `requestReviewerVerdicts` | - /// | #1166 | silence | 7606-7905 `chooseNextOpenIssue` ✓ | ветка `startAfterChoosing` | + /// 1. The literal view was blind. A branch glob quoted inside a string — + /// `"No empty queen/* branch …"`, line 6864 — opened a phantom block + /// comment and blanked the view from there to the end of the file. + /// 6 726 lines of evidence invisible: corroboration saw no mentions + /// past the glob, #1158's two candidates tied at zero, and rule 4 + /// answered with 6005-6304 — the middle of `handleWorkerFinished`, a + /// function that spec never named. The confident wrong range #1176 is + /// about, and its enabler. `maskComments` knows strings now. + /// 2. #1156's clue moved: the quoted log event `queen.review.characterCount` + /// is no longer emitted inside `handleWorkerFinished` but inside + /// `settleCharacterCountVerdicts` (upstream moved it, 2026-09-02). The + /// recording follows the clue, not the old address. /// - /// **Before 0/4, after 3/4.** (The historic 1-in-4 of the issue title was - /// measured against delegation text the human had written by hand; with - /// today's bodies the old code scores 0/4 — #1158 confidently named the - /// guard's well-behaved neighbour, the neighbour trap of #1176.) + /// | case | answers, 2026-09-02 | expected | + /// |---|---|---| + /// | #1156 | 12555-12584 `settleCharacterCountVerdicts` | the same | + /// | #1158 | 8251-8460 `autoAcceptIfUnambiguous` | it, or silence | + /// | #1165 body | silence | silence | + /// | #1165 clue `queen.review.verdicts` | silence | it, or silence | + /// | #1166 | 9976-10275 `chooseNextOpenIssue` | the same | + /// | #1117 | 7432-7731 `requestReviewerVerdicts` | it, or silence | /// - /// The three hits, and why each rule fires: + /// Why each row answers what it answers: /// - /// - #1156 — rule 4: `characterCount` appears exactly once in the file, - /// as the quoted log line `"queen.review.characterCount"` inside - /// `handleWorkerFinished` (4922-…). - /// - #1158 — rule 1: both the guard and its neighbour are named, and the - /// corroboration is measured, not assumed — the neighbour's body - /// contains 0 mentions of the other identifiers, - /// `autoAcceptIfUnambiguous`'s body contains 4 (`ProcessInfo` and - /// `processInfo` on the quoted guard line, `awaitingReview` twice). - /// - #1166 — rule 3: `startAfterChoosing:` is a parameter of - /// `chooseNextOpenIssue`'s signature and of no other; `ownedPaths:` - /// labels four signatures and is discarded as a common label. + /// - #1156 — rule 4: `characterCount` appears exactly once in the file, as + /// the quoted log line `"queen.review.characterCount"` (12576) inside + /// `settleCharacterCountVerdicts`; the range begins at its declaration. + /// - #1158 — rule 1, corroboration: the neighbour + /// `acceptanceBlockReasonDistinguishingEmptyAnswers` (7830-8006) + /// carries 0 of the other identifiers; `autoAcceptIfUnambiguous` + /// (8251-8460) carries 2 — `ProcessInfo` and `processInfo`, the guard + /// the issue quotes. The subject wins outright. Were upstream to move + /// the guard again, the tie is silence, which the case accepts. + /// - #1165 body — `ChatViewModel` alone: no rule answers, correctly. + /// - #1165 clue — rule 2 finds `queen.review.verdicts` at line 7754, 322 + /// lines into the 376-line `requestReviewerVerdicts`. The window capping + /// slides the start to 7508 — mid-body, beginning nowhere in particular + /// — and the #1176 finishing check silences it. This row is the witness + /// for #1176's fourth criterion: delete the guards in `finished` and it + /// goes red with `7508-7807 not starting at requestReviewerVerdicts`. + /// - #1166 — rule 3: `startAfterChoosing:` labels `chooseNextOpenIssue`'s + /// signature and no other; the wide body caps to a window that still + /// begins at the declaration. + /// - #1117 — rule 1, single candidate: one name, one declaration, the + /// range begins at it (7432). No corroboration needed. /// - /// #1165 stays silent **by the caller's hand, not this file's**: its body - /// names one clue, `queen.review.verdicts` — the log line emitted inside - /// `requestReviewerVerdicts` — but the identifier filter in - /// `ChatViewModel.identifiers(from:)` (#1178) rejects tokens with dots, so - /// the clue never reaches `region`. Handed through directly, rule 2 lands - /// 5941-6240 inside `requestReviewerVerdicts` — the fourth case below - /// proves it. Letting dotted event names through that filter is work in + /// #1165's body still filters its clue out before it reaches `region` — + /// `ChatViewModel.identifiers(from:)` (#1178) rejects dotted tokens, so + /// only the handed-through case below exercises the clue. That filter is /// `rings/SR-02/ChatViewModel.swift`, outside this task's boundary. /// - /// #1117 is kept as a witness for the name rule: 5865-6164 inside - /// `requestReviewerVerdicts`. - /// - /// Replay any time — the check criterion 4 stands on: + /// Replay any time: /// /// swiftc -O .swift rings/SR-00/QueenLocalisation.swift -o probe /// probe # or call replayMeasurement(in:) /// - /// With the name preference (rule 1) removed, the replay goes red on - /// #1158 and #1117 — nothing else can find a function the issue names — - /// and the live замер falls to 2/4. Proven from both sides 2026-08-19. + /// What reddens what, measured 2026-09-02: removing the finishing check + /// (the two guards in `finished`) reddens the `queen.review.verdicts` row + /// — that is #1176's fourth criterion. Removing rule 1 entirely no longer + /// reddens #1158 or #1117: both fall to silence, which #1176 accepts — + /// the name rule is what still *points*; the finishing check is what + /// keeps every pointer honest. static func measurementCases() -> [MeasurementCase] { [ MeasurementCase( + // The quoted log event moved upstream into this function + // (2026-09-02); the recording follows the clue, not the old + // handleWorkerFinished address of the 2026-08-19 table. issue: "#1156", identifiers: ["ChatViewModel", "awaitingReview", "characterCount"], - expected: .declaration("handleWorkerFinished") + expected: .declaration("settleCharacterCountVerdicts") ), MeasurementCase( issue: "#1158", @@ -728,7 +814,10 @@ public enum QueenLocalisation { "acceptanceBlockReasonDistinguishingEmptyAnswers", "autoAcceptIfUnambiguous", "awaitingReview", "processInfo", ], - expected: .declaration("autoAcceptIfUnambiguous") + // #1176 criterion 1, verbatim: point at the named function, + // or say nothing. The neighbour's name is in the body too — + // only corroboration separates them, and a tie is silence. + expected: .declarationOrSilence("autoAcceptIfUnambiguous") ), MeasurementCase( issue: "#1165 (body yields no code symbol; silence is correct)", @@ -736,9 +825,15 @@ public enum QueenLocalisation { expected: .silence ), MeasurementCase( + // #1176 criterion 4's witness. The event sits 322 lines into + // a 376-line function, so the capped window would begin + // mid-body; the finishing check silences it. Delete the + // guards in `finished` and this line goes red with a range + // that starts at 7508 — inside the function, beginning at + // nothing. Pointing, or silence — never that. issue: "#1165 (its actual clue, `queen.review.verdicts`, handed through)", identifiers: ["queen.review.verdicts"], - expected: .declaration("requestReviewerVerdicts") + expected: .declarationOrSilence("requestReviewerVerdicts") ), MeasurementCase( issue: "#1166", @@ -749,19 +844,26 @@ public enum QueenLocalisation { expected: .declaration("chooseNextOpenIssue") ), MeasurementCase( + // #1176 criterion 2, verbatim: point at the named function, + // or say nothing. issue: "#1117", identifiers: ["ChatViewModel", "requestReviewerVerdicts"], - expected: .declaration("requestReviewerVerdicts") + expected: .declarationOrSilence("requestReviewerVerdicts") ), ] } /// Replays the замер against a source file (the boundary file the issues /// talk about — for these cases, `rings/SR-02/ChatViewModel.swift`) and - /// returns one verdict line per case: "ok …" or "FAIL …". This is the - /// check the fourth criterion of #1173 stands on — remove the name - /// preference (rule 1) and the #1158/#1117 lines go red, because nothing - /// else can find a function the issue names. Pure; no I/O. + /// returns one verdict line per case: "ok …" or "FAIL …". + /// + /// A returned range is ok only when it BEGINS at the named function's + /// declaration line and stays inside its extent (#1176 criterion 3) — + /// "somewhere inside" is not good enough, because a range that begins + /// mid-body begins "not at the named function" the same way a range that + /// begins at the neighbour does. + /// + /// Pure; no I/O. static func replayMeasurement(in source: String) -> [String] { let cleaned = source .replacingOccurrences(of: "\r\n", with: "\n") @@ -772,13 +874,13 @@ public enum QueenLocalisation { return measurementCases().map { measure -> String in let range = region(in: source, mentioning: measure.identifiers) switch (range, measure.expected) { - case (nil, .silence): + case (nil, .silence), (nil, .declarationOrSilence): return "ok \(measure.issue): silence" case (nil, .declaration(let name)): - return "FAIL \(measure.issue): silence, expected inside \(name)" + return "FAIL \(measure.issue): silence, expected at \(name)" case (let r?, .silence): return "FAIL \(measure.issue): \(r.lowerBound)-\(r.upperBound), expected silence" - case (let r?, .declaration(let name)): + case (let r?, .declaration(let name)), (let r?, .declarationOrSilence(let name)): guard let declIdx = codeLines.firstIndex(where: { declarationName(on: $0) == name }) else { @@ -786,10 +888,10 @@ public enum QueenLocalisation { } let extent = declarationExtent(declLine: declIdx, depths: depths) let expected = (extent.lowerBound + 1)...(extent.upperBound + 1) - if expected.contains(r.lowerBound), r.upperBound <= expected.upperBound { - return "ok \(measure.issue): \(r.lowerBound)-\(r.upperBound) inside \(name)" + if r.lowerBound == expected.lowerBound, r.upperBound <= expected.upperBound { + return "ok \(measure.issue): \(r.lowerBound)-\(r.upperBound) at \(name)" } - return "FAIL \(measure.issue): \(r.lowerBound)-\(r.upperBound) not inside \(name) (\(expected.lowerBound)-\(expected.upperBound))" + return "FAIL \(measure.issue): \(r.lowerBound)-\(r.upperBound) not starting at \(name) (\(expected.lowerBound)-\(expected.upperBound))" } } } From c3f82177d5356d85118a783390df843ff0f880c0 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 15:43:46 +0700 Subject: [PATCH 2/5] fix(ci): wire the four other portable gates, measured before wired The same audit that found queen-core-sync unwired found five more that need no desktop. Four are wired here; each was RUN against the shipping ref first rather than assumed to pass: sources-drift, sources-drift-selftest, binary-drift, run-completeness-selftest - all four pass in seconds. NOT drift-guard, which the audit also surfaced: it is one line invoking run_chat_sse_e2e.sh with an extra scenario enabled, it drives the Swift compiler, and it did not finish inside four minutes. Its own comment calls it 'slow, explicit, never accidental', and the script it wraps already runs in swift-logic. Wiring a gate that hangs is worse than leaving it unwired. Separate steps rather than one `make a b c d`: make stops at the first failure, and a single step would report one broken gate while hiding the state of the three behind it. Co-Authored-By: Claude Opus 5 --- .github/workflows/trios-logic.yml | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trios-logic.yml b/.github/workflows/trios-logic.yml index 31d2213f72..45784e1b73 100644 --- a/.github/workflows/trios-logic.yml +++ b/.github/workflows/trios-logic.yml @@ -37,13 +37,45 @@ jobs: # # Ubuntu and no Swift: this is `cmp`, and it costs seconds. The comparison # does not need the compiler that consumes its result. - queen-core-sync: + # FOUR MORE GATES THAT RAN NOWHERE, from the same audit. + # + # 68 make targets, 11 invoked by a workflow. Most of the rest should be local - + # `make` builds an app, `relaunch` needs a window server - but these five need + # no desktop at all, and each was MEASURED against the shipping ref before + # being wired here rather than assumed to pass: + # + # sources-drift the build's source list against the tree + # sources-drift-selftest ...and the proof that gate can still fail + # binary-drift the shipped binary against what the tree builds + # run-completeness-selftest the test-run parser, against known transcripts + # + # NOT drift-guard, which the same audit surfaced: it is one line invoking + # run_chat_sse_e2e.sh with an extra scenario enabled, it drives the Swift + # compiler, and it did not finish inside four minutes. Its own comment says it + # is "slow, explicit, never accidental". The script it wraps already runs in + # swift-logic below. + portable-gates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # Separate steps, not one `make a b c d`: make stops at the first failure, + # and a single step would report one broken gate and hide the state of the + # three behind it. Which gate failed is the whole content of the answer. - name: The Linux copy of the policy matches the ring, byte for byte working-directory: trios run: make queen-core-sync + - name: The build's source list matches the tree + working-directory: trios + run: make sources-drift + - name: ...and that gate can still fail + working-directory: trios + run: make sources-drift-selftest + - name: The shipped binary matches what the tree builds + working-directory: trios + run: make binary-drift + - name: The test-run parser reads a known transcript correctly + working-directory: trios + run: make run-completeness-selftest server-units: runs-on: ubuntu-latest From 9124ff948db76fd98e0447a51147e1b930f7f805 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 15:55:15 +0700 Subject: [PATCH 3/5] fix(ci): the public dashboard had a 194-check contract and no workflow trios/apps/website is the t27.ai dashboard - the page anyone reads to see what the Queen is doing. It carries a contract that imports the REAL page module and pins the review lifecycle against the closed set of states the server publishes. Measured: the string 'apps/website' appeared in NO workflow in this repository. Typecheck, the contract (194 checks) and the build all pass and always had - nothing was broken. What was missing was anything that would notice when the server's published states and the page's copy of them stop agreeing, which is the one thing the contract exists to catch. Its own workflow rather than a job in 'trios logic': that one runs a macOS Swift job, and a paragraph of website CSS should not wake a Mac runner. Separate steps, so the answer says which of the three failed - `bun run check` chains them with && and reports only the first. Co-Authored-By: Claude Opus 5 --- .github/workflows/queen-dashboard.yml | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/queen-dashboard.yml diff --git a/.github/workflows/queen-dashboard.yml b/.github/workflows/queen-dashboard.yml new file mode 100644 index 0000000000..ec86765cdc --- /dev/null +++ b/.github/workflows/queen-dashboard.yml @@ -0,0 +1,47 @@ +name: queen dashboard + +# THE PUBLIC FACE HAD A CONTRACT AND NO WORKFLOW. +# +# `trios/apps/website` is the t27.ai dashboard - the page anyone reads to see +# what the Queen is doing. It carries `qa/queen-review-lifecycle-contract.mjs`, +# which imports the REAL page module (the same `src/pages/Queen.tsx` the site +# builds) and pins the review lifecycle against the closed set of states the +# server publishes: 194 checks. +# +# Measured 2026-09-06: the string `apps/website` appeared in NO workflow in this +# repository. Typecheck, the contract and the build all pass and always had - +# nothing was broken. What was missing was anything that would notice when the +# server's published states and the page's copy of them stop agreeing, which is +# the one thing this contract exists to catch. +# +# ITS OWN WORKFLOW, not a job in `trios logic`: that one runs a macOS Swift job, +# and a paragraph of website CSS should not wake a Mac runner. The path filter +# is the point of putting it here. +on: + pull_request: + paths: + - 'trios/apps/website/**' + - '.github/workflows/queen-dashboard.yml' + +jobs: + contract: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + # `--frozen-lockfile`, because a gate that quietly resolves a different + # dependency tree than the one committed is testing a build nobody ships. + - name: Install + working-directory: trios/apps/website + run: bun install --frozen-lockfile + # Separate steps, so the answer says WHICH of the three failed. `bun run + # check` chains all three with `&&` and reports only the first. + - name: Types + working-directory: trios/apps/website + run: bun run typecheck + - name: The review lifecycle contract, against the real page module + working-directory: trios/apps/website + run: bun run qa + - name: The page still builds + working-directory: trios/apps/website + run: bun run build From e2bb315ad141f91f33072a5d97da292b4bdff071 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 16:11:10 +0700 Subject: [PATCH 4/5] fix(ci): three more gates, found by reading the repository's own declaration `make check` names its gate suite in one line of prerequisites - 24 of them - and CI ran 8. A word-list audit had missed type-floor, recipe-backticks and skill-frontmatter entirely, because they are named after their SUBJECT rather than their function: nothing in 'type-floor' or 'vendor-step' says gate. When the system under audit declares the thing you are inferring, read the declaration. All three measured against the shipping ref first. All three pass in seconds. NOT t27-rings, and the reason is written where the next person will look. It is the best gate in the suite - Verilog generated from the .t27 source, compiled with iverilog, simulated under vvp and checked against the Swift table, on top of a 460-case Rust parity - and it passes. But t27c is built from a SEPARATE repository this checkout does not contain, and the target is written to SKIP rather than fail when the toolchain is absent. Wiring it would manufacture a green job that runs nothing, which is the defect this file was added to end. Co-Authored-By: Claude Opus 5 --- .github/workflows/trios-logic.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/trios-logic.yml b/.github/workflows/trios-logic.yml index 45784e1b73..f02d469701 100644 --- a/.github/workflows/trios-logic.yml +++ b/.github/workflows/trios-logic.yml @@ -76,6 +76,35 @@ jobs: - name: The test-run parser reads a known transcript correctly working-directory: trios run: make run-completeness-selftest + # THREE MORE, FOUND BY READING THE REPOSITORY'S OWN DECLARATION. + # + # `make check` names its gate suite in one line of prerequisites: 24 of + # them, of which CI ran 8. A word-list audit had missed these three + # entirely, because they are named after their SUBJECT rather than their + # function - nothing in `type-floor` or `vendor-step` says "gate". When + # the system under audit declares the thing you are inferring, read the + # declaration. + - name: The type floor + working-directory: trios + run: make type-floor + - name: No backticks in a recipe + working-directory: trios + run: make recipe-backticks + - name: Every skill has its frontmatter + working-directory: trios + run: make skill-frontmatter + # + # NOT t27-rings, and the reason is worth writing down. It is the best gate + # in the suite - it generates Verilog from `rings/T27-00/queen_core.t27`, + # compiles it with iverilog, simulates under vvp and checks the answers + # against the Swift table, on top of the 460-case Rust parity. It passes. + # + # It cannot be wired here. `t27c` is built from `$(T27_ROOT)/bootstrap`, + # which lives in a SEPARATE repository this checkout does not contain, and + # the target is written to SKIP rather than fail when the toolchain is + # absent. Wiring it would produce a green job that runs nothing - which is + # precisely the defect the rest of this file was added to end. Vendoring + # or fetching t27c is a real change and belongs in its own. server-units: runs-on: ubuntu-latest From de028e3892362e876ff464b781f20b50ee8a4f65 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 16:28:08 +0700 Subject: [PATCH 5/5] fix(ci): the rings now run, and the job proves they were not skipped `t27-rings` generates Rust from rings/T27-00/queen_core.t27 and checks 14 rows and 21 constants against the Swift table, does the same for the A2A rules in T27-01, then generates VERILOG from the same source, compiles it with iverilog and simulates it under vvp. It is the strongest claim this repository makes about itself, and it ran nowhere. It was left unwired one round ago on purpose: t27c is built from a SEPARATE repository and the target SKIPS - exit 0 - when that toolchain is absent, so wiring it as-is would have produced a green job that measured nothing. The repository is public, so the honest version is to fetch it. Verified end to end locally first: a fresh --depth 1 clone carries bootstrap/, cargo builds t27c from it, and all three halves pass. AND THEN PROVE IT MEASURED. The target says '[SKIP] ... NOT MEASURED' in plain words and still exits 0 - good manners, useless to CI. The step greps for that phrase and fails on it, then demands ring00_parity, ring01_rules and ring00_verilog by name, so a future edit that quietly drops one cannot pass by leaving the others green. A gate that can silently skip needs a second gate on top of it. The clone is read-only, into the runner temp, never into the checkout: nothing here writes to that repository. Co-Authored-By: Claude Opus 5 --- .github/workflows/trios-logic.yml | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/.github/workflows/trios-logic.yml b/.github/workflows/trios-logic.yml index f02d469701..742e3ca1e7 100644 --- a/.github/workflows/trios-logic.yml +++ b/.github/workflows/trios-logic.yml @@ -106,6 +106,56 @@ jobs: # precisely the defect the rest of this file was added to end. Vendoring # or fetching t27c is a real change and belongs in its own. + # THE RINGS, ANSWERED BY THREE LANGUAGES AND A SIMULATOR. + # + # `t27-rings` generates Rust from `rings/T27-00/queen_core.t27` and checks 14 + # rows and 21 constants against the Swift table, does the same for the A2A + # rules in T27-01, then generates VERILOG from the same source, compiles it + # with iverilog and simulates it under vvp. It is the strongest claim this + # repository makes about itself, and it ran nowhere. + # + # It was left unwired one round ago on purpose, because `t27c` is built from a + # SEPARATE repository and the target SKIPS - exit 0 - when that toolchain is + # absent. Wiring it as-is would have produced a green job that measured + # nothing. The repository is public, so the honest version is to fetch it. + # + # AND THEN PROVE IT MEASURED. The target says `[SKIP] ... NOT MEASURED` in + # plain words when it cannot run, which is good manners and useless to CI: the + # exit code is still 0. The last step reads the log and fails on that phrase, + # and additionally demands both halves by name. A gate that can silently skip + # needs a second gate on top of it, and this is that. + t27-rings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Public, and read-only here: cloned into the runner's temp, never into the + # checkout, and nothing writes back to it. `--depth 1` because the history + # of the compiler is not part of the claim. + - name: Fetch the T27 compiler sources + run: git clone --depth 1 https://github.com/gHashTag/t27.git "$RUNNER_TEMP/t27" + - name: Verilog toolchain + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends iverilog + - name: The rings answer the same as the policy + working-directory: trios + env: + T27_ROOT: ${{ runner.temp }}/t27 + run: | + set -euo pipefail + make t27-rings 2>&1 | tee "$RUNNER_TEMP/t27-rings.log" + log="$RUNNER_TEMP/t27-rings.log" + if grep -q 'NOT MEASURED' "$log"; then + echo "::error::t27-rings skipped instead of measuring - the toolchain it needs was not there" + grep -A2 'NOT MEASURED' "$log" + exit 1 + fi + # Both halves by name, so a future edit that quietly drops one cannot + # pass by leaving the other green. + grep -q 'OK] ring00_parity' "$log" + grep -q 'OK] ring01_rules' "$log" + grep -q 'OK] ring00_verilog' "$log" + server-units: runs-on: ubuntu-latest steps: