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..4208672 --- /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; 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: 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/backlog.md b/docs/evidence/backlog.md index b003bfa..ed0af63 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 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-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 0000000..5b5b5a1 Binary files /dev/null and b/docs/evidence/runs/2026-10-13-r30-owner-full.log.gz differ 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..c9dc05e --- /dev/null +++ b/docs/evidence/runs/2026-10-13-r30-owner-gates.md @@ -0,0 +1,78 @@ +# 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. + +## 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. + +## 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. 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