From b8d66c7822092ececa2fdc2537ac4419fcff2ff3 Mon Sep 17 00:00:00 2001 From: agentrelaybot Date: Thu, 17 Sep 2026 16:11:19 -0700 Subject: [PATCH 1/2] port(drive-cloud): emit the tick as a v2 kernel spec The cloud tick is the last high-volume flow still submitted as Relayflow v1, and v1 launches are the only remaining traffic on the SQS/Lambda launch bridge: 693 of 1065 v1 runs over 7 days are this one flow, against 49 v2 runs total. Cloud already accepts `--relayflow-version v2` and routes those launches through the Cloudflare queue, so the Lambda cannot be retired until this flow has a v2 form. This translates rather than re-authors. gen-drive-cloud-v2.py reuses gen-drive-cloud.build(), so the cloud-specific shaping -- one cycle, a non-fatal verify, the commit/handoff steps -- keeps exactly one definition and drive.yaml stays the single source of truth. The mapping is small because the v2 authoring schema kept v1's camelCase for everything this tick uses: dependsOn, maxIterations, timeoutMs and verification are unchanged, gate shapes included. What moves is the envelope -- version to 0.1.0, the workflows wrapper flattened to steps, step.name to step.id, agent task to instruction, swarm.timeoutMs to budget.maxWallclockMs, and swarm.channel to the agent steps' stream surface. swarm.pattern: dag is dropped because dependsOn already is the dag. Two v1 concepts do not survive, both deliberately: - The `agents` roster is not emitted. v2's NamedAgentSpec requires both cli and model, and drive.yaml pins no model, so emitting a roster would mean inventing model pins here -- a behaviour change wearing a port's clothing. Each agent step carries its roster cli inline instead, which is what the already-ported drive-local.yaml does. `role` is prose the lead depends on, so it is prefixed onto the instruction rather than dropped; `preset` has no v2 equivalent and is gone. - timeoutMs on agent steps. v2 does not accept it, and drive.yaml's own comments record that the platform never enforced it, so agent steps are bounded by budget.maxWallclockMs -- which is the first bound that actually holds. Verified three ways. The generated spec passes @relayflows/schema, while drive-cloud.yaml fails it on exactly the four points this mapping addresses and the already-ported drive-local.yaml and restack-verify.yaml pass, so the validator discriminates. `--check` asserts step-for-step equivalence against the v1 document -- byte-identical shell commands, identical gates, dependsOn, maxIterations and cli -- and it was confirmed to fail on a dropped gate, a reflowed command, a changed cli, a dropped maxIterations, reordered steps and a missing channel surface. `--check` also fails on a stale or hand-edited file. Nothing is switched over here: ops/launch-gate.sh still submits drive-cloud.yaml on v1. Flipping it is a separate change, once this spec has carried a real run. --- ops/gen-drive-cloud-v2.py | 239 +++++++++++++++++++++++ workflows/drive-cloud-v2.yaml | 349 ++++++++++++++++++++++++++++++++++ 2 files changed, 588 insertions(+) create mode 100644 ops/gen-drive-cloud-v2.py create mode 100644 workflows/drive-cloud-v2.yaml diff --git a/ops/gen-drive-cloud-v2.py b/ops/gen-drive-cloud-v2.py new file mode 100644 index 000000000..5ef553d98 --- /dev/null +++ b/ops/gen-drive-cloud-v2.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Generate workflows/drive-cloud-v2.yaml from workflows/drive.yaml. + +The v2 kernel spec of the cloud tick. Cloud submits this with +`--relayflow-version v2`, which routes the launch through the Cloudflare +queue instead of the SQS/Lambda bridge, so retiring that Lambda needs this +file to exist and be equivalent to drive-cloud.yaml. + +This translates rather than re-authors: it reuses gen-drive-cloud.build(), +so the cloud-specific shaping (one cycle, non-fatal verify, commit/handoff) +has exactly one definition and drive.yaml stays the single source of truth. +Regenerate both together: + + python3 ops/gen-drive-cloud.py && python3 ops/gen-drive-cloud-v2.py + +The mapping is mechanical because the v2 authoring schema kept v1's +camelCase for everything the tick uses -- `dependsOn`, `maxIterations`, +`timeoutMs` and `verification` are unchanged, gates included. What moves: + + version: '1.0' -> '0.1.0' (kernel rejects other values) + workflows[0].steps -> steps (v2 has no workflows wrapper) + step.name -> step.id + agent step .task -> .instruction + swarm.timeoutMs -> budget.maxWallclockMs + swarm.channel -> agent step surfaces.streams[].stream + swarm.pattern: dag -> dropped; dependsOn already IS the dag + +Two v1 concepts have no v2 equivalent and are handled explicitly below: +the `agents` roster and its `preset`. +""" +import copy +import importlib.util +import sys + +import yaml + +SPEC_VERSION = "0.1.0" + +# Mirrors kernel/relayflowd-core/src/spec.rs STEP_*_FIELDS. The kernel +# rejects unknown step fields (SpecError::UnknownField) rather than ignoring +# them, so a field this translator forgets to map is a hard launch failure, +# not a silent behaviour change. Asserting the key sets here turns that into +# a generator-time error with the offending field named. +STEP_COMMON_FIELDS = { + "id", + "type", + "dependsOn", + "input", + "maxIterations", + "retry", + "verification", + "memory", + "requirements", +} +STEP_FIELDS_BY_TYPE = { + "deterministic": {"command", "timeoutMs", "lease_ms"}, + "llm": {"prompt", "model", "cli"}, + # No timeoutMs: agent steps are bounded only by budget.maxWallclockMs. + # That is not a regression from v1, where the comments in drive.yaml + # record that timeoutMs on agent steps was never enforced anyway. + "agent": { + "instruction", + "agent", + "cli", + "model", + "cwd", + "output", + "transport", + "recoveryMode", + "surfaces", + "permissions", + }, +} + + +def load_v1_cloud_doc(): + """The v1 cloud doc, straight from the existing generator.""" + spec = importlib.util.spec_from_file_location( + "gen_drive_cloud", "ops/gen-drive-cloud.py" + ) + module = importlib.util.module_from_spec(spec) + # Importing a sibling script would otherwise leave ops/__pycache__ behind + # on every run, which is untracked noise in `git status`. + previous = sys.dont_write_bytecode + sys.dont_write_bytecode = True + try: + spec.loader.exec_module(module) + finally: + sys.dont_write_bytecode = previous + return module.build() + + +def translate_step(v1_step, agents_by_name, channel): + step = copy.deepcopy(v1_step) + step_type = step.get("type") + + step["id"] = step.pop("name") + + if step_type == "agent": + step["instruction"] = step.pop("task") + # Inline the roster entry's cli instead of emitting an `agents` map. + # The v2 roster (NamedAgentSpec) requires BOTH cli and model, and + # drive.yaml declares no model -- emitting one would mean inventing a + # model pin for the lead and builder here, which is a behaviour change + # disguised as a port. An inline cli is what the already-ported + # workflows/drive-local.yaml does, and it carries v1's mapping exactly. + agent_name = step.pop("agent", None) + if agent_name is not None: + agent = agents_by_name.get(agent_name) + if agent is None: + raise SystemExit( + f"step {step['id']}: references undeclared agent {agent_name!r}" + ) + step["cli"] = agent["cli"] + # `preset` has no v2 equivalent. `role` does not either, but it is + # load-bearing prose -- it is how drive.yaml tells the lead it is + # the Lead -- so it is carried into the instruction rather than + # dropped. Prefixed, not appended: the instruction ends with the + # ASSESS_DONE contract that the verification gate reads. + role = agent.get("role") + if role: + step["instruction"] = f"{role}\n\n{step['instruction']}" + # v1's swarm.channel, per-step. Without this the agent is not a + # participant on a named stream and cannot be steered mid-run. + if channel: + step["surfaces"] = {"streams": [{"stream": channel}]} + # Not enforced in v1 and not accepted in v2. + step.pop("timeoutMs", None) + + allowed = STEP_COMMON_FIELDS | STEP_FIELDS_BY_TYPE.get(step_type, set()) + unknown = sorted(set(step) - allowed) + if unknown: + raise SystemExit( + f"step {step['id']} (type {step_type}): the kernel rejects " + f"unknown fields {unknown}; teach this translator to map them" + ) + return step + + +def build(): + v1 = load_v1_cloud_doc() + swarm = v1.get("swarm") or {} + agents_by_name = {a["name"]: a for a in v1.get("agents") or []} + + out = { + "version": SPEC_VERSION, + "name": v1["name"], + "description": v1["description"].replace( + "GENERATED from workflows/drive.yaml by ops/gen-drive-cloud.py.", + "GENERATED from workflows/drive.yaml by ops/gen-drive-cloud-v2.py.", + ), + } + + wallclock = swarm.get("timeoutMs") + if wallclock: + out["budget"] = {"maxWallclockMs": wallclock} + + out["steps"] = [ + translate_step(s, agents_by_name, swarm.get("channel")) + for s in v1["workflows"][0]["steps"] + ] + return out + + +def assert_equivalent_to_v1(v2, v1): + """The port must not change what the tick does. + + Schema validity is not the property that matters here -- a spec that + passes the schema but drops a gate, reorders the dag or rewrites a shell + command is a silently different flow. So this compares the two documents + field by field and fails on anything the mapping above does not explain. + """ + swarm = v1.get("swarm") or {} + agents_by_name = {a["name"]: a for a in v1.get("agents") or []} + v1_steps = v1["workflows"][0]["steps"] + + assert v2["version"] == SPEC_VERSION, v2["version"] + assert v2["name"] == v1["name"], v2["name"] + assert v2.get("budget", {}).get("maxWallclockMs") == swarm.get("timeoutMs") + # Same steps, same order: dependsOn encodes the dag, but order is what a + # reader diffs, and a reordered emit would hide a dropped step. + assert [s["id"] for s in v2["steps"]] == [s["name"] for s in v1_steps] + + for new, old in zip(v2["steps"], v1_steps): + where = f"step {new['id']}" + assert new["type"] == old["type"], where + assert new.get("dependsOn") == old.get("dependsOn"), where + assert new.get("maxIterations") == old.get("maxIterations"), where + # Gates are control flow. v1 and v2 share the gate shape exactly, so + # this is an identity check, not a translation. + assert new.get("verification") == old.get("verification"), where + + if old["type"] == "deterministic": + # Byte-identical: every one of these commands encodes a hard-won + # sandbox fact (exec bits, 413 flushes, stale trees). Reflowing + # one would be a behaviour change no reviewer would spot. + assert new["command"] == old["command"], where + assert new.get("timeoutMs") == old.get("timeoutMs"), where + elif old["type"] == "agent": + agent = agents_by_name[old["agent"]] + assert new["cli"] == agent["cli"], where + expected = old["task"] + role = agent.get("role") + if role: + expected = f"{role}\n\n{expected}" + assert new["instruction"] == expected, where + assert new["surfaces"]["streams"][0]["stream"] == swarm["channel"], where + assert "timeoutMs" not in new, where + + +if __name__ == "__main__": + doc = build() + path = "workflows/drive-cloud-v2.yaml" + + assert_equivalent_to_v1(doc, load_v1_cloud_doc()) + + if "--check" in sys.argv: + # Drift gate: a hand-edit, or a drive.yaml change landed without + # regenerating, must fail rather than be silently overwritten later. + with open(path) as f: + on_disk = yaml.safe_load(f) + if on_disk != doc: + raise SystemExit( + f"{path} is stale; run: python3 ops/gen-drive-cloud-v2.py" + ) + print(f"{path} is current and equivalent to drive-cloud.yaml", file=sys.stderr) + raise SystemExit(0) + + with open(path, "w") as f: + f.write( + "# GENERATED from workflows/drive.yaml by ops/gen-drive-cloud-v2.py.\n" + "# Do not hand-edit: change drive.yaml, then regenerate.\n" + ) + yaml.safe_dump(doc, f, sort_keys=False, width=100, default_flow_style=False) + agents = sum(1 for s in doc["steps"] if s["type"] == "agent") + print( + f"wrote {path} ({len(doc['steps'])} steps, {agents} agent)", + file=sys.stderr, + ) diff --git a/workflows/drive-cloud-v2.yaml b/workflows/drive-cloud-v2.yaml new file mode 100644 index 000000000..da7ef18a4 --- /dev/null +++ b/workflows/drive-cloud-v2.yaml @@ -0,0 +1,349 @@ +# GENERATED from workflows/drive.yaml by ops/gen-drive-cloud-v2.py. +# Do not hand-edit: change drive.yaml, then regenerate. +version: 0.1.0 +name: flows-drive-cloud +description: 'The Lead''s tick, shaped for a cloud sandbox with the laptop closed. + + A cloud sandbox has no git remote and no GitHub token, so this flow + + never delivers: it runs 1 full work-package cycles back to back + + in ONE sandbox, committing each to the sandbox branch. Recover the work + + with `agent-relay cloud sync `. Nothing reaches main without a + + human. GENERATED from workflows/drive.yaml by ops/gen-drive-cloud-v2.py. + + ' +budget: + maxWallclockMs: 3600000 +steps: +- type: deterministic + command: "# Materialize the repo; never assume it. This step assumed a clone\n# with an `origin` remote\ + \ and so every cloud tick died here with\n# `fatal: 'origin' does not appear to be a git repository`\ + \ (runs\n# 9fc8d996, ff35187a, 06505b94, 4cf36ea7, b33c2c9a).\n#\n# A cloud workflow sandbox does\ + \ NOT get a clone. The platform's own\n# materialization is the code sync: the CLI tars the `git ls-files`\n\ + # set and the bootstrap extracts it into the code mount, then runs\n# `git init` over it. Files yes,\ + \ `.git` history and remotes no.\n# A checkout with a remote only exists on a host that already has\ + \ one\n# (laptop, fleet node). Both shapes are supported below; neither is\n# assumed, and an unmaterialized\ + \ sandbox fails closed and typed\n# rather than failing later as an unexplained tool error.\nset -eu\n\ + echo \"SYNC_WORKDIR=$(pwd)\"\n\nmissing=\"\"\nfor required in AGENTS.md docs/RFC-0001-everything-is-a-relayflow.md\ + \ ops/DIRECTIVES.md kernel packages/sdk; do\n [ -e \"$required\" ] || missing=\"$missing $required\"\ + \ndone\nif [ -n \"$missing\" ]; then\n echo \"SYNC_FAIL_NOT_MATERIALIZED: the repo is not present\ + \ in this execution environment.\" >&2\n echo \" missing:$missing\" >&2\n echo \" cwd: $(pwd)\"\ + \ >&2\n echo \" A cloud run must upload the working tree: \\`agent-relay cloud run\\`\" >&2\n echo\ + \ \" syncs code by default; \\`--no-sync-code\\` produces exactly this state.\" >&2\n exit 78\n\ + fi\necho \"SYNC_MATERIALIZED=ok\"\n\n# Fail fast on a stale tree. A per-step sandbox can be seeded\ + \ from an\n# older orchestrator archive, and five consecutive runs burned ~20\n# minutes each producing\ + \ diffs that reverted merged work \u2014 a stale\n# tree diffed against fresh main looks like a wholesale\ + \ revert. The\n# guards at delivery caught them, but only after the cost was paid.\n#\n# ops/FORBIDDEN_PATHS\ + \ lists paths that must NOT exist. Their presence\n# here means this sandbox is not the tree we uploaded,\ + \ and nothing\n# built on it can be trusted.\nif [ -f ops/FORBIDDEN_PATHS ]; then\n stale=\"\"\n\ + \ while IFS= read -r forbidden; do\n case \"$forbidden\" in ''|\\#*) continue ;; esac\n [ -e\ + \ \"$forbidden\" ] && stale=\"$stale $forbidden\"\n done < ops/FORBIDDEN_PATHS\n if [ -n \"$stale\"\ + \ ]; then\n echo \"SYNC_FAIL_STALE_TREE: this sandbox contains paths that do not exist on the base:\"\ + \ >&2\n for p in $stale; do echo \" $p\" >&2; done\n echo \" The workspace was seeded from\ + \ an older archive, so it is not the tree\" >&2\n echo \" that was uploaded. A diff computed from\ + \ it reverts merged work.\" >&2\n echo \" Failing now rather than spending a full cycle to produce\ + \ an unusable diff.\" >&2\n exit 75\n fi\nfi\n\ngit rev-parse --git-dir >/dev/null 2>&1 || git\ + \ init -q\ngit config user.email \"lead@relayflows.local\"\ngit config user.name \"Relayflow Lead\"\ + \n\nif git remote get-url origin >/dev/null 2>&1; then\n # Real checkout (laptop / fleet node): take\ + \ the true origin/main.\n echo \"SYNC_MODE=remote\"\n git fetch --quiet origin\n git checkout --quiet\ + \ -B main origin/main\n base=$(git rev-parse --short origin/main)\nelse\n # Sandbox snapshot: there\ + \ is no remote to fetch and nothing to\n # rebase onto. The snapshot IS the base. Commit it so the\ + \ tick has\n # a parent to diff against \u2014 `git diff main` in the review step\n # needs a `main`\ + \ that exists.\n echo \"SYNC_MODE=snapshot\"\n if ! git rev-parse --verify --quiet HEAD >/dev/null\ + \ 2>&1; then\n git add -A\n git commit --quiet -m \"snapshot base for this tick\" || true\n\ + \ fi\n git branch --quiet -f main HEAD 2>/dev/null || git checkout --quiet -b main\n base=$(git\ + \ rev-parse --short HEAD)\nfi\n\ngit checkout --quiet -B \"flow/drive-${base}-$(date +%m%d%H%M)\"\n\ + echo \"SYNC_BASE=$base\"\necho \"SYNC_BRANCH=$(git rev-parse --abbrev-ref HEAD)\"\necho SYNCED\n" + id: sync +- type: agent + dependsOn: + - sync + verification: + type: output_contains + value: ASSESS_DONE + id: assess-1 + instruction: "The Relayflow Lead. Assesses state, plans one work package, reports honestly.\n\nYou are\ + \ the Relayflow Lead (charter/LEAD.md). Assess the repo.\n\nYOUR SCOPE IS THE TASK YOU WERE GIVEN.\ + \ Two launchers exist and they\ndeliver it differently: ops/launch-gate.sh commits an ops/TARGET.md\n\ + naming one gate, while the autodrive loop passes the task directly\nand writes NO TARGET.md. If ops/TARGET.md\ + \ is absent that is normal \u2014\nit is not missing context and there is nothing to go looking for.\n\ + \nEither way: QUOTE the scope into ops/NEXT.md, never cite the path.\nTARGET.md lives only in the\ + \ throwaway launch worktree and is NOT in\nthe delivered diff, so a reviewer sees a reference to a\ + \ file that\ndoes not exist. Review flagged that on PR #19 and again on #35, #40\nand #48 \u2014 it\ + \ is now enforced in verify, which REFUSES a NEXT.md that\ncites a path not present in the tree. Anything\ + \ you rely on must\nappear in the package itself.\n\nAnd when you state that something passes, paste\ + \ the literal command\nand its output. \"Three tests pass\" with no captured output is not a\nclaim\ + \ a reviewer can check, and it was also flagged on PR #19. This\nis AGENTS.md's central standard,\ + \ applied to your own reporting. It is the operator's scoping decision and it overrides your\nown\ + \ judgement about priority \u2014 several runs execute in parallel, each\npinned to a different gate,\ + \ and a run that wanders outside its target\nwill collide with a sibling. Stay inside it or, if the\ + \ target is\ngenuinely unreachable, say so in ops/NEEDS_HUMAN.md rather than\nsilently choosing different\ + \ work.\nThen read ops/STATE.md \u2014 it is ground truth about gates and open\nPRs for an environment\ + \ with no git history, and it names the known\nsandbox faults that are NOT reasons to block. Then\ + \ read\nops/DIRECTIVES.md \u2014 standing human directives outrank the backlog;\nif one is unsatisfied,\ + \ it IS the work package.\nThen read docs/bootstrap-report.md and ops/DRIVE-LOG.md if they exist,\n\ + `git log --oneline -15`, `gh pr list --state open` and open PR review\nstate, kernel/ and packages/sdk/\ + \ test status. Then write ops/NEXT.md: the\nSINGLE highest-priority work package toward the current\ + \ gate\n(gate 1 until its done-when in RFC-0001 \xA73 holds), with: objective,\nfiles in scope, definition\ + \ of done (must include passing commands),\nand what is explicitly OUT of scope for this tick. If\ + \ an open PR is\nawaiting fixes from review, the work package is fixing it \u2014 never\nstart new\ + \ work over unfinished work. If work is blocked on a human\ndecision, write ops/NEEDS_HUMAN.md stating\ + \ the exact question and the\noptions \u2014 and then STILL end with ASSESS_DONE.\n\nCOMMIT YOUR WORK\ + \ PACKAGE BEFORE YOU FINISH:\n git add -A && git commit -m \"assess: work package for this tick\"\ + \nEach step runs in its OWN sandbox and files reach the next step only\nthrough the executor's propagation,\ + \ which is lossy: on runs a2089144\nand 2560e02d your predecessor wrote ops/NEXT.md, said so truthfully,\n\ + and the file never arrived \u2014 one of those runs finished with a\nzero-file patch. Committing puts\ + \ the package in git history rather\nthan leaving it as a loose working-tree file. If the commit fails,\n\ + say so in your output rather than finishing silently. The assess-gate step\nbelow reads that file\ + \ and parks the run with a typed outcome.\nALWAYS end with ASSESS_DONE, blocked or not: this gate\ + \ cannot tell a\ndifferent final token from a crashed agent, so on run 54ebd998 the\nLead correctly\ + \ reported BLOCKED_NEEDS_HUMAN three times and was\nscored as failing three times. Saying you are\ + \ blocked is a result,\nnot a failure \u2014 but it must be said in the file, not the token.\n" + cli: claude + surfaces: + streams: + - stream: flows-drive-cloud +- type: deterministic + dependsOn: + - assess-1 + command: "# A typed park, not a crash. The assess step cannot express \"blocked\"\n# in its final token\ + \ (its gate only recognises ASSESS_DONE), so the\n# Lead writes ops/NEEDS_HUMAN.md instead and this\ + \ step reads it.\nset -u\n# An escalation is trusted only when TWO INDEPENDENT SIGNALS AGREE:\n# the\ + \ file exists AND this tick is what wrote it.\n#\n# Existence alone is not a signal. ops/NEEDS_HUMAN.md\ + \ was committed to\n# main on 2026-09-06 (082c62aa) and nothing in this repo has ever\n# deleted it\ + \ \u2014 no `rm`, no `git rm`, and `git log --diff-filter=D`\n# over that path is empty. ops/launch-gate.sh\ + \ builds each run's\n# worktree from origin/main and does not strip it, so every tick from\n# 2026-09-12\ + \ onward escalated here before doing any work, on a\n# question a human had already answered. PRs\ + \ #417, #420, #422, #424,\n# #426, #427 and #428 are seven consecutive cloud runs whose entire\n#\ + \ diff is this file and ops/NEXT.md, re-litigating the same conflict.\n# None merged. A full cloud\ + \ run was burned on each.\n#\n# A stale file must never be able to masquerade as a live escalation.\n\ + # \"This tick\" is the same test the ops/NEXT.md freshness check below\n# uses \u2014 a commit in\ + \ `main..HEAD` \u2014 widened by the uncommitted case,\n# because propagation between per-step sandboxes\ + \ is lossy and assess\n# may write the file and fail to commit it. Losing a live escalation\n# is\ + \ the worse error of the two, so an unproven-fresh file that is\n# dirty in the working tree still\ + \ parks the run.\n#\n# The default is to TRUST the escalation. Only a positive, SUCCESSFUL\n# answer\ + \ from git may downgrade it to stale, because \"git printed\n# nothing\" and \"git could not answer\"\ + \ look identical otherwise \u2014 and\n# a sandbox is exactly where git cannot answer. SYNC_MODE=snapshot\n\ + # runs `git init` over an extracted tarball, so a step that runs\n# before main exists, a missing\ + \ .git, or any git failure would\n# silently classify a LIVE escalation as stale and walk the builder\n\ + # straight past a human decision. That inverts the tradeoff above,\n# so an unprovable escalation\ + \ parks the run.\nif [ -f ops/NEEDS_HUMAN.md ]; then\n escalation=unprovable\n if git rev-parse\ + \ --git-dir >/dev/null 2>&1 \\\n && git rev-parse --verify --quiet main >/dev/null 2>&1; then\n\ + \ tick_log=$(git log --oneline main..HEAD -- ops/NEEDS_HUMAN.md 2>/dev/null)\n if [ $? -ne 0\ + \ ]; then\n escalation=unprovable\n elif [ -n \"$tick_log\" ]; then\n escalation=this_tick_committed\n\ + \ else\n tick_dirty=$(git status --porcelain -- ops/NEEDS_HUMAN.md 2>/dev/null)\n if\ + \ [ $? -ne 0 ]; then\n escalation=unprovable\n elif [ -n \"$tick_dirty\" ]; then\n \ + \ escalation=this_tick_uncommitted\n else\n escalation=stale\n fi\n fi\n \ + \ fi\n if [ \"$escalation\" = stale ]; then\n echo \"ASSESS_STALE_NEEDS_HUMAN_IGNORED: ops/NEEDS_HUMAN.md\ + \ exists but this tick did not write it.\"\n echo \" It is the committed record of an escalation\ + \ that has already been answered,\"\n echo \" not a live one, so it does not park this run. Delete\ + \ it from main once its\"\n echo \" question is resolved \u2014 a resolved escalation left in\ + \ the tree is a lie the\"\n echo \" next assessor has to spend a run disproving.\"\n else\n \ + \ if [ \"$escalation\" = unprovable ]; then\n echo \"ASSESS_ESCALATION_FRESHNESS_UNPROVABLE:\ + \ git could not say whether this tick\"\n echo \" wrote ops/NEEDS_HUMAN.md (no repo, no main,\ + \ or git failed). Failing safe and\"\n echo \" treating it as live: ignoring a real escalation\ + \ is the worse of the two errors.\"\n fi\n echo \"ASSESS_BLOCKED_NEEDS_HUMAN: the Lead escalated\ + \ a decision it cannot make ($escalation).\"\n echo \"--- ops/NEEDS_HUMAN.md ---\"\n cat ops/NEEDS_HUMAN.md\n\ + \ exit 75\n fi\nfi\nif [ ! -f ops/NEXT.md ]; then\n echo \"ASSESS_FAIL: no ops/NEXT.md \u2014\ + \ an assessment that named no work package did not assess\"\n exit 1\nfi\n# The assessment must have\ + \ WRITTEN this tick's package, not merely\n# left the previous one in place. On run 457a6102 assess\ + \ reported\n# \"The work package is written to ops/NEXT.md\" and the very next step\n# read the OLD\ + \ file \u2014 the logs carry the reason:\n# \"relayfile flush failed after the command succeeded\ + \ (exit 1);\n# a later agent step may see stale files\"\n# The builder then correctly refused to\ + \ invent scope, but only after\n# a whole build step had been spent. Catch it here instead: if\n#\ + \ ops/NEXT.md is identical to the base, the assessment did not land,\n# whoever is at fault.\n# Look\ + \ for the package in the working tree OR in a commit made this\n# tick. Propagation between per-step\ + \ sandboxes is lossy, so a package\n# that exists only as a loose file may not arrive; one committed\ + \ by\n# the assess step travels in git history instead.\nif git log --oneline main..HEAD -- ops/NEXT.md\ + \ 2>/dev/null | grep -q .; then\n echo \"ASSESS_PACKAGE_COMMITTED: found ops/NEXT.md change in this\ + \ tick's history\"\nelif git diff --quiet main -- ops/NEXT.md 2>/dev/null; then\n # Warn, do not\ + \ fail. This was fatal, and it killed four runs in six\n # while the loop produced nothing \u2014\ + \ a worse outcome than the risk\n # it guarded against.\n #\n # The risk it guarded was \"the builder\ + \ gets scope nobody wrote this\n # tick\". But scope does not actually come from ops/NEXT.md: it\ + \ comes\n # from ops/TARGET.md, which the launcher COMMITS into the uploaded\n # tree, so it is\ + \ present in every per-step sandbox and cannot be\n # lost to the propagation fault. NEXT.md refines\ + \ the target; it does\n # not define it.\n echo \"ASSESS_WARN_STALE_NEXT: ops/NEXT.md did not change\ + \ from the base commit.\"\n echo \" The assess step's package did not survive the step boundary\ + \ (a known\"\n echo \" platform fault: per-step sandboxes lose both loose files and git objects).\"\ + \n echo \" Proceeding, because ops/TARGET.md is committed in the tree and carries this\"\n echo\ + \ \" run's scope. The builder is not working blind \u2014 it is working from the\"\n echo \" target\ + \ rather than from a refinement of it.\"\n if [ -f ops/TARGET.md ]; then\n echo \"--- ops/TARGET.md\ + \ (the scope that did survive) ---\"\n head -8 ops/TARGET.md\n else\n echo \"ASSESS_FAIL_NO_SCOPE:\ + \ neither a fresh ops/NEXT.md nor an ops/TARGET.md.\"\n echo \" With no scope from either source\ + \ the builder WOULD be working blind.\"\n exit 1\n fi\nfi\n# A package with no definition of done\ + \ cannot be verified, and the\n# builder cannot honestly report BUILD_DONE against it.\n# A package\ + \ must be verifiable, but do not dictate its wording. This\n# check demanded the literal phrase \"\ + definition of done\" and so\n# rejected a CORRECT assessment three times on run 30475b25 \u2014 one\n\ + # that reported gate 2's primitives already complete and proposed\n# moving to gate 3, and was right\ + \ on both counts. A gate that\n# rejects true reports is as bad as one that accepts false ones.\n\ + #\n# Accept either shape: a runnable command (that is what \"verifiable\"\n# actually means), or an\ + \ explicit statement that this tick has no\n# buildable package.\nif grep -qiE \"definition of done|definition-of-done|done\ + \ when|done-when|acceptance criteria\" ops/NEXT.md \\\n || grep -qE \"(cargo|npm|node|sh|pytest)\ + \ [a-z]\" ops/NEXT.md \\\n || grep -qiE \"no buildable work|nothing to build|assessment only|gate\ + \ .* is (green|complete)\" ops/NEXT.md; then\n :\nelse\n echo \"ASSESS_FAIL_NO_DOD: ops/NEXT.md\ + \ names neither a runnable command nor a\"\n echo \" statement that this tick has no buildable package.\ + \ A work package that\"\n echo \" cannot be verified cannot be built against.\"\n exit 1\nfi\n\ + echo \"ASSESS_GATE_PASS ($(grep -m1 -oE 'WP-[0-9]+[^|]*' ops/NEXT.md || echo 'work package'))\"\n" + timeoutMs: 120000 + id: assess-gate-1 +- type: agent + dependsOn: + - assess-gate-1 + maxIterations: 3 + verification: + type: output_contains + value: BUILD_DONE + id: build-1 + instruction: "Implements the work package. Rust for kernel/, TypeScript for packages/sdk/.\n\nRead ops/NEXT.md,\ + \ AGENTS.md, and the relevant parts of\ndocs/RFC-0001-everything-is-a-relayflow.md. Implement exactly\ + \ that\nwork package \u2014 nothing more. Run the definition-of-done commands\nyourself and iterate\ + \ until they pass. Keep files small and\nsingle-purpose. End with BUILD_DONE only when the definition\ + \ of done\npasses locally; paste the passing output.\n" + cli: codex + surfaces: + streams: + - stream: flows-drive-cloud +- type: deterministic + dependsOn: + - build-1 + command: "# CLOUD VARIANT (generated): a FAILED verify is recorded and the\n# run continues. Nothing\ + \ is delivered from a sandbox, so a failure\n# here cannot ship; the next cycle's assess treats it\ + \ as the work\n# package. On a delivering environment verify stays fatal.\n# A gate that cannot fail\ + \ is not a gate. Never pipe a test command\n# into tail inside the status check: the pipeline's status\ + \ is tail's.\nset -u\nran=0; ok=0\n\n# Bound every long-running command, not just the suites. Run\n\ + # 6d045b23 sat in verify for 29+ minutes: its suites were bounded but\n# `cargo build` and `npm ci`\ + \ were not, so a cold sandbox installing a\n# toolchain and compiling from scratch had no ceiling\ + \ at all.\n# Bounding half the step is not bounding the step.\n#\n# timeoutMs is NOT enforced by the\ + \ platform \u2014\n# observed three times on 2026-08-28 (verify-1 at 31min against a\n# 20min bound,\ + \ review-1 at 36min against 30min, plus an unbounded\n# toolchain install). And the kernel suite now\ + \ contains a test that\n# intermittently hangs under sandbox timing:\n# an_entry_appended_during_watch_registration_is_delivered_exactly_once\n\ + # ran past 60s in cloud while passing locally in 0.54s. Without a\n# bound here, one hanging test\ + \ consumes the entire run budget.\nrun_bounded() {\n _label=\"$1\"; shift\n if command -v timeout\ + \ >/dev/null 2>&1; then\n timeout \"${VERIFY_SUITE_TIMEOUT:-900}\" \"$@\"\n elif command -v gtimeout\ + \ >/dev/null 2>&1; then\n gtimeout \"${VERIFY_SUITE_TIMEOUT:-900}\" \"$@\"\n else\n echo \"\ + VERIFY_WARN: no timeout(1); $_label runs unbounded\" >&2\n \"$@\"\n fi\n}\n\nif [ -d kernel ];\ + \ then\n # Invoke through `sh`: in a cloud sandbox this script was present\n # but not executable\ + \ (observed on run 4cf36ea7). Git tracks it as\n # mode 100755, so the exec bit is lost somewhere\ + \ in materialization\n # \u2014 which stage is NOT established, so no mechanism is claimed here.\n\ + \ # `sh