Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/live.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# This workflow implements build target 8 of docs/design/stories/293.md: the
# live rung. Task #324, anchoring NEVER-293.10.
#
# NEVER-293.10 (anti-claim): the live rung is NEVER a required CI check, and
# it NEVER sends PRIVATE-class content to a cloud model.
#
# WHY A SEPARATE FILE, ON PURPOSE, RATHER THAN A JOB IN ci.yml:
#
# A job added to ci.yml runs on every `pull_request` — that file's `on:`
# block (ci.yml:30-36) triggers on `pull_request`, bare, with no
# `branches:` filter. A live job must NEVER run on a PR. ci.yml
# deliberately carries no `paths:` filter and no `branches:` filter
# anywhere (NEVER-9.8, ci.yml:16-22), because a filtered workflow leaves
# its required checks stuck at "pending" forever — so there is no way to
# carve out a non-PR-triggering job inside that file without reintroducing
# the exact filter NEVER-9.8 forbids.
#
# A separate `workflow_dispatch`-only workflow is the only shape that
# CANNOT fire on a PR. That is precisely what makes it safe from ever
# being accidentally promoted to required: a required context that never
# reports on a PR blocks every PR permanently (issue #204's rule, restated
# in ci.yml's comment above the `seam` job) — so a context that can never
# report on a PR is structurally disqualified from ever being made
# required, not merely policy-disqualified.
#
# A third, independent reason: `pull_request` from a fork receives no
# secrets. A required live job on a fork PR could never authenticate to a
# cloud model even if someone tried to wire it up. This is Problem 7a of
# docs/design/stories/293.md — three independently sufficient reasons a
# required live job is structurally illegal, not merely inadvisable.
#
# WHAT THIS JOB ACTUALLY DOES — AND WHAT IT DOES NOT DO:
#
# This job does NOT run the live rung end to end. A real conformance run
# is MODEL-DRIVEN by a human operator (see scripts/live-rung.ts's own
# header): the operator's agent host reads a SKILL.md and performs it
# against a real forge, issuing one `bun run live-rung exec` per tool
# call. `scripts/live-rung.ts` is only the RECORDING SEAM that run is
# driven through, not the run itself, and no CI job can stand in for a
# human operator driving a model.
#
# What this job DOES do is prove the seam still works, safely and without
# touching any forge: it runs `bun run live-rung --help` (prints usage,
# exits 0, touches no filesystem or network) and `bun test
# test/live-rung.test.ts` (the seam's own unit suite, entirely against
# fixtures and the fake port). It creates no session, resolves no real
# commit, spawns no real `gh`/`git` beyond what the test suite itself
# sandboxes, and reaches no cloud model.

name: Live rung seam check

on:
workflow_dispatch:

env:
BUN_VERSION: "1.4.0"
NODE_VERSION: "20"

jobs:
live:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: bun install
- run: bun run live-rung --help
- run: bun test test/live-rung.test.ts
272 changes: 272 additions & 0 deletions docs/evidence/324-20260915T131526Z.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
---
issue: 324
story: 293
repo: dev-pmallapp/iAI
branch: task/324-live-rung-never-required
commit: 603b02d21bc422fe6ea4b36b90b3de630a85af0e
run: 2026-09-15T13:15:26Z
verdict: PASS
---

# Evidence: #324 — the live rung, never required

Build target 8 of `docs/design/stories/293.md`. Cases **13, 14, 15 and 19**.

`commit:` pins the **parent** of the commit carrying this artifact. The live
conformance run itself was performed on **2026-09-12 at commit `1527dfb`**,
which is what the attestation below states.

**This artifact is deliberately self-contained.** Its subject — the fixture
repository `dev-pmallapp/iai-live-rung-fixture` — was created to be destroyed,
and has been deleted. Every figure and every argv below is reproduced here in
full, because there is nothing left to go and look at. An artifact whose
evidence lives only in a deleted repository is an assertion, not a record.

## Chain

```
bun run build exit 0
bun test 1156 tests / 1142 pass / 0 fail / 14 skip / 4218 expect()
bun run lint 0 violations across 126 files
bun run typecheck exit 0
bun run skill-lint 4 SKILL.md + 12 contract files, 0 errors
bun run claim-lint 124 files, 0 errors, 1 warning
bun run skill-harness exit 0
bun run skill-harness --inject exit 1 <- this mode's success condition
scripts/verify-required-checks.sh PASS, SIX contexts
scripts/verify-workflow-hygiene.sh PASS
```

The 1 claim-lint warning is `docs/design/stories/9.md`, which predates the
metadata convention (ruling G1b of #41). **It must stay a warning.**

## Case results

| Case | Claim | Verdict |
|---|---|---|
| 13 | `NEVER-293.10` | **PASS** — `live` absent from all four hardcoded lists; count read, not hardcoded |
| 14 | `NEVER-293.10` | **PASS** — 0 `if:` keys on required jobs, count asserted non-zero first |
| 15 | `NEVER-293.10` | **PASS** — over all 4 classes, denominator read from `DATA_CLASSES` |
| 19 | `CLAIM-293.9` | **PASS with a qualification** — attested below; token cost is an upper bound, see #337 |

## The live conformance run

A model — `amd-anthropic/Claude-Opus-5` on host **opencode**, which is routing
category **`plan`** per `references/model-routing.md:32` — read
`skills/goal-create/SKILL.md` and performed it twice against a real forge.
Every command was routed through `scripts/live-rung.ts`'s `exec`, the only
recorded route to the real port.

**This is a different population from the seam rung's.** The seam rung runs a
*transcription* against a *fake*; gate ruling G2 turns on that distinction.
This run is the model reading the markdown.

| | run 1 | run 2 |
|---|---|---|
| calls | 4 | 3 |
| reads | 3 | 3 |
| forge mutations | **1** | **0** |

Run 2 issued **no mutating command at all**, which is exactly what
`skills/goal-create/SKILL.md:91-93` promises.

### The result worth keeping

**The live run and the transcription agree exactly.** `SCENARIO_ROSTER`'s
`goal-create/absent-milestone` scores run 1 at `mutations=1 reads=3` and run 2
at `mutations=0 reads=3`. The live model produced the same six numbers.

That is evidence for `CLAIM-293.11` the transcription audit could not produce.
`docs/audits/293-transcription-audit.md` compared **prose to code**; this
compares **code to reality**. Neither subsumes the other, and the audit's
verdict is not upgraded by this: n=1 over a non-deterministic trajectory is an
existence proof, not a property. `CLAIM-41.8` stays `model-judged`, and gate
ruling G2 still forbids reading either result as licence to change that.

### The conformance artifact, verbatim

```json
{
"schemaVersion": 1,
"session": {
"repo": "dev-pmallapp/iai-live-rung-fixture",
"resolvedCommit": "1527dfbedb274760617dbfec7a5d3b281e8674e7",
"startedAt": "2026-09-12T15:28:29.924Z",
"host": "opencode"
},
"tokenCost": 305200,
"runs": {
"1": { "totalCalls": 4, "reads": 3, "mutations": 1,
"kinds": { "forge-read": 2, "forge-mutation": 1, "git-read": 1, "git-write": 0, "unclassified": 0 } },
"2": { "totalCalls": 3, "reads": 3, "mutations": 0,
"kinds": { "forge-read": 2, "forge-mutation": 0, "git-read": 1, "git-write": 0, "unclassified": 0 } }
},
"overall": { "totalCalls": 7, "reads": 6, "mutations": 1 },
"nonZeroExits": []
}
```

## #330 is paid, and the precedent inverted

**No `gh` argv this repository builds had ever been executed.** Six tasks
deferred it. This run executed **thirteen**, each built by the real exported
builder in `packages/core/src/gh/`, each routed through the recording seam:

| Family | argv executed | Result |
|---|---|---|
| milestones | `milestoneList`, `milestoneCreate`, `milestoneUpdate` | 3/3 accepted |
| labels | `labelList`, `labelCreate` | 2/2 accepted |
| issues | `issueList`, `issueCreate`, `issueEditBody`, `issueClose` | 4/4 accepted |
| comments | `commentList`, `commentCreate`, `commentEdit` | 3/3 accepted |
| issues | `gh issue edit --milestone` | 1/1 accepted |

**13 executed, 13 accepted, 0 rejected.** `nonZeroExits: []`.

The S2.1 precedent was sixty passing tests and **7 of 9 real verbs rejected on
first contact**. That did not repeat. The argv layer is vindicated — for these
thirteen. `pr` (12 exports), `sub-issues` (14), and the rest of each family
remain unexecuted, so **#330 is paid in part, not in full.**

### The exit code was not taken as the answer

#320's M9/M9b pair measured that a double which refuses and a double which
accepts return the **same verdict** and differ only in **state**. So every
mutation was verified by reading the forge afterwards:

| Mutation | State observed after |
|---|---|
| `milestoneUpdate` | milestone 2's description read `\| probe \| AMENDED by milestoneUpdate \|` |
| `issueEditBody` | issue 1's body read `edited by issueEditBody` |
| `issueClose` | issue 1 state `CLOSED` |
| `commentEdit` | the comment body read `edited by commentEdit` |
| `issue edit --milestone` | issue 1's milestone read `probe-milestone-330` |
| `labelCreate` | label `probe-330` present on issue 1 |

### Both of the fake's unmodelled shapes work against a real forge

`docs/audits/293-transcription-audit.md` recorded two authorised mutations that
are dead in the seam roster: `goal-create`'s milestone `PATCH`, and
`story-create`'s `--milestone` reassign on `gh issue edit`. Both were executed
here and **both succeeded**.

So the dead branches are dead **because of the fake**, precisely as the audit
claimed — not because the transcriptions are wrong. That closes the record the
audit could only open.

## Case 15, and the finding underneath it

The egress gate runs **first** in `live-rung init`, before anything touches the
filesystem, and demonstrates its converse on the same path: if a `USER/`-derived
payload is not refused, `init` fails. A gate that cannot be shown refusing is an
unexercised branch. Observed on the live path:

```
egress gate ADMITTED the declared-synthetic goals fixture (class INTERNAL, destination cloud)
egress gate correctly REFUSED a USER/-derived payload (class PRIVATE)
```

**The finding, now #336.** `packages/core/src/classify/classify.ts` imports only
`./levels` and `./recognisers` — it never imports `./path`, so `classifyPath`'s
`USER/` rule is consulted by nothing. Measured:

| payload | `classify()` |
|---|---|
| `{ source_path: "USER/HEALTH/labs.yaml", … }` | `PRIVATE` |
| `{ zzz_unrelated: "x" }` — no `USER/` anywhere | `PRIVATE` |
| `{ work_state: "USER/HEALTH/labs.yaml" }` | **`INTERNAL`** |
| `classifyPath("USER/HEALTH/labs.yaml")` | `PRIVATE` |

Row 3 is the hole. Row 2 is why it went unnoticed: a test using a `USER/`-ish
payload with unrecognised keys passes **whether or not the rule is wired in** —
it cannot distinguish the reason for its own answer.

**Case 15 still passes honestly.** Its wording is *"`USER/`-derived content is
`PRIVATE` **by fail-safe default**"* — the unrecognised-key rule, which is
exactly what fires. The test now says so in its own name, a control proves the
discrimination, and a tripwire asserts `classify.ts` does not import `./path`,
so wiring it in forces this record to be revisited deliberately.

**`NEVER-293.10` holds:** the gap is in **classification**, not in the guard.
The guard blocks everything it classifies `PRIVATE`.

**And the gate is not what protects this command.** A real goals document under
`work_state` would clear it exactly as the synthetic fixture did. What stops a
run against the real source is narrower and duller: the payload is built from
`renderGoalsFixture` with no flag, argument or code path that can redirect it —
asserted by test. Two protections guarding two different things; conflating them
would leave a hole nobody is watching.

## Why the live rung can never become required

`.github/workflows/live.yml` is a **separate file** with `workflow_dispatch` as
its only trigger. The separation is the mechanism, not tidiness:

- a job in `ci.yml` runs on every `pull_request`, and a live job must not;
- `ci.yml` deliberately carries no `paths:` and no `branches:` filter, because
NEVER-9.8 says a filtered workflow leaves required checks pending forever — so
there is no way to add a non-PR-triggering job to that file;
- a `workflow_dispatch`-only workflow cannot fire on a PR, which is also why it
can never be accidentally promoted: a context that never reports would block
every PR forever if required;
- fork PRs receive no secrets — Problem 7a's third independent reason.

The job proves the seam (`live-rung --help` and its test file) and never opens a
session against a real repository.

`test/ci-workflow.test.ts` asserts the count **before and after** by reading it,
never by hardcoding `6` — so **#334** taking it to seven will not require editing
this test to keep it true. The partition now spans **both** workflow files: every
job in every workflow must be required, or named not-required with a reason.

## Attestation — `CLAIM-293.9`, case 19

`references/verification.md:37` defines `human-attested` as *"a person states it
is so"*, failing when *"nobody is named, or the attestation is inferred from
silence"*.

> **I, `dev-pmallapp`, state** that the live conformance run of `goal-create` was
> performed on **2026-09-12**, at commit **`1527dfb`**, on host **opencode**, at
> routing category **`plan`** (`amd-anthropic/Claude-Opus-5`, per
> `references/model-routing.md:32`), with the artifact reproduced in full in this
> document.
>
> The run was a fair exercise: the goals source was #319's declared-`synthetic`
> fixture, no expected answer was placed in the prompt, and the mutation counts
> were produced by the recording seam rather than transcribed by hand.

**Token cost: 305.2k tokens / \$36.14 — an opencode SESSION TOTAL for
2026-09-12, not the isolated cost of the `goal-create` verb.**

`docs/milestones/M2.md:240-241` asks for the measured cost of *Tier 0 plus one
Tier-1 verb*. This figure is an **upper bound** on that: the same session also
built the live rung, the transcription audit, the CI workflow and the mutation
rounds. It is recorded as an upper bound and **must not be quoted as the verb's
cost**.

> **`M2.md:240-241` SHOULD NOT BE TICKED ON THIS NUMBER.** Raised as **#337**,
> with the cheap fix named: one dedicated session that runs a single Tier-1 verb
> end to end, so the session total *is* the verb's cost. `--token-cost` already
> accepts the figure. Closing a context-budget criterion on a bound two orders of
> magnitude above the thing being bounded is the *"stronger-sounding class than
> the check supports"* failure `references/verification.md:43-46` names.

## The fixture repository is gone, by design

`dev-pmallapp/iai-live-rung-fixture` was created for this run and **deleted by
the operator via the web interface afterwards** — verified absent at the time of
writing: `gh repo view` returns *"Could not resolve to a Repository"*.

The token in use carries `repo` but **not `delete_repo`**, so the deletion could
not be automated. Recorded because the next live rung will hit the same wall.

## What this does NOT establish

- **`CLAIM-293.11` is not upgraded.** n=1 over a non-deterministic trajectory is
an existence proof, not a property. `CLAIM-41.8` and `CLAIM-47.4` stay
`model-judged`; gate ruling G2 forbids otherwise.
- **#330 is paid in part.** Thirteen argvs met a real forge. `pr` (12 exports)
and `sub-issues` (14) did not. The bill is smaller, not settled.
- **M2's token-cost criterion is not closed** — #337.
- **The `USER/` classification gap is open** — #336. No caller exploits it today.
- **One skill was run live, not four.** `goal-create` only.
- **The live rung is not a required check, and must never become one.**
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"typecheck": "tsc -b tsconfig.json --force",
"skill-lint": "bun scripts/skill-lint.ts",
"claim-lint": "bun scripts/claim-lint.ts",
"skill-harness": "bun scripts/skill-harness.ts"
"skill-harness": "bun scripts/skill-harness.ts",
"live-rung": "bun scripts/live-rung.ts"
},
"devDependencies": {
"typescript": "^7.0.2",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/classify/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type { DataClass } from "./levels";
export { DATA_CLASSES } from "./levels";
export { classify } from "./classify";
export { classifyPath } from "./path";
export { classifyKeyName, classifySecretShape, normalizeKey } from "./recognisers";
26 changes: 20 additions & 6 deletions packages/core/src/classify/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@
// result (docs/design/stories/15.md, CLAIM-15.1).
export type DataClass = "PUBLIC" | "INTERNAL" | "PRIVATE" | "SECRET";

const RANK: Record<DataClass, number> = {
PUBLIC: 0,
INTERNAL: 1,
PRIVATE: 2,
SECRET: 3,
};
// THE single source of truth for which classes exist, in rank order,
// least-sensitive first (Task #324, case 15 of docs/test-plans/293-plan.md,
// anchoring NEVER-293.10). `RANK` below is DERIVED from this array rather
// than restating the four members, so a fifth class added to `DataClass`
// cannot silently go missing from one of the two without also going missing
// from the other — the two would simply fail to compile or fail
// packages/core/test/classify.test.ts's agreement assertions instead.
//
// Frozen so a caller holding a reference cannot mutate the shared array out
// from under every other reader of this module.
export const DATA_CLASSES: readonly DataClass[] = Object.freeze([
"PUBLIC",
"INTERNAL",
"PRIVATE",
"SECRET",
]);

const RANK: Record<DataClass, number> = Object.freeze(
Object.fromEntries(DATA_CLASSES.map((dataClass, index) => [dataClass, index])) as Record<DataClass, number>,
);

export function rankOf(dataClass: DataClass): number {
return RANK[dataClass];
Expand Down
Loading
Loading