From 63c54b950b2aa170a3ef88077cf5ccc6fbc5e190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=B1=D0=BE=D1=82?= Date: Tue, 13 Oct 2026 06:28:30 +0200 Subject: [PATCH 1/3] fix: gate owner board declarations and release ancestry --- .github/workflows/validate.yml | 28 +-- docs/HANDOFF.md | 11 ++ docs/evidence/backlog.md | 3 +- .../runs/2026-10-13-r30-owner-gates.md | 55 ++++++ package.json | 2 +- test/member_gates.py | 180 ++++++++++++++++++ test/member_owner_gates_test.py | 64 +++++++ test/validate.py | 65 +++++++ 8 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 docs/HANDOFF.md create mode 100644 docs/evidence/runs/2026-10-13-r30-owner-gates.md create mode 100644 test/member_gates.py create mode 100644 test/member_owner_gates_test.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5cd66c9..4beb72c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,21 +1,18 @@ name: validate on: - push: - # A TAG push must not start this workflow. `release.yml` calls it through - # `workflow_call`, and both runs land in the same concurrency group - # (`validate-${{ github.ref }}`) — so the push-triggered one cancels the - # release's own call, `release` and `publish` are SKIPPED for a `needs:` that - # was cancelled rather than failed, and the tag ships nothing while the run - # list shows a green validate beside it. Measured 2026-08-20 on - # `agent-stack v0.13.0`: validate green, release cancelled at 2s, npm still - # serving the previous version. It is a race, which is why the two siblings - # with the same shape happened to publish. - tags-ignore: ['**'] + # One explicit frozen candidate after local checks and independent review. pull_request: - # Callable, so `release.yml` gates a publish on THIS suite instead of a - # copy of it. The negative self-tests live here as steps; running them - # from the release workflow any other way means a second home per plant. + types: [ready_for_review] + schedule: + - cron: '0 23 * * *' + timezone: Europe/Warsaw + workflow_dispatch: + inputs: + candidate_reason: + description: 'Recorded explicit candidate exception with task and frozen SHA' + required: true + type: string workflow_call: permissions: @@ -41,6 +38,9 @@ jobs: - name: Structural validator run: python3 test/validate.py + - name: Owner board and release guard plants + run: python3 test/member_owner_gates_test.py + - name: Audit regressions (sherlock plan, one file per closed finding) run: npm run test:audit diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..39e17d3 --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,11 @@ +# Current work — R30 owner gates + +Objective: close agent-stack's missing own-board/release-ancestry checks under the +family's original R30. [Bounded packet and checks](evidence/runs/2026-10-13-r30-owner-gates.md) +is the entry for the next agent. Branch `fix/r30-owner-gates-1013`, basecb8f699. +Eight baseline defects reproduced; candidate14 controls pass; aggregate review, +integration and publication remain pending. No installed payload changes. + +Exact next task: independent source/negative-control review, then full local gate +and a frozen draft-to-ready candidate. Preserve nightly CI and normal required +checks. Commit/push and verify exact release source before hub re-pin/install. diff --git a/docs/evidence/backlog.md b/docs/evidence/backlog.md index b003bfa..9b3b122 100644 --- a/docs/evidence/backlog.md +++ b/docs/evidence/backlog.md @@ -45,9 +45,10 @@ row per line, appended, so two runs closing two rows do not land on the same lin | B-157 | **The back-links to the Fabric adapter run one way.** agent-interop now points to `fabric-agent-adapter`'s `creating-fabric-agents`, `adapting-projects-to-fabric` and `building-fabric-services`; of those, only `adapting-projects-to-fabric/SKILL.md:25-26` points back (wire questions). The adapter belongs to the Fabric owner: ask there for a pointer from the other two to agent-interop's tier doctrine. Nothing contradicts today — the adapter carries its own copy of the tier rule (`delegation.md:47-55`, 0.20.0). | wave 4 seam reading, 2026-10-12 | 1 | 0 | 1 | **1** | **open** — an ask to the Fabric adapter's owner | | B-158 | **The residue-ledger check misses an aliased temp-dir call.** `TEMP_MAKERS` (`test/validate.py`) excludes every dotted receiver but `tempfile.`, so `import tempfile as tf; tf.mkdtemp()` and `from tempfile import mkdtemp as m` pass. make-skill 0.31.0 ported this check and closed the gap there (any receiver, renamed from-imports refused, two negative controls); port that back. No current file here uses an alias. | make-skill W4-B seam reading, 2026-10-12 | 1 | 0 | 1 | **1** | **closed in v0.27.1 (2026-10-12)** — `TEMP_MAKERS` is make-skill 0.31.0's pattern (any single receiver, one-line renaming from-import refused); the alias and renamed-import negative controls in `test/validate.py` were watched failing on the old pattern (`B-158 self-test: a temp-dir call through a module alias (\`tf.\`) was not found`, `… a renamed from-import of a temp-dir maker was not found`), then passed. Evidence: verification.md, v0.27.1 | | AG-06a | **A temp tree created outside `test/` is still invisible.** `check_temp_trees_go_through_the_residue_ledger` reads this repository's own suite. A tree made by anything else — a script under `plugins/`, a future helper outside `test/` — is not accounted for, and `residue.report()` counts only what `workspace()` handed out. The mechanism catches a *reviewed* bypass; it does not observe `$TMPDIR`. Observing it would mean a run claiming ownership of directories it cannot prove it made, which is the reaping mistake `agent-sync` refuses by design. | AG-06 port, 2026-08-20; plan R28 of the hub's 2026-10-11 integration waves | 1 | 0 | 1 | **1** | **closed in v0.27.1 (2026-10-12)** for the static half: the check now reads every `*.py` the repository ships or runs (`test/` recursively, `plugins/`, `bin/`) with exact-path, exact-count, reasoned exemptions (`test/residue.py` 1, `agent-harness/scripts/audit_agent.py` 2). Its first run refused a real bypass, `test/audit_regressions/instruction_budget.py` line 21, now `residue.workspace()`. Plants: an aliased maker in a plugin script, a renamed from-import under `bin/`, a third maker in `audit_agent.py` — each refused by file and line. What stays true: the check reads source, it does not observe `$TMPDIR`; that half stays with AG-06b's content fingerprint. Evidence: verification.md, v0.27.1 | -| AG-06b | **The 2592 existing trees are reported and left alone.** `find "$TMPDIR" -maxdepth 4 -type f -path '*/copy/sub/b.sh' \\| wc -l` → **2592** on 2026-08-20, of which **16 were added by this run's own verification** — one run of the leaking fixture before the port, and one of the v0.12.0 archive's copy of it. They are plain `tmpXXXXXXXX` and indistinguishable from every other program's, so they are named rather than swept. The identical fixture still leaks in **`seo-aeo-audit`**, which another agent holds; referred, not edited. | AG-06 port, 2026-08-20; plan R28 of the hub's 2026-10-11 integration waves | 2 | 0 | 1 | **2** | **closed in v0.27.1 (2026-10-12)** — the trees ARE identifiable by content, so they are reapable without claiming another program's directory: `python3 test/residue.py --reap-own` (dry run by default, `--apply` to remove) claims only an `mkdtemp()`-named, non-symlink child of the temp root holding exactly one of the fixture's five end-state file sets and its directory set byte for byte, plus optionally its own manifest, unchanged for 24 h; root, age floor and the removal window hardened after the PR #45 reading (verification.md, v0.27.1). `test/reap_own_test.py` 6/6 in a sandbox temp root; a planted permissive reaper (any tree holding `sub/b.sh`) was refused: `near-misses were claimed as own: ['tmpextrab01', 'tmpextraf01']`. Real `$TMPDIR`, dry, 2026-10-12: `0 tree(s) match`; the `find` count is 0 — the 2592 trees are already gone, and both leaking suites (`agent-stack`, `seo-aeo-audit`) now use `residue.workspace()`. Nothing in the real `$TMPDIR` was deleted | +| AG-06b | **The 2592 existing trees are reported and left alone.** `find "$TMPDIR" -maxdepth 4 -type f -path '*/copy/sub/b.sh' \| wc -l` → **2592** on 2026-08-20, of which **16 were added by this run's own verification** — one run of the leaking fixture before the port, and one of the v0.12.0 archive's copy of it. They are plain `tmpXXXXXXXX` and indistinguishable from every other program's, so they are named rather than swept. The identical fixture still leaks in **`seo-aeo-audit`**, which another agent holds; referred, not edited. | AG-06 port, 2026-08-20; plan R28 of the hub's 2026-10-11 integration waves | 2 | 0 | 1 | **2** | **closed in v0.27.1 (2026-10-12)** — the trees ARE identifiable by content, so they are reapable without claiming another program's directory: `python3 test/residue.py --reap-own` (dry run by default, `--apply` to remove) claims only an `mkdtemp()`-named, non-symlink child of the temp root holding exactly one of the fixture's five end-state file sets and its directory set byte for byte, plus optionally its own manifest, unchanged for 24 h; root, age floor and the removal window hardened after the PR #45 reading (verification.md, v0.27.1). `test/reap_own_test.py` 6/6 in a sandbox temp root; a planted permissive reaper (any tree holding `sub/b.sh`) was refused: `near-misses were claimed as own: ['tmpextrab01', 'tmpextraf01']`. Real `$TMPDIR`, dry, 2026-10-12: `0 tree(s) match`; the `find` count is 0 — the 2592 trees are already gone, and both leaking suites (`agent-stack`, `seo-aeo-audit`) now use `residue.workspace()`. Nothing in the real `$TMPDIR` was deleted | | B-159 | **`seo-aeo-audit` still has the AG-06a/B-158 hole.** Its temp-tree check reads less than the whole repository and misses aliased calls, and `test/negatives.py:195` makes a bare `tempfile.mkdtemp(prefix="tp-negatives-snap-")` outside any residue ledger (read on its `origin/main`, 2026-10-12). Port agent-stack 0.27.1's whole-repository scan, any-receiver pattern and exact-shape exemptions there, and route that snapshot through the ledger. | agent-stack PR #45 blind reading, 2026-10-12 | 1 | 0 | 1 | **1** | **open** — another repository; referred, not edited | | B-160 | **make-skill's temp-tree check reads `test/` only.** make-skill 0.31.0 has the any-receiver pattern but walks `test/` alone, so a maker in a shipped script is invisible there exactly as AG-06a described here. Port agent-stack 0.27.1's whole-repository scan with exact-path, exact-count, exact-shape exemptions back to make-skill. | agent-stack PR #45 blind reading, 2026-10-12 | 1 | 0 | 1 | **1** | **open** — another repository; referred, not edited | +| B-161 | **The owner validator accepted duplicate declarations and a disabled release ancestry check.** Eight real-validator plants passed on cb8f699; the same seam also hid an even-backslash Markdown pipe in AG-06b. | hub R30, 2026-10-13 | 2 | 0 | 1 | **2** | **open** — implementation candidate and14 process controls; independent review, integration and release pending. [Packet](runs/2026-10-13-r30-owner-gates.md) | ## Open, and why diff --git a/docs/evidence/runs/2026-10-13-r30-owner-gates.md b/docs/evidence/runs/2026-10-13-r30-owner-gates.md new file mode 100644 index 0000000..b15d552 --- /dev/null +++ b/docs/evidence/runs/2026-10-13-r30-owner-gates.md @@ -0,0 +1,55 @@ +# R30 owner board and release gates — 2026-10-13 + +Base: `cb8f6993a61473f3974bc3af67925439ed4abb75`, version0.27.1. +Owner: agent-stack, leaseR30-GATES-1013. This is a bounded implementation packet +within the hub's original R30/B108/B109, not a closure of other members' gates. + +## Contract and reproduction + +The owner validator must refuse a malformed or duplicate declared board identity +and a release workflow whose tag ancestry guard can be bypassed. Preserve the +board's composite historical IDs, explicit umbrella namespace and summary table. +Use the already independently reviewed shared release parser, with no shell/YAML +semantic claim beyond its accepted structural grammar. Preserve release gates and +follow the current operator's nightly/explicit-candidate hosted-check policy. + +Before the edit, the real `test/validate.py` passed at the base. Eight isolated +process plants then each returned0: duplicate declaration, missing board, missing +header, absent ancestry command, shallow checkout, conditional guard, ignored +failure and early successful exit. The test expected exit1 with the intended +reason and reported8failures. Owned failure copies remain in the residue ledger; +private temporary paths are not copied here. + +## Implementation and controls + +`test/member_gates.py` is byte-identical to telegram-dev's shared helper at +`711576f89d06525ee94ad4aac0f779a5baeefdd5`; only its release parser and Markdown +cell/label primitives are used. The board adapter is member-specific: one primary +8column declaration table; grouped IDs expanded for duplicate detection; explicit +umbrella IDs kept in a separate namespace. The3column summary is a reference, +not a second declaration. Unsupported primary header shapes fail closed. + +The first port found AG-06b's historical command cell contained an even number +of backslashes before a pipe, making9 rendered cells. Its Markdown escape now +keeps8 cells without changing the displayed command. This correction is included +in the same board-contract change. + +`python3 test/member_owner_gates_test.py` now passes14 real-process controls: +11 defects plus3 valid controls. Added probes cover composite-ID overlap, the +real even-backslash defect and a missing separator; valid controls retain the +summary, distinguish hub/local IDs and accept bold formatting. Every mutation +asserts changed bytes. The suite requires the expected diagnostic as well as +exit1; it is wired into npm test and the hosted workflow. Its14 owned trees were +removed on successful completion; failures remain attributable by residue. + +## Delivery and bounds + +Independent review and full local/hosted gates remain pending. No version, tag, +registry or installation is changed. The new nightly schedule is23:00Warsaw; +only deliberate ready-for-review promotion runs an explicit candidate. Routine +push/open/synchronize full-suite triggers are removed under operator policy. + +Exact next task: independently review this candidate and its parser boundaries, +then run the owner full gate once, promote a frozen draft PR, integrate normally +and include it in a reviewed patch release. Do not advance the hub pin from this +unreleased branch. diff --git a/package.json b/package.json index da91e7f..6a5916c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@ssheleg/agent-stack", "version": "0.27.1", "scripts": { - "test": "python3 test/validate.py && python3 test/plant_guard_test.py && python3 test/reap_own_test.py && node test/installer_test.js && npm run test:audit", + "test": "python3 test/validate.py && python3 test/plant_guard_test.py && python3 test/member_owner_gates_test.py && python3 test/reap_own_test.py && node test/installer_test.js && npm run test:audit", "test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done" }, "description": "Production patterns for AI agent orchestrators \u2014 tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay \u2014 plus the wallet side of reselling LLM access. This package is the installer CLI.", diff --git a/test/member_gates.py b/test/member_gates.py new file mode 100644 index 0000000..42cc61b --- /dev/null +++ b/test/member_gates.py @@ -0,0 +1,180 @@ +# shared-mechanism: sshlg-skills/member-gates-v1 +# diverges: none +# Copies: telegram-dev, xr-dev, web3d-dev; change and verify the family together. +"""Portable owner gates, shared byte-for-byte by the FW02 member wave. + +Stdlib only. Release validation checks the current workflow's structural contract, +not arbitrary YAML or shell semantics. Unknown workflow layouts fail closed. +""" +from pathlib import Path +import re +import shlex + + +def cells(line): + """Markdown pipes escaped by an odd number of backslashes are cell content.""" + result, start, escapes = [], 0, 0 + for index, char in enumerate(line): + if char == '|' and escapes % 2 == 0: + result.append(line[start:index].strip()) + start = index + 1 + escapes = escapes + 1 if char == '\\' else 0 + result.append(line[start:].strip()) + return result[1:-1] if not result[0] and not result[-1] else result + + +def label(value): + value = value.strip().strip('*` ') + link = re.fullmatch(r'\[([^\]]+)\]\([^)]*\)', value) + return (link.group(1) if link else value).strip('*` ') + + +def board_errors(text): + errors, seen, width, declares, count, fenced = [], {}, None, False, 0, False + pending = None + for number, line in enumerate(text.splitlines(), 1): + line = line.strip() + row = cells(line) if line.startswith('|') else [] + separator = bool(row) and all(re.fullmatch(r':?-{3,}:?', c) for c in row) + if pending is not None: + if separator and len(row) == width and line.endswith('|'): + pending = None + continue + errors.append(f'{pending}: board header needs an immediate matching separator') + pending = None + if line.startswith(('```', '~~~')): + fenced = not fenced + width = None + continue + if fenced: + continue + if not line.startswith('|'): + width = None + continue + first = label(row[0]) if row else '' + if first.lower() == 'row': + errors.append(f'{number}: board Row header unsupported; use ID for declarations') + width = None + continue + if first.lower() == 'id': + width, declares, pending = len(row), True, number + continue + if separator: + if width is not None: + errors.append(f'{number}: board misplaced separator') + continue + identifier = re.fullmatch(r'[A-Z]{1,4}-\d+[a-z]?', first) + if width is None: + if identifier: + errors.append(f'{number}: board row without ID header: {first}') + continue + if len(row) != width or not line.endswith('|'): + errors.append(f'{number}: board row width: {first} has {len(row)} cells; header declares {width}') + if not identifier: + errors.append(f'{number}: board invalid ID: {first!r}') + continue + if declares: + count += 1 + if first in seen: + errors.append(f'{number}: board duplicate ID {first}; first at {seen[first]}') + seen[first] = number + if pending is not None: + errors.append(f'{pending}: board header needs an immediate matching separator') + if not count: + errors.append('board has no declared rows') + return errors + + +def release_errors(text): + """Require the live release job's checkout and unconditional refusing guard. + + Bounded to this family's two-space job/six-space step layout; comments or + matching text in another job/step cannot satisfy this contract. + """ + errors = [] + active = '\n'.join(line for line in text.splitlines() if not line.lstrip().startswith('#')) + if re.search(r'^\s*(?:-\s*)?<<\s*:', active, re.M): + return ['release ancestry guard merge keys require explicit review'] + if re.search(r'^\s*(?:-\s*)?[\'"][^\'"\n]+[\'"]\s*:', active, re.M): + return ['release ancestry guard quoted mapping keys require explicit review'] + if len(re.findall(r'^jobs:', active, re.M)) != 1 or len(re.findall(r'^ release:', active, re.M)) != 1: + return ['release ancestry guard job is missing or ambiguous'] + if re.search(r'^(?:defaults:| defaults:)', active, re.M): + return ['release ancestry guard defaults require explicit review'] + job = re.search(r'^ release:\s*\n(?P.*?)(?=^ [\w-]+:|\Z)', active, re.M | re.S) + if not job: + return ['release job missing or unsupported layout'] + body = job.group('body') + if len(re.findall(r'^ steps:', body, re.M)) != 1: + return ['release ancestry guard steps are missing or ambiguous'] + if re.search(r'^ continue-on-error:\s*(?!false\s*(?:#.*)?$)\S', body, re.M): + errors.append('release ancestry failure ignored at job scope') + steps = re.split(r'(?=^ - )', body, flags=re.M)[1:] + full, checkouts, guards = [], [], [] + for index, step in enumerate(steps): + if re.search(r'^(?: - | )uses:\s*actions/checkout@', step, re.M): + checkouts.append(index) + options = re.findall(r'^ with:\s*\n((?: [^\n]*\n?|\n)*)', step, re.M) + depth = re.findall(r'^ fetch-depth:\s*(.*?)\s*(?:#.*)?$', options[0], re.M) if len(options) == 1 else [] + uses = re.findall(r'^(?: - | )uses:', step, re.M) + if (len(uses) == 1 and len(re.findall(r'^ with:', step, re.M)) == 1 + and not re.search(r'^(?: - | )if:', step, re.M) + and not re.search(r'^(?: - | )continue-on-error:\s*(?!false\s*(?:#.*)?$)\S', step, re.M) + and len(depth) == 1 and depth[0].strip('\'"') == '0'): + full.append(index) + run = re.search(r'^ run:\s*\|[-+]?\s*\n((?: [^\n]*\n?|\n)*)', step, re.M) + if not run: + continue + shell = run.group(1) + guard = re.search(r'^ if ! git merge-base --is-ancestor HEAD [\'"]origin/\$DEF[\'"]; then\s*\n(?P.*?)(?=^ fi\s*$)', shell, re.M | re.S) + if not guard: + continue + guards.append(index) + # This is the supported shell setup, not a substring hunt through arbitrary + # programs. Unknown setup/control flow must be reviewed with new fixtures. + prefix = [line.strip() for line in shell[:guard.start()].splitlines() if line.strip()] + expected = ['set -eu', 'DEF="$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)"', + 'git fetch --no-tags origin "$DEF"'] + if prefix != expected or len(re.findall(r'^(?: - | )run:', step, re.M)) != 1: + errors.append('release ancestry guard has unsupported setup or duplicate run') + commands = [line.strip() for line in guard.group('failure').replace('\\\n', '').splitlines() if line.strip()] + try: + refusal = shlex.split(commands[-1], comments=True) == ['exit', '1'] + for command in commands[:-1]: + tokens = list(shlex.shlex(command, posix=True, punctuation_chars=';&|')) + if not tokens or tokens[0] != 'echo' or any(t and set(t) <= set(';&|') for t in tokens) or '$(' in command or '`' in command: + refusal = False + except (ValueError, IndexError): + refusal = False + if not refusal: + errors.append('release ancestry guard refusal has unsupported control flow') + shell_option = re.findall(r'^(?: - | )shell:\s*(.*?)\s*$', step, re.M) + if shell_option and shell_option != ['bash']: + errors.append('release ancestry guard shell requires explicit review') + if not re.search(r'^\s+exit 1\s*(?:#.*)?$', guard.group('failure'), re.M): + errors.append('release ancestry guard has no refusing exit') + if re.search(r'^(?: - | )if:', step, re.M): + errors.append('release ancestry step conditional') + if re.search(r'^(?: - | )continue-on-error:\s*(?!false\s*(?:#.*)?$)\S', step, re.M): + errors.append('release ancestry failure ignored at step scope') + if not guards: + errors.append('release ancestry guard missing from a live run block') + before = [i for i in checkouts if guards and i < guards[0]] + if not full or (guards and (not before or before[-1] not in full)): + errors.append('release full-history checkout missing before ancestry guard') + return errors + + +def check(root, fail): + root = Path(root) + for relative, inspect, missing in [ + ('docs/evidence/backlog.md', board_errors, 'board missing'), + ('.github/workflows/release.yml', release_errors, 'release workflow missing'), + ]: + try: + text = (root / relative).read_text(encoding='utf-8') + except OSError as exc: + fail(f'{relative}: {missing}: {exc.__class__.__name__}') + continue + for error in inspect(text): + fail(f'{relative}: {error}') diff --git a/test/member_owner_gates_test.py b/test/member_owner_gates_test.py new file mode 100644 index 0000000..41f2dfe --- /dev/null +++ b/test/member_owner_gates_test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Run bounded plants through the real owner validator; account for every copy.""" +from pathlib import Path +import re +import shutil +import subprocess +import sys +import residue + +ROOT = Path(__file__).resolve().parent.parent +BOARD = 'docs/evidence/backlog.md' +RELEASE = '.github/workflows/release.yml' + +def duplicate(text): + line = next(x for x in text.splitlines() if x.startswith('| B-160 |')) + return text.replace(line, line.replace('| B-160 |', '| B-159 |'), 1) + +cases = [ + ('duplicate declaration', BOARD, duplicate, 'board duplicate ID'), + ('group overlaps later declaration', BOARD, lambda s: s.replace('| B-160 |', '| AG-04 |', 1), 'board duplicate ID'), + ('even backslash pipe escape', BOARD, lambda s: s.replace('\\| wc -l', '\\\\| wc -l', 1), 'owner board row width'), + ('missing separator', BOARD, lambda s: s.replace('|---|---|---|---|---|---|---|---|', '', 1), 'matching separator'), + ('missing board', BOARD, lambda _: None, 'owner board missing'), + ('missing header', BOARD, lambda s: s.replace('| id | What | Source |', '| key | What | Source |', 1), 'owner board declaration header'), + ('missing ancestry', RELEASE, lambda s: s.replace('git merge-base --is-ancestor', 'git merge-base-removed --is-ancestor'), 'release ancestry guard'), + ('shallow checkout', RELEASE, lambda s: s.replace('fetch-depth: 0', 'fetch-depth: 1'), 'release full-history checkout'), + ('ancestry conditional', RELEASE, lambda s: s.replace(' - name: The tag must be reachable', ' - if: false\n name: The tag must be reachable'), 'release ancestry step conditional'), + ('ancestry ignores failure', RELEASE, lambda s: s.replace(' - name: The tag must be reachable', ' - continue-on-error: true\n name: The tag must be reachable'), 'release ancestry failure ignored'), + ('early successful exit', RELEASE, lambda s: s.replace(' if ! git merge-base', ' exit 0\n if ! git merge-base', 1), 'release ancestry guard'), +] +failures=[] +for label, relative, change, diagnostic in cases: + residue.open_case(label) + temporary=Path(residue.workspace('member-owner-gates')) + copy=temporary/'repo' + shutil.copytree(ROOT, copy, ignore=shutil.ignore_patterns('.git','node_modules','__pycache__','.agent-sync')) + target=copy/relative + before=target.read_text();after=change(before);assert before!=after,label + if after is None:target.unlink() + else:target.write_text(after) + p=subprocess.run([sys.executable,str(copy/'test/validate.py')],cwd=copy,capture_output=True,text=True) + output=p.stdout+p.stderr + if p.returncode!=1 or diagnostic not in output: + failures.append(f'{label}: expected exit1 and {diagnostic!r}; got exit{p.returncode}') + else:residue.close_case(label) +# The original summary repeats IDs deliberately; namespace and formatting must +# not create false duplicates or silently drop declarations. +controls = [ + ('unchanged board', lambda s:s), + ('foreign namespace distinct', lambda s:s.replace('| B-160 |','| B-118 |',1)), + ('formatted identifier', lambda s:s.replace('| B-160 |','| **B-160** |',1)), +] +for label, change in controls: + residue.open_case(label) + temporary=Path(residue.workspace('member-owner-control'));copy=temporary/'repo' + shutil.copytree(ROOT,copy,ignore=shutil.ignore_patterns('.git','node_modules','__pycache__','.agent-sync')) + target=copy/BOARD;target.write_text(change(target.read_text())) + p=subprocess.run([sys.executable,str(copy/'test/validate.py')],cwd=copy,capture_output=True,text=True) + if p.returncode:failures.append(label+' rejected: '+p.stdout+p.stderr) + else:residue.close_case(label) +for failure in failures:print('FAIL: '+failure) +print(f'Owner gates: {len(cases)+len(controls)} process controls; {len(failures)} failures') +residue.report() +raise SystemExit(bool(failures)) diff --git a/test/validate.py b/test/validate.py index 00fd8fd..6307f17 100644 --- a/test/validate.py +++ b/test/validate.py @@ -1306,6 +1306,71 @@ def check_interop_points_to_the_estate_skills(): "HOW belongs to that skill, and this skill must name it rather than restate it") +def check_owner_board_and_release_guards(): + """The owner refuses malformed declarations and a bypassed tag ancestry gate. + + The board's first eight-column table declares identities. Its three-column + summary below references those same identities and is not a second declaration. + The portable grammar/release implementation is shared with the FW02 members. + """ + from member_gates import cells, label, release_errors + board = os.path.join(ROOT, "docs/evidence/backlog.md") + try: + lines = open(board, encoding="utf-8").read().splitlines() + except OSError: + fail("owner board missing or unreadable: docs/evidence/backlog.md") + lines = [] + header = ["id", "what", "source", "blast", "age", "effort", "p", "status"] + starts = [i for i, line in enumerate(lines) + if line.strip().startswith("|") and + [label(c).lower() for c in cells(line.strip())] == header] + if len(starts) != 1: + fail("owner board declaration header missing or ambiguous") + else: + start = starts[0] + end = start + 1 + while end < len(lines) and lines[end].strip().startswith("|"): + end += 1 + declared = {} + separator = cells(lines[start + 1].strip()) if start + 1 < end else [] + if len(separator) != len(header) or not all(re.fullmatch(r":?-{3,}:?", c) for c in separator): + fail("owner board declaration header needs its matching separator") + for number in range(start + 2, end): + row = cells(lines[number].strip()) + if len(row) != len(header) or not lines[number].strip().endswith("|"): + fail(f"owner board row width at line {number + 1}") + continue + # Historical grouped declarations and the explicitly qualified hub + # reference are distinct from the summary's repeated references. + identifier = label(row[0]) + namespace = "hub" if identifier.endswith(" (umbrella)") else "agent-stack" + if namespace == "hub": + identifier = identifier[:-11] + atoms = identifier.split(" + ") + if not all(re.fullmatch(r"[A-Z]{1,4}-(?:[0-9]+[a-z]?|[A-Z][0-9]*)", atom) for atom in atoms): + fail(f"owner board invalid ID at line {number + 1}") + continue + for atom in atoms: + key = (namespace, atom) + if key in declared: + fail(f"board duplicate ID {namespace}:{atom} at line {number + 1}; first at {declared[key]}") + declared[key] = number + 1 + if not declared: + fail("owner board has no declarations") + path = os.path.join(ROOT, ".github/workflows/release.yml") + try: + workflow = open(path, encoding="utf-8").read() + except OSError: + fail("owner release workflow missing or unreadable") + return + for error in release_errors(workflow): + fail(".github/workflows/release.yml: " + error) + ci = open(os.path.join(ROOT, ".github/workflows/validate.yml"), encoding="utf-8").read() + if not re.search(r"^\s*run: python3 test/member_owner_gates_test\.py\s*$", ci, re.M): + fail("owner gate plants missing from validate.yml") + + +check_owner_board_and_release_guards() check_ledger_tables_keep_their_shape() check_shipped_front_matter_survives_a_real_reader() check_interop_points_to_the_estate_skills() From 7aa4dd36518bc853013aed775ccfe18dcec98a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=B1=D0=BE=D1=82?= Date: Tue, 13 Oct 2026 06:32:24 +0200 Subject: [PATCH 2/3] fix: refuse board declarations orphaned by table boundaries --- docs/HANDOFF.md | 2 +- docs/evidence/backlog.md | 2 +- .../runs/2026-10-13-r30-owner-gates.md | 10 ++++++++++ test/member_owner_gates_test.py | 2 ++ test/validate.py | 20 +++++++++++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 39e17d3..adf28b4 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -3,7 +3,7 @@ Objective: close agent-stack's missing own-board/release-ancestry checks under the family's original R30. [Bounded packet and checks](evidence/runs/2026-10-13-r30-owner-gates.md) is the entry for the next agent. Branch `fix/r30-owner-gates-1013`, basecb8f699. -Eight baseline defects reproduced; candidate14 controls pass; aggregate review, +Eight baseline defects reproduced; candidate16 controls pass after two orphan-row review corrections; aggregate review, integration and publication remain pending. No installed payload changes. Exact next task: independent source/negative-control review, then full local gate diff --git a/docs/evidence/backlog.md b/docs/evidence/backlog.md index 9b3b122..ed0af63 100644 --- a/docs/evidence/backlog.md +++ b/docs/evidence/backlog.md @@ -48,7 +48,7 @@ row per line, appended, so two runs closing two rows do not land on the same lin | AG-06b | **The 2592 existing trees are reported and left alone.** `find "$TMPDIR" -maxdepth 4 -type f -path '*/copy/sub/b.sh' \| wc -l` → **2592** on 2026-08-20, of which **16 were added by this run's own verification** — one run of the leaking fixture before the port, and one of the v0.12.0 archive's copy of it. They are plain `tmpXXXXXXXX` and indistinguishable from every other program's, so they are named rather than swept. The identical fixture still leaks in **`seo-aeo-audit`**, which another agent holds; referred, not edited. | AG-06 port, 2026-08-20; plan R28 of the hub's 2026-10-11 integration waves | 2 | 0 | 1 | **2** | **closed in v0.27.1 (2026-10-12)** — the trees ARE identifiable by content, so they are reapable without claiming another program's directory: `python3 test/residue.py --reap-own` (dry run by default, `--apply` to remove) claims only an `mkdtemp()`-named, non-symlink child of the temp root holding exactly one of the fixture's five end-state file sets and its directory set byte for byte, plus optionally its own manifest, unchanged for 24 h; root, age floor and the removal window hardened after the PR #45 reading (verification.md, v0.27.1). `test/reap_own_test.py` 6/6 in a sandbox temp root; a planted permissive reaper (any tree holding `sub/b.sh`) was refused: `near-misses were claimed as own: ['tmpextrab01', 'tmpextraf01']`. Real `$TMPDIR`, dry, 2026-10-12: `0 tree(s) match`; the `find` count is 0 — the 2592 trees are already gone, and both leaking suites (`agent-stack`, `seo-aeo-audit`) now use `residue.workspace()`. Nothing in the real `$TMPDIR` was deleted | | B-159 | **`seo-aeo-audit` still has the AG-06a/B-158 hole.** Its temp-tree check reads less than the whole repository and misses aliased calls, and `test/negatives.py:195` makes a bare `tempfile.mkdtemp(prefix="tp-negatives-snap-")` outside any residue ledger (read on its `origin/main`, 2026-10-12). Port agent-stack 0.27.1's whole-repository scan, any-receiver pattern and exact-shape exemptions there, and route that snapshot through the ledger. | agent-stack PR #45 blind reading, 2026-10-12 | 1 | 0 | 1 | **1** | **open** — another repository; referred, not edited | | B-160 | **make-skill's temp-tree check reads `test/` only.** make-skill 0.31.0 has the any-receiver pattern but walks `test/` alone, so a maker in a shipped script is invisible there exactly as AG-06a described here. Port agent-stack 0.27.1's whole-repository scan with exact-path, exact-count, exact-shape exemptions back to make-skill. | agent-stack PR #45 blind reading, 2026-10-12 | 1 | 0 | 1 | **1** | **open** — another repository; referred, not edited | -| B-161 | **The owner validator accepted duplicate declarations and a disabled release ancestry check.** Eight real-validator plants passed on cb8f699; the same seam also hid an even-backslash Markdown pipe in AG-06b. | hub R30, 2026-10-13 | 2 | 0 | 1 | **2** | **open** — implementation candidate and14 process controls; independent review, integration and release pending. [Packet](runs/2026-10-13-r30-owner-gates.md) | +| B-161 | **The owner validator accepted duplicate declarations and a disabled release ancestry check.** Eight real-validator plants passed on cb8f699; the same seam also hid an even-backslash Markdown pipe in AG-06b. | hub R30, 2026-10-13 | 2 | 0 | 1 | **2** | **open** — implementation candidate and16 process controls; independent review, integration and release pending. [Packet](runs/2026-10-13-r30-owner-gates.md) | ## Open, and why diff --git a/docs/evidence/runs/2026-10-13-r30-owner-gates.md b/docs/evidence/runs/2026-10-13-r30-owner-gates.md index b15d552..588d1e6 100644 --- a/docs/evidence/runs/2026-10-13-r30-owner-gates.md +++ b/docs/evidence/runs/2026-10-13-r30-owner-gates.md @@ -53,3 +53,13 @@ Exact next task: independently review this candidate and its parser boundaries, then run the owner full gate once, promote a frozen draft PR, integrate normally and include it in a reviewed patch release. Do not advance the hub pin from this unreleased branch. + +## Independent review correction + +At63c54b9 the peer inserted a blank line or HTML comment before B-159 and +then duplicated its identity in B-160. Both real validators returned0: the +adapter stopped before the later rows. Two added process controls reproduced +these failures locally. The guard now refuses ID-shaped orphan rows outside +the primary declaration table, except the explicitly identified3column summary. +The expanded suite passes16 controls with16 owned trees removed. This is a +new source revision requiring peer readback; earlier14PASS is historical. diff --git a/test/member_owner_gates_test.py b/test/member_owner_gates_test.py index 41f2dfe..58f3a42 100644 --- a/test/member_owner_gates_test.py +++ b/test/member_owner_gates_test.py @@ -20,6 +20,8 @@ def duplicate(text): ('group overlaps later declaration', BOARD, lambda s: s.replace('| B-160 |', '| AG-04 |', 1), 'board duplicate ID'), ('even backslash pipe escape', BOARD, lambda s: s.replace('\\| wc -l', '\\\\| wc -l', 1), 'owner board row width'), ('missing separator', BOARD, lambda s: s.replace('|---|---|---|---|---|---|---|---|', '', 1), 'matching separator'), + ('orphan after blank', BOARD, lambda s: duplicate(s.replace('| B-159 |','\n| B-159 |',1)), 'owner board orphan declaration'), + ('orphan after comment', BOARD, lambda s: duplicate(s.replace('| B-159 |','\n| B-159 |',1)), 'owner board orphan declaration'), ('missing board', BOARD, lambda _: None, 'owner board missing'), ('missing header', BOARD, lambda s: s.replace('| id | What | Source |', '| key | What | Source |', 1), 'owner board declaration header'), ('missing ancestry', RELEASE, lambda s: s.replace('git merge-base --is-ancestor', 'git merge-base-removed --is-ancestor'), 'release ancestry guard'), diff --git a/test/validate.py b/test/validate.py index 6307f17..5702ab8 100644 --- a/test/validate.py +++ b/test/validate.py @@ -1357,6 +1357,26 @@ def check_owner_board_and_release_guards(): declared[key] = number + 1 if not declared: fail("owner board has no declarations") + # Blank/comment boundaries cannot make later declarations disappear. + # Only this explicitly identified summary table may reference identities + # outside the primary table. Other ID-shaped rows need a declared home. + summary = False + for number, line in enumerate(lines): + if start <= number < end: + continue + stripped = line.strip() + if not stripped.startswith("|"): + summary = False + continue + row = cells(stripped) + headings = [label(c).lower() for c in row] + if headings == ["id", "what", "why it is still open"]: + summary = True + continue + identity = label(row[0]) if row else "" + if re.match(r"[A-Z]{1,4}-(?:[0-9]+|[A-Z])", identity): + if not summary or len(row) != 3: + fail(f"owner board orphan declaration at line {number + 1}") path = os.path.join(ROOT, ".github/workflows/release.yml") try: workflow = open(path, encoding="utf-8").read() From 32823f980781743f979dea544eff949a36823b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=B1=D0=BE=D1=82?= Date: Tue, 13 Oct 2026 06:36:44 +0200 Subject: [PATCH 3/3] docs: record independent R30 review and full local gate --- docs/HANDOFF.md | 8 ++++---- .../evidence/runs/2026-10-13-r30-owner-full.json | 8 ++++++++ .../runs/2026-10-13-r30-owner-full.log.gz | Bin 0 -> 4112 bytes docs/evidence/runs/2026-10-13-r30-owner-gates.md | 13 +++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 docs/evidence/runs/2026-10-13-r30-owner-full.json create mode 100644 docs/evidence/runs/2026-10-13-r30-owner-full.log.gz diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index adf28b4..4208672 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -3,9 +3,9 @@ Objective: close agent-stack's missing own-board/release-ancestry checks under the family's original R30. [Bounded packet and checks](evidence/runs/2026-10-13-r30-owner-gates.md) is the entry for the next agent. Branch `fix/r30-owner-gates-1013`, basecb8f699. -Eight baseline defects reproduced; candidate16 controls pass after two orphan-row review corrections; aggregate review, -integration and publication remain pending. No installed payload changes. +Eight baseline defects reproduced; candidate16 controls pass after two orphan-row review corrections; independent review and local aggregate pass at7aa4dd3; +hosted integration and publication remain pending. No installed payload changes. -Exact next task: independent source/negative-control review, then full local gate -and a frozen draft-to-ready candidate. Preserve nightly CI and normal required +Exact next task: frozen draft-to-ready candidate with genuine hosted checks, +then normal exact-head merge. Preserve nightly CI and normal required checks. Commit/push and verify exact release source before hub re-pin/install. diff --git a/docs/evidence/runs/2026-10-13-r30-owner-full.json b/docs/evidence/runs/2026-10-13-r30-owner-full.json new file mode 100644 index 0000000..d4de620 --- /dev/null +++ b/docs/evidence/runs/2026-10-13-r30-owner-full.json @@ -0,0 +1,8 @@ +{ + "source": "7aa4dd36518bc853013aed775ccfe18dcec98a85", + "command": "npm test", + "exit_code": 0, + "log_sha256": "85d0cf29551ffe86b373c5a69c7a2131dcfff51ad64a1c48259b0342138faec9", + "log": "2026-10-13-r30-owner-full.log.gz", + "scope": "Local source validation; hosted integration and publication separate" +} diff --git a/docs/evidence/runs/2026-10-13-r30-owner-full.log.gz b/docs/evidence/runs/2026-10-13-r30-owner-full.log.gz new file mode 100644 index 0000000000000000000000000000000000000000..5b5b5a16502b5df12b66b2bca66aaeddf5689002 GIT binary patch literal 4112 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 Oh5rHJeL!iBDF6U-nb@=d literal 0 HcmV?d00001 diff --git a/docs/evidence/runs/2026-10-13-r30-owner-gates.md b/docs/evidence/runs/2026-10-13-r30-owner-gates.md index 588d1e6..c9dc05e 100644 --- a/docs/evidence/runs/2026-10-13-r30-owner-gates.md +++ b/docs/evidence/runs/2026-10-13-r30-owner-gates.md @@ -63,3 +63,16 @@ these failures locally. The guard now refuses ID-shaped orphan rows outside the primary declaration table, except the explicitly identified3column summary. The expanded suite passes16 controls with16 owned trees removed. This is a new source revision requiring peer readback; earlier14PASS is historical. + +## Final bounded review and local aggregate + +Independent peer review of7aa4dd36518bc853013aed775ccfe18dcec98a85 passed: +16process controls replayed, plus5 independent actual-validator negatives +(blank/comment orphan rows, grouped overlap, formatted duplicate and duplicate +header). The shared helper remained byte-equal to its cited canonical source. + +`npm test` then passed at that exact source:17 structural checks,9 plant-guard +cases,16 owner controls,13 reap cases,11 installer cases and all audit regressions. +[Full receipt](2026-10-13-r30-owner-full.json) and compressed output bind the +checked source. This later documentation update does not retroactively change +that source identity. Genuine exact-head hosted CI and normal merge are next.