diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4048ec9f3b..8e49b3b5c9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,13 @@ +# blank_issues_enabled stays false: an issue opened outside a template is an +# issue with no Boundary section, and the Queen cannot reserve anything for it. blank_issues_enabled: false contact_links: - name: Trinity Documentation url: https://github.com/gHashTag/t27/blob/master/docs/nona-03-manifest/SOUL.md about: Read SOUL.md before creating issues + - name: How the game works, and how to join + url: https://t27.ai/blog/how-to-join-the-swarm/ + about: What the swarm is, the two ways in, the board tab by tab, and the laws. + - name: The board + url: https://app.t27.ai/queen/ + about: Live: what the bees are doing, the roadmap, and who contributed. diff --git a/.github/ISSUE_TEMPLATE/swarm-task.md b/.github/ISSUE_TEMPLATE/swarm-task.md new file mode 100644 index 0000000000..6763bfbdbf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/swarm-task.md @@ -0,0 +1,47 @@ +--- +name: Task for the swarm +about: Work a bee can pick up. The Boundary section is what makes that possible. +title: '' +labels: '' +assignees: '' +--- + + + +## Summary + +What is wrong, missing, or hand-written where a spec should be. One paragraph. + +## Boundary + + + +- `specs/port/tools/example.t27` +- `tools/example.py` + +## Acceptance + + + +- [ ] The spec compiles and its own `test` blocks pass +- [ ] `Closes #N` is in the pull request (law L1) diff --git a/.github/workflows/needs-boundary.yml b/.github/workflows/needs-boundary.yml new file mode 100644 index 0000000000..f77ee0d32f --- /dev/null +++ b/.github/workflows/needs-boundary.yml @@ -0,0 +1,54 @@ +name: Needs boundary (make the unreachable backlog visible) + +# AN ISSUE WITHOUT A BOUNDARY IS INVISIBLE WORK. +# +# The Queen reserves paths before she dispatches, so two workers never edit the +# same file at once. An issue that never says which paths it touches has nothing +# to reserve, so she skips it - and says so only as a number in a tick summary +# nobody reads. +# +# Measured 2026-09-23: 563 of 654 open issues had no `## Boundary` section, and +# the tick reported `missingBoundary` for 565 candidates while the swarm sat at +# 2 of 20 lanes. The backlog was not empty. It was unreachable, and nothing in +# the issue list said which ones or why. +# +# This labels them. A label is a filter, a count and a worklist: the 563 stop +# being an invisible number and become a page somebody can work through. It +# never edits an issue body - what the boundary should be is a judgement about +# the work, and a wrong boundary is worse than none, because it reserves the +# wrong files. +# +# The parser is the Queen's: a line beginning `## Boundary` or `## Границы` +# (agent-server queen-tick.ts boundaryPathsOf, and its Swift twin in queend). +# If that rule ever changes, this must follow it or the label will lie. + +on: + schedule: + - cron: '43 5 * * *' + workflow_dispatch: + inputs: + dry_run: + description: 'Count and print, label nothing' + required: false + type: boolean + default: false + +concurrency: + group: needs-boundary + cancel-in-progress: false + +permissions: + issues: write + +jobs: + label: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Label the issues no bee can take + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ inputs.dry_run }} + run: python3 tools/queen/needs_boundary.py ${{ inputs.dry_run && '--dry-run' || '' }} diff --git a/docs/T27-CONSTITUTION.md b/docs/T27-CONSTITUTION.md index 73da99120b..3af45ed26b 100644 --- a/docs/T27-CONSTITUTION.md +++ b/docs/T27-CONSTITUTION.md @@ -64,12 +64,15 @@ Grandfathered non-English paths are listed only in **`docs/.legacy-non-english-d ## § 2 — Invariant Laws (never change without constitutional amendment) -These seven laws are the **constitutional bedrock** of Trinity S³AI / t27. They govern behavior, not formats or scientific claims. Amendments require explicit consensus and version bump. +These laws are the **constitutional bedrock** of Trinity S³AI / t27. They govern behavior, not formats or scientific claims. Amendments require explicit consensus and version bump. -### Law Table (L1–L7) +**L0 was adopted on 2026-09-23, and it is an addition rather than a rewrite.** L1-L7 say how work enters the repository - traced, generated, ASCII, tested - and every one of them is carried by a live gate. What they never said is what the work is *for*. That gap was already being filled informally: the board's source cited "trios CLAUDE.md, law L0" for the rewrite goal, and no such law existed in that file or anywhere else. `trios/CLAUDE.md` carries its own L1-L9 about shell scripts, clippy and ports. A citation that does not resolve is precisely what the honesty rule forbids, so the choice was to delete the claim or to make it true. It was made true. + +### Law Table (L0–L7) | Law # | Name | Body | Enforcement | |-------|------|------|-------------| +| **L0** | **PURPOSE** | Everything below the interface is written once, in `.t27`, and generated to its target - Rust for servers, Zig, C and Verilog for the core and silicon. Two exceptions, both named: the seed (`t27c` itself stays hand-written Rust) and the interface (Swift for trios, TSX for the web) | The measured `.t27` share of the stack must not fall: `apps/website/scripts/roadmap-stack.mjs` in gHashTag/trinity, published at `/roadmap/stack.json` and drawn by the ROADMAP view | | **L1** | **TRACEABILITY** | No code merged without `Closes #N` — every PR must reference a GitHub issue | `.github/workflows/issue-gate.yml` | | **L2** | **GENERATION** | Files under `gen/` are generated; edit the `.t27` spec instead | `./target/release/t27c validate-gen-headers` | | **L3** | **PURITY** | All `.t27` / `.zig` / `.v` / `.c` source — ASCII-only identifiers & comments | `SOUL.md`, `ADR-004`, build.rs language checks | @@ -78,10 +81,11 @@ These seven laws are the **constitutional bedrock** of Trinity S³AI / t27. They | **L6** | **CEILING** | `conformance/FORMAT-SPEC-001.json` + `specs/numeric/gf16.t27` are the numeric ceiling — never forked | SSOT: seal coverage CI | | **L7** | **UNITY** | No new `*.sh` on the critical path for validation / gen / data | `SOUL.md` Article VIII; `t27c` + `tri` only | -### Alias Index (legacy → L1–L7) +### Alias Index (legacy → L0–L7) | Legacy name | New name | |-------------|----------| +| law L0 (cited in gHashTag/trinity board source before it existed) | L0 PURPOSE | | ISSUE-GATE | L1 TRACEABILITY | | NO-HAND-EDIT-GEN | L2 GENERATION | | SOUL-ASCII | L3 PURITY | @@ -92,9 +96,10 @@ These seven laws are the **constitutional bedrock** of Trinity S³AI / t27. They ### Law Priority -Laws follow **Asimov-style priority** (L1 > L2 > … > L7): +Laws follow **Asimov-style priority** (L1 > L2 > … > L7). **L0 sits outside that ordering on purpose**: it is the destination, not a rule about the journey, and it may never be cited to excuse breaking L1-L7. Work that reaches L0 faster by merging untraced, hand-editing generated files or shipping an untested spec has not reached it at all. -1. **L1 TRACEABILITY** (highest) — Without issue linkage, nothing enters the repository +0. **L0 PURPOSE** (the goal, not a tie-breaker) — the stack below the interface becomes `.t27` +1. **L1 TRACEABILITY** — Without issue linkage, nothing enters the repository 2. **L2 GENERATION** — Generated files are output, not source 3. **L3 PURITY** — Language policy enables universal tooling 4. **L4 TESTABILITY** — TDD ensures specifications are verifiable diff --git a/docs/now/2026-09-23-law-l0-and-the-unreachable-backlog.md b/docs/now/2026-09-23-law-l0-and-the-unreachable-backlog.md new file mode 100644 index 0000000000..2ed2a96e7a --- /dev/null +++ b/docs/now/2026-09-23-law-l0-and-the-unreachable-backlog.md @@ -0,0 +1,16 @@ +# NOW -- L0 PURPOSE adopted, and the backlog no bee could take made visible (2026-09-23) + +## The law the board cited before it existed (Closes #4605) + +- `QueenRoadmap.tsx:2` and `roadmap-stack.mjs:5` both cited "trios CLAUDE.md, law L0" for the rewrite goal. `trios/CLAUDE.md` carries L1-L9 about shell scripts, `Closes #N`, clippy, tests, port 9005, a GB fallback, an experience log and push-first. There is no L0 in that file, in this repository, or anywhere else. +- The cause is a real gap, not a typo: L1-L7 govern how work ENTERS the repository and each has a live gate, but none of them says what the work is FOR. The `.t27` rewrite is the stated goal, measured on the ROADMAP view and named as the win condition in `onboarding.t27`, and it had no constitutional standing at all. +- **L0 PURPOSE** is therefore adopted as an ADDITION, not a rewrite: everything below the interface is written once in `.t27` and generated to its target, with two named exceptions (the seed `t27c` stays hand-written Rust; the interface stays Swift for trios and TSX for the web). +- It sits OUTSIDE the Asimov ordering on purpose and may never be cited to excuse breaking L1-L7: work that reaches the goal faster by merging untraced, hand-editing generated files or shipping an untested spec has not reached it. +- Enforcement is a measurement that already exists rather than a new one: the `.t27` share of the stack, computed by `apps/website/scripts/roadmap-stack.mjs` from the GitHub tree API and published at `/roadmap/stack.json`, must not fall. A law nobody can check is decoration. + +## The backlog was unreachable, not empty + +- Measured today: 563 of 654 open issues carry no `## Boundary` section, so the Queen can reserve no paths for them and skips every one; her tick reported `missingBoundary` for 565 candidates while the swarm sat at 2 of 20 lanes. +- `.github/ISSUE_TEMPLATE/swarm-task.md` carries the section, so the class stops growing, and links the tutorial and the board from the issue chooser. +- `tools/queen/needs_boundary.py` plus `.github/workflows/needs-boundary.yml` label the ones missing it: 553 to label, with the 10 roadmap goals and epics held back because a goal is not a task. Self-test passes and the dry run was taken against the live repository. +- Neither writes a boundary into an issue body. Which paths a task may touch is a judgement about the work, and a WRONG boundary is worse than a missing one because it reserves the wrong files. diff --git a/tools/queen/needs_boundary.py b/tools/queen/needs_boundary.py new file mode 100644 index 0000000000..724527e85b --- /dev/null +++ b/tools/queen/needs_boundary.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Label the open issues no bee can take, because they declare no boundary. + +WHY + +The Queen reserves paths before she dispatches, so that two workers never edit +the same file at once. An issue that never says which paths it touches has +nothing to reserve, so she skips it -- and says so only as a number inside a +tick summary nobody reads. + +Measured 2026-09-23: 563 of 654 open issues had no `## Boundary` section, while +the Queen's tick reported `missingBoundary` for 565 candidates and the swarm sat +at 2 of 20 lanes. The backlog was not empty. It was unreachable, and the issue +list said nothing about which ones or why. + +WHAT IT DOES, AND WHAT IT REFUSES TO DO + +It adds `needs-boundary` to open issues without the section, and removes it from +issues that have since gained one. That is all. It never writes a boundary into +an issue body: which paths a task may touch is a judgement about the work, and a +WRONG boundary is worse than a missing one, because it reserves the wrong files +and blocks whatever really owns them. + +THE PARSER IS THE QUEEN'S, NOT A NEW ONE + +A boundary is a line beginning `## Boundary` or `## Границы` -- the rule in +`boundaryPathsOf` (BrowserOS trios/agent-server queen-tick.ts) and its Swift +twin in queend. This file matches that rule and nothing wider: if it accepted a +heading the Queen does not, the label would say "dispatchable" about an issue +she will keep skipping. + +Usage: + python3 tools/queen/needs_boundary.py --dry-run + python3 tools/queen/needs_boundary.py + python3 tools/queen/needs_boundary.py --self-test +""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys + +REPO = "gHashTag/t27" +LABEL = "needs-boundary" +LABEL_COLOR = "d93f0b" +LABEL_ABOUT = ( + "No '## Boundary' section, so the Queen can reserve nothing for it and no " + "bee can take it" +) + +# The Queen's rule, matched exactly: a level-two heading that starts with +# Boundary or the Russian heading her parser also accepts. +BOUNDARY = re.compile(r"^##[ \t]+(Boundary|Границы)", re.MULTILINE) + + +def has_boundary(body: str | None) -> bool: + """Whether the Queen would find a boundary section in this issue body.""" + return bool(BOUNDARY.search(body or "")) + + +def gh(args: list[str]) -> str: + done = subprocess.run(["gh", *args], capture_output=True, text=True) + if done.returncode != 0: + raise RuntimeError(done.stderr.strip()[:400]) + return done.stdout + + +def open_issues() -> list[dict]: + """Every open issue, pull requests excluded: a PR is not dispatchable work.""" + rows: list[dict] = [] + out = gh([ + "api", "--paginate", + f"repos/{REPO}/issues?state=open&per_page=100", + "--jq", + "[.[] | select(.pull_request == null) " + "| {number, body, labels: [.labels[].name]}]", + ]) + for line in out.strip().splitlines(): + if line.strip(): + rows.extend(json.loads(line)) + return rows + + +# A goal is not a task. The roadmap stages and the epics are deliberately not +# dispatchable -- they are containers for the issues that are -- so labelling +# them "needs-boundary" would put eight permanent entries at the top of a list +# whose whole value is that everything on it is worth fixing. +NOT_TASKS = frozenset({"roadmap", "epic"}) + + +def decide(issues: list[dict]) -> tuple[list[int], list[int]]: + """(to label, to unlabel). Pure, so --self-test can drive it.""" + add, drop = [], [] + for issue in issues: + labels = issue.get("labels", []) + labelled = LABEL in labels + if NOT_TASKS & set(labels): + # A goal that somehow carries the label still gets it removed. + if labelled: + drop.append(issue["number"]) + continue + if has_boundary(issue.get("body")): + if labelled: + drop.append(issue["number"]) + elif not labelled: + add.append(issue["number"]) + return add, drop + + +def ensure_label() -> None: + try: + gh(["label", "create", LABEL, "--repo", REPO, + "--color", LABEL_COLOR, "--description", LABEL_ABOUT]) + except RuntimeError as error: + # Already there is the normal case after the first run. + if "already exists" not in str(error): + raise + + +def self_test() -> int: + cases = [ + ("## Boundary\n- a.py\n", True, "the heading the Queen accepts"), + ("## Границы\n- a.py\n", True, "the Russian heading she also accepts"), + ("## Boundary: files\n", True, "a heading with a trailing colon"), + ("### Boundary\n", False, "a level-three heading is not the one she reads"), + ("## boundary\n", False, "she matches the capital B"), + ("Boundary: src/\n", False, "prose is not a section"), + ("text\n\n## Boundary\n- a\n", True, "not only at the top of the body"), + ("", False, "an empty body"), + (None, False, "no body at all"), + ] + failures = 0 + for body, want, why in cases: + got = has_boundary(body) + if got != want: + print(f"FAIL ({why}): {got} != {want}") + failures += 1 + + add, drop = decide([ + {"number": 1, "body": "nothing", "labels": []}, + {"number": 2, "body": "## Boundary\n- a", "labels": [LABEL]}, + {"number": 3, "body": "## Boundary\n- a", "labels": []}, + {"number": 4, "body": "nothing", "labels": [LABEL]}, + # A goal is not a task: never labelled, and cleared if it ever was. + {"number": 5, "body": "nothing", "labels": ["roadmap"]}, + {"number": 6, "body": "nothing", "labels": ["epic", LABEL]}, + ]) + if add != [1] or drop != [2, 6]: + print(f"FAIL (decide): add={add} drop={drop}, expected add=[1] drop=[2, 6]") + failures += 1 + + print("needs_boundary self-test:", "PASS" if failures == 0 else f"{failures} FAILED") + return 1 if failures else 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + if args.self_test: + return self_test() + + issues = open_issues() + add, drop = decide(issues) + goals = sum(1 for i in issues if NOT_TASKS & set(i.get("labels", []))) + print( + f"open issues: {len(issues)}; without a boundary: " + f"{sum(1 for i in issues if not has_boundary(i.get('body')))}; " + f"goals held back: {goals}; to label: {len(add)}; to unlabel: {len(drop)}" + ) + if args.dry_run: + print("dry run: nothing was changed") + print("would label:", ", ".join(f"#{n}" for n in add[:25]) or "(none)") + return 0 + + if add: + ensure_label() + for number in add: + gh(["issue", "edit", str(number), "--repo", REPO, "--add-label", LABEL]) + for number in drop: + gh(["issue", "edit", str(number), "--repo", REPO, "--remove-label", LABEL]) + print(f"labelled {len(add)}, cleared {len(drop)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())