Compare commits
2 Commits
feat/scrip
...
feat/hatch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f69282cf | ||
|
|
65fc400fcd |
@@ -1,20 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: code-airouter-deepseekv4flash-tdd-repo-hatchmate
|
|
||||||
version: 1
|
|
||||||
description: "Airouter DeepSeek V4 Flash code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org."
|
|
||||||
|
|
||||||
# Inherits airouter context's label gate — only the airouter dispatcher
|
|
||||||
# advertises this composite (see dispatcher/poller._collect_supported_harnesses).
|
|
||||||
requires_labels: [airouter]
|
|
||||||
|
|
||||||
layers:
|
|
||||||
- context: deepseek-code-methodology/v1
|
|
||||||
- context: best-practices/v1
|
|
||||||
- context: airouter/v1
|
|
||||||
- context: gitea-ssh/v1
|
|
||||||
- context: gitea-ssh-hatchmate/v1
|
|
||||||
- context: agent-repo/v1
|
|
||||||
- context: tdd-protect/v1
|
|
||||||
- context: tdd-gate/v1
|
|
||||||
- capability: python-dev/v1
|
|
||||||
- capability: tdd-file-lock/v1
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: code-haiku-tdd-repo-hatchmate
|
|
||||||
version: 1
|
|
||||||
description: "Haiku code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org."
|
|
||||||
|
|
||||||
layers:
|
|
||||||
- context: code-methodology/v1
|
|
||||||
- context: anthropic-cloud-paul-oauth/v1
|
|
||||||
- context: gitea-ssh/v1
|
|
||||||
- context: gitea-ssh-hatchmate/v1
|
|
||||||
- context: agent-repo/v1
|
|
||||||
- context: tdd-protect/v1
|
|
||||||
- context: tdd-gate/v1
|
|
||||||
- capability: python-dev/v1
|
|
||||||
- capability: tdd-file-lock/v1
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: code-sonnet-tdd-repo-hatchmate
|
|
||||||
version: 1
|
|
||||||
description: "Sonnet code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org."
|
|
||||||
|
|
||||||
layers:
|
|
||||||
- context: code-methodology/v1
|
|
||||||
- context: anthropic-cloud-paul-oauth/v1
|
|
||||||
- context: gitea-ssh/v1
|
|
||||||
- context: gitea-ssh-hatchmate/v1
|
|
||||||
- context: agent-repo/v1
|
|
||||||
- context: tdd-protect/v1
|
|
||||||
- context: tdd-gate/v1
|
|
||||||
- capability: python-dev/v1
|
|
||||||
- capability: tdd-file-lock/v1
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: workflow-script-nocp-norepo
|
|
||||||
version: 1
|
|
||||||
description: "Internal composite for kind: script nodes with cp_write=false and NO effective agent_repo (typed-workflows WF-COMP-31e, planner item e993c9fa). Brings only the shared-variables filesystem context: no repo-readonly clone, no CP credential, and therefore ZERO secrets_required entries. That makes the node eligible for an M45 tenant namespace (the NS-12 gate rejects any ESO account secret) and for the M33 egress broker sidecar, which supplies the node's credentials through bindings. Not model-bearing; the compiled shape is cli=\"shell\", params.cmd=<cmd>."
|
|
||||||
|
|
||||||
# Counterpart of workflow-script-nocp/v1 for templates that declare no agent_repo.
|
|
||||||
# The compiler (controlplane/script_composites.py resolve_script_composite) selects
|
|
||||||
# this only when effective_repo is False; with AGENT_REPO_URL unset the entrypoint
|
|
||||||
# V2 guard does not require /workspace/project. Never layer repo-readonly/v1 here:
|
|
||||||
# its gitea-ssh-readonly secret is exactly what this composite exists to avoid.
|
|
||||||
layers:
|
|
||||||
# spec/workflow-variables.md WV-19 / spec/harness.md HD-14 — provides the
|
|
||||||
# [shared-variables] tag.
|
|
||||||
- context: shared-variables/v1
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: workflow-script-nocp
|
|
||||||
version: 1
|
|
||||||
description: "Internal composite for kind: script nodes with cp_write=false (typed-workflows WF-COMP-31b, Option C). Brings the shared-variables filesystem context and the read-only project-repo clone, and NO CP credential: it layers neither cp-service-token/v1 (the AU-51 internal bearer) nor cp-write-token/v1 (the AU-55 per-step write token). Its only forge credential is repo-readonly/v1's read-only deploy key. Not model-bearing; the compiled shape is cli=\"shell\", params.cmd=<cmd>."
|
|
||||||
|
|
||||||
# Top-down order (WF-COMP-31b / WF-COMP-34): shared-variables first, repo-readonly
|
|
||||||
# LAST so its clone runs after the variable wiring and before the script starts.
|
|
||||||
# Never gitea-ssh/v1 (bug 45a48685). repo-readonly/v1 cannot be dropped: the
|
|
||||||
# entrypoint V2 guard trips when AGENT_REPO_URL is set and /workspace/project is
|
|
||||||
# missing, and every shipped script template declares agent_repo.
|
|
||||||
layers:
|
|
||||||
# spec/workflow-variables.md WV-19 / spec/harness.md HD-14 — agent-facing
|
|
||||||
# /variables/ context; provides the [shared-variables] tag.
|
|
||||||
- context: shared-variables/v1
|
|
||||||
# spec/typed-workflows.md WF-COMP-34 — read-only project-repo clone.
|
|
||||||
- context: repo-readonly/v1
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: workflow-script-write
|
|
||||||
version: 1
|
|
||||||
description: "Internal composite for kind: script nodes with cp_write=true (typed-workflows WF-COMP-31c, Option C). Brings the shared-variables filesystem context, the AU-55 per-step CP write token (cp-write-token/v1 sets CP_WRITE_TOKEN_FILE; the dispatcher delivers the wfcw_ file into the script container only, AU-55b) and the read-only project-repo clone. Omits cp-service-token/v1: a cp_write=true container gets the AU-55 token only, never the AU-51 internal bearer. Not model-bearing; the compiled shape is cli=\"shell\", params.cmd=<cmd>."
|
|
||||||
|
|
||||||
# Top-down order (WF-COMP-31c / WF-COMP-34): shared-variables, cp-write-token,
|
|
||||||
# repo-readonly LAST. Never gitea-ssh/v1 (bug 45a48685) and never
|
|
||||||
# cp-service-token/v1.
|
|
||||||
layers:
|
|
||||||
# spec/workflow-variables.md WV-19 / spec/harness.md HD-14.
|
|
||||||
- context: shared-variables/v1
|
|
||||||
# spec/authentication.md AU-55 / AU-55b — env-only layer; exports
|
|
||||||
# CP_WRITE_TOKEN_FILE=/run/agent/cp-write-token/token.
|
|
||||||
- context: cp-write-token/v1
|
|
||||||
# spec/typed-workflows.md WF-COMP-34 — read-only project-repo clone.
|
|
||||||
- context: repo-readonly/v1
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: workflow-script
|
|
||||||
version: 1
|
|
||||||
description: "Internal composite for the flag-on kind: script path (typed-workflows WF-COMP-31 / workflow-variables WV-19). Brings the shared-variables filesystem context AND the AU-51 CP write credential (cp-service-token/v1 sets CP_SERVICE_TOKEN_FILE in the entrypoint process env before shell_runner runs). NOT a model-bearing composite — the kind: script compiled shape is cli=\"shell\", params.cmd=<cmd>; this composite provides the harness layers that path needs and nothing else."
|
|
||||||
|
|
||||||
# Order matters only for HC-6 env last-writer-wins / HC-8 git identity. The two
|
|
||||||
# layers here have disjoint env vars (shared-variables sets WORKFLOW_VARIABLES_*
|
|
||||||
# paths, cp-service-token sets CP_SERVICE_TOKEN_FILE) and no overlapping git
|
|
||||||
# identity, so the listed order has no observable merge effect — both layers
|
|
||||||
# reach the script-node container either way.
|
|
||||||
layers:
|
|
||||||
# spec/workflow-variables.md WV-19 / spec/harness.md HD-14 — agent-facing
|
|
||||||
# /variables/ context (CLAUDE.md mounted at /opt/harness/context/shared-variables/CLAUDE.md)
|
|
||||||
# plus WORKFLOW_VARIABLES_ROOT=/variables and templated WORKFLOW_VARIABLES_SELF=<step_path>.
|
|
||||||
# Provides the [shared-variables] tag used by the dispatcher's extra_layers injection.
|
|
||||||
- context: shared-variables/v1
|
|
||||||
# spec/typed-workflows.md WF-COMP-31 "AU-51 credential env seam" — this layer's
|
|
||||||
# init.sh restages the ESO-mounted CP-service-token secret (root-owned mode 0400)
|
|
||||||
# to an agent-owned 0600 copy at /run/agent/cp-service-token/token and exports
|
|
||||||
# CP_SERVICE_TOKEN_FILE pointing at it. Verified: env.CP_SERVICE_TOKEN_FILE is set
|
|
||||||
# on the entrypoint process environment before shell_runner runs (entrypoint reads
|
|
||||||
# os.environ via env=os.environ.copy() in subprocess.run at entrypoint/runners/shell.py:73,
|
|
||||||
# so the same credential channel an agent node gets reaches a script node unchanged).
|
|
||||||
- context: cp-service-token/v1
|
|
||||||
# spec/typed-workflows.md WF-COMP-34 (bug 3bee78fa, 2026-10-02) — read-only
|
|
||||||
# project-repo clone for harness-path kind: script nodes. Layered LAST so
|
|
||||||
# its clone runs after the two AU-51 wiring layers and before the script
|
|
||||||
# starts (init.sh order: shared-variables -> cp-service-token -> repo-readonly).
|
|
||||||
# When AGENT_REPO_URL is unset (the pre-fix runtime, or any template that
|
|
||||||
# does not declare agent_repo) this layer is a no-op; the V2 guard
|
|
||||||
# (entrypoint/entrypoint.py:901-906) does NOT trip because the unset case
|
|
||||||
# is a no-op there too. Layered-in instead of layering agent-repo/v1
|
|
||||||
# because that context's fork management and push side effects are wrong
|
|
||||||
# for a read-only script node (see bug item body).
|
|
||||||
- context: repo-readonly/v1
|
|
||||||
@@ -110,7 +110,7 @@ if [ -n "${AGENT_REPO_URL:-}" ] && [ -n "${AGENT_BRANCH:-}" ]; then
|
|||||||
# finalize.sh to merge into ci_metadata.json (AR-25). Falls back
|
# finalize.sh to merge into ci_metadata.json (AR-25). Falls back
|
||||||
# to the legacy inline AR-14a logic if the CLI is unavailable.
|
# to the legacy inline AR-14a logic if the CLI is unavailable.
|
||||||
mkdir -p /workspace/.agent-output
|
mkdir -p /workspace/.agent-output
|
||||||
AR39_OUT=$(python3 /opt/agent/agent_repo_base.py select-base \
|
AR39_OUT=$(python3 -m entrypoint.agent_repo_base select-base \
|
||||||
--workdir /workspace/project \
|
--workdir /workspace/project \
|
||||||
--branch "$AGENT_BRANCH" \
|
--branch "$AGENT_BRANCH" \
|
||||||
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \
|
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
kind: context
|
|
||||||
name: cp-write-token
|
|
||||||
version: 1
|
|
||||||
description: "Per-step scoped CP write token (AU-55, Option C): points CP_WRITE_TOKEN_FILE at the wfcw_ bearer the dispatcher delivers into the script container only (spec/typed-workflows.md WF-COMP-31c, spec/authentication.md AU-55b)"
|
|
||||||
requires: []
|
|
||||||
provides: []
|
|
||||||
|
|
||||||
# AU-55b delivery: the token does NOT exist at manifest-submit time, so it is
|
|
||||||
# NOT a secrets_required / ESO mount and NOT under /variables/. The dispatcher
|
|
||||||
# mints it at dispatch (mint_step_tokens, scope='cp_write') and delivers it
|
|
||||||
# into the script container ONLY:
|
|
||||||
# - Kubernetes: item `cp-write-token` of the per-task wfvars-<task_id>-<gen8>
|
|
||||||
# Secret, projected at the path below (mode 0440 + pod fsGroup, so the
|
|
||||||
# non-root agent uid can read it).
|
|
||||||
# - Docker: create -> put_archive -> start on the script container (never the
|
|
||||||
# wfvars-* data volume).
|
|
||||||
# This layer therefore carries env only: no init script, no secrets_required,
|
|
||||||
# no files. It must stay that way; a secrets_required entry here would make the
|
|
||||||
# dispatcher wait on an ESO secret that never exists.
|
|
||||||
#
|
|
||||||
# The token is only valid on the two AU-57 routes for the run's own item
|
|
||||||
# (PATCH / GET /v1/work-items/{id}). It is NOT the AU-51 internal bearer;
|
|
||||||
# cp-service-token/v1 is deliberately not layered alongside it.
|
|
||||||
env:
|
|
||||||
CP_WRITE_TOKEN_FILE: /run/agent/cp-write-token/token
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
kind: context
|
|
||||||
name: repo-readonly
|
|
||||||
version: 1
|
|
||||||
description: "Read-only project-repo clone for harness-path kind: script nodes (spec/typed-workflows.md WF-COMP-34, bug 3bee78fa). Clones AGENT_REPO_URL (the upstream project repo URL emitted by the WF-COMP-34 compiler emission as payload.agent_repo.url and propagated to the container env by the dispatcher) into /workspace/project at its default branch. No fork management, no AR-39 select-base, no scratchpad directory, no .gitignore munging, no finalize.sh push. The script node reads repo files only; it never writes back through a fork. Distinct from agent-repo/v1 (the agent-node branch-per-task fork-management context) on every one of those axes."
|
|
||||||
requires: []
|
|
||||||
provides: []
|
|
||||||
|
|
||||||
# HC-11: read-only layer. No push target; the credential below is a read-only
|
|
||||||
# deploy key (Gitea key id 20 on skynet/agent-runtimes), so a script node
|
|
||||||
# cannot write even if its code tries (bug 45a48685).
|
|
||||||
write_targets: []
|
|
||||||
|
|
||||||
# WF-COMP-34: this layer's init.sh is the read-only clone. It only runs
|
|
||||||
# when AGENT_REPO_URL is set; the unset case is a no-op (a script node
|
|
||||||
# whose template omits agent_repo keeps its pre-fix silent empty workspace
|
|
||||||
# runtime exactly, verified by
|
|
||||||
# tests/test_entrypoint.py::TestV2WorkspaceAssertions::test_v2_no_agent_repo_url_skips_workspace_check).
|
|
||||||
#
|
|
||||||
# AGENT_WORKING_DIR=/workspace/project is the env var
|
|
||||||
# entrypoint.py:_apply_working_dir reads (entrypoint/entrypoint.py:908-916),
|
|
||||||
# and the path entrypoint/runners/shell.py:67 sets as the subprocess cwd
|
|
||||||
# when context["workspace"] is unset and no clone action ran.
|
|
||||||
env:
|
|
||||||
AGENT_WORKING_DIR: /workspace/project
|
|
||||||
|
|
||||||
scripts:
|
|
||||||
init: "./init.sh"
|
|
||||||
|
|
||||||
ssh_hosts:
|
|
||||||
- alias: gitea.oreillyit.nz-ai-enablement
|
|
||||||
hostname: gitea.oreillyit.nz
|
|
||||||
user: git
|
|
||||||
identity_secret: GITEA_SSH_KEY
|
|
||||||
|
|
||||||
secrets_required:
|
|
||||||
- name: gitea-ssh-readonly
|
|
||||||
account_ref: "platform-readonly.space:platform"
|
|
||||||
mount_path: /run/agent/secrets/gitea-ssh-readonly
|
|
||||||
mode: "0400"
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# repo-readonly init — read-only project-repo clone for harness-path
|
|
||||||
# kind: script nodes (spec/typed-workflows.md WF-COMP-34, bug 3bee78fa).
|
|
||||||
#
|
|
||||||
# Threat model: the script node is invoked by entrypoint/runners/shell.py
|
|
||||||
# with cwd=context["workspace"]; the existing entrypoint paths set
|
|
||||||
# context["workspace"] to /workspace/project from AGENT_WORKING_DIR
|
|
||||||
# (the env var this init.sh exports via its harness.yaml env field).
|
|
||||||
# The init.sh MUST clone AGENT_REPO_URL (the upstream project repo URL
|
|
||||||
# emitted by the WF-COMP-34 compiler emission as payload.agent_repo.url
|
|
||||||
# and propagated to the container env by the dispatcher) into
|
|
||||||
# /workspace/project at its default branch — read-only, no fork, no push,
|
|
||||||
# no AR-39 select-base, no scratchpad, no .gitignore munging.
|
|
||||||
#
|
|
||||||
# Bug discovered live 2026-10-02 (bug 3bee78fa, run
|
|
||||||
# 9a368e71-d6ab-4185-a98c-9b31d41bb4b5): without this layer, every
|
|
||||||
# harness-path kind: script node ran with /workspace/project missing,
|
|
||||||
# so any preflight that read repo files (e.g. preflight-test-collection
|
|
||||||
# invoking pytest --collect-only tests/test_k8s_gc.py) saw an empty cwd
|
|
||||||
# and either reported zero files or failed with the upstream tool's own
|
|
||||||
# misconfig error. The WF-COMP-34 compiler emission (this repo's
|
|
||||||
# controlplane/typed_workflows/compiler.py, bug 3bee78fa) writes
|
|
||||||
# payload.agent_repo onto every harness-path script node's manifest, so
|
|
||||||
# the URL reaches the env when the template (or the node) declares
|
|
||||||
# agent_repo. This init.sh is the other half — the layer that actually
|
|
||||||
# clones it.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "=== repo-readonly/v1 init.sh ==="
|
|
||||||
|
|
||||||
# WF-COMP-34 unset-case contract: if AGENT_REPO_URL is unset, this is a
|
|
||||||
# no-op. The script node keeps its pre-fix silent empty workspace
|
|
||||||
# runtime exactly — a template that omits agent_repo does not get a
|
|
||||||
# surprise clone. The entrypoint V2 guard (entrypoint/entrypoint.py:901-906)
|
|
||||||
# only fires when AGENT_REPO_URL IS set; the unset case is a no-op on
|
|
||||||
# both sides (no clone here, no V2 trip there), so the legacy
|
|
||||||
# script_on_harness=False path's observable behaviour is preserved
|
|
||||||
# verbatim.
|
|
||||||
if [ -z "${AGENT_REPO_URL:-}" ]; then
|
|
||||||
echo "AGENT_REPO_URL unset; no clone (WF-COMP-34 unset-case contract)."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If the clone target already has content (e.g. a previous layer cloned
|
|
||||||
# into it), skip — never clobber. The check is on NON-EMPTY, not on
|
|
||||||
# existence: entrypoint/harness_init.py always pre-creates an empty
|
|
||||||
# /workspace/project, so a bare -d test skipped the clone every time
|
|
||||||
# (bug eede60c3). git clone into an existing empty directory is valid.
|
|
||||||
if [ -d /workspace/project ] && [ -n "$(ls -A /workspace/project 2>/dev/null)" ]; then
|
|
||||||
echo "/workspace/project already populated; skipping clone."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stage the ESO-mounted read-only deploy key where the harness-init
|
|
||||||
# ~/.ssh/config IdentityFile expects it (same destination gitea-ssh/v1 used;
|
|
||||||
# that layer is no longer part of workflow-script/v1 — bug 45a48685).
|
|
||||||
SSH_KEY_SRC="/run/agent/secrets/gitea-ssh-readonly/private_key"
|
|
||||||
SSH_KEY_DST="/home/agent/.ssh/gitea-oreillyit-nz-ai-enablement"
|
|
||||||
if [ ! -r "$SSH_KEY_SRC" ]; then
|
|
||||||
echo "ERROR: read-only SSH key not found at $SSH_KEY_SRC — ESO ExternalSecret not Ready?" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p /home/agent/.ssh
|
|
||||||
install -m 0600 -o agent -g agent "$SSH_KEY_SRC" "$SSH_KEY_DST"
|
|
||||||
|
|
||||||
mkdir -p /workspace
|
|
||||||
|
|
||||||
echo "Cloning (read-only): $AGENT_REPO_URL -> /workspace/project"
|
|
||||||
# Read-only clone: no checkout of a task branch, no push target, no
|
|
||||||
# remote operations beyond the initial clone. --depth 1 is enough —
|
|
||||||
# a script node reads files, not history. If history is needed later,
|
|
||||||
# the spec can be amended; today every preflight is a pytest --collect-only
|
|
||||||
# or a prettier --check or similar that does not need commit ancestry.
|
|
||||||
if git clone --depth 1 "$AGENT_REPO_URL" /workspace/project; then
|
|
||||||
echo "Cloned $AGENT_REPO_URL to /workspace/project"
|
|
||||||
else
|
|
||||||
# Loud failure: a harness-path script node that declares agent_repo
|
|
||||||
# and finds the clone fails SHOULD fail the task at init time, not
|
|
||||||
# silently leave /workspace/project missing for the V2 guard to
|
|
||||||
# catch 26 seconds later (that failure mode is the symptom this
|
|
||||||
# bug was filed for).
|
|
||||||
echo "ERROR: Failed to clone $AGENT_REPO_URL" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Strip any symlinks (security: prevent /proc/1/environ exfiltration;
|
|
||||||
# mirrors agent-repo/v1's defence).
|
|
||||||
find /workspace/project -type l -exec rm {} \; 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "AGENT_WORKING_DIR=${AGENT_WORKING_DIR:-/workspace/project}"
|
|
||||||
echo "=== repo-readonly/v1 init.sh complete ==="
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# Shared Variables Context
|
|
||||||
|
|
||||||
Your container is running with the **shared variables filesystem** mounted at `/variables/`.
|
|
||||||
This is the only way you read a step's inputs and return its outputs to the control
|
|
||||||
plane — there is no API to call, no credential to hold, no env var to substitute.
|
|
||||||
You write plain files under your own step folder; the CP picks them up.
|
|
||||||
|
|
||||||
Normative source: `spec/workflow-variables.md` (WV-1..6a). This document summarises
|
|
||||||
the contract; when in doubt, the spec wins.
|
|
||||||
|
|
||||||
## What is where
|
|
||||||
|
|
||||||
```
|
|
||||||
/variables/
|
|
||||||
workflow/ # run inputs, frozen at dispatch time (WV-8)
|
|
||||||
run-id
|
|
||||||
template
|
|
||||||
item.uuid
|
|
||||||
item.title
|
|
||||||
item.label
|
|
||||||
item.flow-state
|
|
||||||
artifacts.<key> # per WorkflowInput.required_artifacts
|
|
||||||
<step>/ # this step's OWN folder — write outputs here
|
|
||||||
<loop-id>_<n>/<step>/ # loop body node in iteration n (n is 1-based)
|
|
||||||
.self # path to your own step folder (read-only)
|
|
||||||
.done # completion marker, written by the entrypoint
|
|
||||||
```
|
|
||||||
|
|
||||||
You only write under your OWN folder. The pack sidecar walks that folder after you
|
|
||||||
exit and returns its contents to the CP. Earlier loop iterations' results and your
|
|
||||||
direct `depends_on` dependencies' outputs are also under `/variables/`, pre-filled
|
|
||||||
when your container started — you only READ those.
|
|
||||||
|
|
||||||
## Two env vars are set for you
|
|
||||||
|
|
||||||
- `WORKFLOW_VARIABLES_ROOT` — always `/variables`.
|
|
||||||
- `WORKFLOW_VARIABLES_SELF` — the path to your own step folder
|
|
||||||
(e.g. `/variables/judge` for a top-level node named `judge`, or
|
|
||||||
`/variables/review-loop_2/judge` for loop iteration 2's `judge` node).
|
|
||||||
|
|
||||||
Neither ever contains a value. Both are paths only. Treat both as read-only inputs.
|
|
||||||
|
|
||||||
## Key naming (the contract you'll be checked against)
|
|
||||||
|
|
||||||
- A variable's key is the joined, lowercased path from `/variables/` to the leaf
|
|
||||||
file, with `.` as the separator.
|
|
||||||
`/variables/judge/VERDICT` ↔ `judge.verdict`.
|
|
||||||
`/variables/review-loop_2/judge/findings/HIGH` ↔ `review-loop_2.judge.findings.high`.
|
|
||||||
- **Leaf files are UPPERCASE** on disk by convention; folders stay lowercase.
|
|
||||||
Lookup via the helpers and the CP API is case-insensitive.
|
|
||||||
- Each path segment, after lowercasing, must match `^[a-z0-9][a-z0-9_-]{0,63}$`.
|
|
||||||
Maximum depth is 8 segments. Dotfiles, symlinks, FIFOs, sockets, and devices are
|
|
||||||
skipped with a warning (never read or followed).
|
|
||||||
- Two leaf files at the same level whose names lowercase to the same key → both
|
|
||||||
rejected at pack time as `invalid_output_key`.
|
|
||||||
- **Reserved names** that cannot be used as step or loop ids (case-insensitive):
|
|
||||||
`workflow`, `while`, `for`, `foreach`, `loop`, `until`, `do`, `repeat`, `if`,
|
|
||||||
`else`, `elif`, `elseif`, `then`, `switch`, `case`, `default`, `when`, `break`,
|
|
||||||
`continue`, `return`, `goto`, `yield`, `try`, `catch`, `except`, `finally`,
|
|
||||||
`raise`, `throw`, `and`, `or`, `not`, `xor`, `in`, `is`, `true`, `false`,
|
|
||||||
`null`, `none`, `nil`, `yes`, `no`, `on`, `off`. The full list lives in
|
|
||||||
`controlplane/typed_workflows/reserved_ids.py` and may grow by spec amendment.
|
|
||||||
- Step/loop ids are restricted to lowercase letters, digits, `-` and `_`, starting
|
|
||||||
with a letter; no trailing `_<digits>` (reserved for loop-iteration suffixes).
|
|
||||||
|
|
||||||
## What you can see (visibility)
|
|
||||||
|
|
||||||
- `workflow.*` — always. These are the run inputs, frozen for the life of the run
|
|
||||||
(later item changes are not reflected here; pass them on through step outputs).
|
|
||||||
- The outputs of each of your **direct** `depends_on` nodes (the same set the
|
|
||||||
manifest DAG wires you to) — not transitive.
|
|
||||||
- Inside a loop body, **every prior iteration's** outputs for **every** step in
|
|
||||||
the loop — not just the immediately preceding one. So iteration 2 sees what
|
|
||||||
iterations 1, 2, … of every loop body node wrote. Iteration 1 sees only its
|
|
||||||
direct dependencies.
|
|
||||||
- Nothing else. Your own folder is always created, even if empty.
|
|
||||||
|
|
||||||
## Declared outputs
|
|
||||||
|
|
||||||
If your node's template declares `outputs: [<key>, ...]`, those are the keys you
|
|
||||||
are expected to write. Any file actually present under your step folder is
|
|
||||||
accepted regardless — `outputs:` is for policy and tooling, not an allowlist.
|
|
||||||
A missing declared output produces a warning by default (configurable per-project
|
|
||||||
to fail the step with `variables_missing_output`).
|
|
||||||
|
|
||||||
## Values
|
|
||||||
|
|
||||||
- **Strings only.** JSON is supported as a string value — you parse it yourself;
|
|
||||||
the CP never interprets a value's structure.
|
|
||||||
- Per-value cap is configurable (`max_value_bytes`, default 1 MiB). Per-step cap
|
|
||||||
(`max_step_bytes`) and per-step file count (`max_step_files`) also apply.
|
|
||||||
- Values are data — never substituted into prompts, commands, or environment
|
|
||||||
variables by the CP. Don't put values in shell; read them through the helpers
|
|
||||||
or the CLI.
|
|
||||||
|
|
||||||
## Helpers (use these, not shell)
|
|
||||||
|
|
||||||
A Python package is pre-installed in your image at `/opt/agent/variables/` and a
|
|
||||||
CLI at `/usr/local/bin/wfvar`. Both are stdlib-only.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from variables import load, save, keys
|
|
||||||
|
|
||||||
value = load("verdict") # load from your own folder
|
|
||||||
save("verdict", "CLEAN") # write into your own folder
|
|
||||||
keys() # list keys under your own folder
|
|
||||||
keys(prefix="judge.") # narrow by prefix (read-only access)
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wfvar get VERDICT # print the value, NO trailing newline
|
|
||||||
wfvar set VERDICT CLEAN # write into your own folder
|
|
||||||
wfvar list # list your own keys
|
|
||||||
wfvar path # print your own step folder (same as $WORKFLOW_VARIABLES_SELF)
|
|
||||||
wfvar get judge.findings.high # read another step's output (visibility permitting)
|
|
||||||
```
|
|
||||||
|
|
||||||
Exit codes: `0` ok, `1` not found (key is valid but absent), `2` invalid key or
|
|
||||||
usage, `3` value exceeds per-value limit, `4` write attempted outside your own
|
|
||||||
step. Do not wrap `wfvar get` with extra shell that strips newlines — it prints
|
|
||||||
exactly the stored bytes.
|
|
||||||
|
|
||||||
**Don't write plan-like content with `wfvar set <key> <body>`** — argv breaks on
|
|
||||||
shell quoting and truncates at ARG_MAX. For multi-line / quoting-sensitive bodies
|
|
||||||
(e.g. the decompose plan files at `/variables/<node>/items/<local_id>`), use your
|
|
||||||
file-write tool or a heredoc with a terminator that cannot occur in the body,
|
|
||||||
writing directly into `/variables/<node>/...`.
|
|
||||||
|
|
||||||
## Safety
|
|
||||||
|
|
||||||
The pack sidecar reads your step folder with strict traversal protections
|
|
||||||
(`O_NOFOLLOW`, `S_ISREG` checks, size caps, 4 KiB marker cap). It does not follow
|
|
||||||
symlinks you might drop in. A symlink at any level is silently skipped with a
|
|
||||||
warning — never assume it will be read.
|
|
||||||
|
|
||||||
## What you must NOT do
|
|
||||||
|
|
||||||
- Do not read or write outside your own step folder under `/variables/`.
|
|
||||||
- Do not call out to the CP API directly — the helpers and the pack sidecar
|
|
||||||
handle all variable transport.
|
|
||||||
- Do not assume `/variables/` exists in an unstamped container — it doesn't.
|
|
||||||
- Do not write to `/variables/.done` — the entrypoint owns that marker; it is
|
|
||||||
written for you after `run_finalize_phase` returns, whether you exited 0, non-zero,
|
|
||||||
or via SIGTERM.
|
|
||||||
- Do not store values in env vars, command lines, or prompts — values are data,
|
|
||||||
and data doesn't go there.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
kind: context
|
|
||||||
name: shared-variables
|
|
||||||
version: 1
|
|
||||||
description: "Workflow variables filesystem contract — /variables/ layout, key naming, helpers and CLI for CP-held step inputs/outputs (spec/workflow-variables.md WV-19; injected per-task via HD-14 extra_layers)"
|
|
||||||
requires: []
|
|
||||||
provides: [shared-variables]
|
|
||||||
|
|
||||||
# WV-20: both env vars are set by THIS harness layer (not the entrypoint, not
|
|
||||||
# the container backend). WORKFLOW_VARIABLES_ROOT is static; WORKFLOW_VARIABLES_SELF
|
|
||||||
# is templated by the composer at harness-composition time on the dispatcher,
|
|
||||||
# substituting the node's resolved step_path (the SAME value WV-15 derives at mint
|
|
||||||
# time). A task with no shared-variables/v1 layer (unstamped) has neither env var
|
|
||||||
# set, and /variables does not exist for it at all.
|
|
||||||
env:
|
|
||||||
WORKFLOW_VARIABLES_ROOT: /variables
|
|
||||||
WORKFLOW_VARIABLES_SELF: "{{ step_path }}"
|
|
||||||
|
|
||||||
context_files:
|
|
||||||
- source: ./CLAUDE.md
|
|
||||||
target: /opt/harness/context/shared-variables/CLAUDE.md
|
|
||||||
@@ -6,7 +6,7 @@ endpoint:
|
|||||||
api_key_env: AIROUTER_API_KEY
|
api_key_env: AIROUTER_API_KEY
|
||||||
runner: agentic
|
runner: agentic
|
||||||
default_flags:
|
default_flags:
|
||||||
model: "Qwen3.8" # live airouter id (c3551e3a); 3.6 was stale
|
model: "Qwen3.6"
|
||||||
provider_context_ref: "airouter/v1"
|
provider_context_ref: "airouter/v1"
|
||||||
provider_secret_ref: "AIROUTER_API_KEY"
|
provider_secret_ref: "AIROUTER_API_KEY"
|
||||||
session_capable: false
|
session_capable: false
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
|
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
|
||||||
escalation:
|
escalation:
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
labels: [airouter]
|
labels: [airouter]
|
||||||
prompt: |
|
prompt: |
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ nodes:
|
|||||||
- id: parent-link-check
|
- id: parent-link-check
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/parent-link-check
|
cmd: scripts/workflow/parent-link-check
|
||||||
cp_write: false
|
|
||||||
on_fail: refuse_dispatch
|
on_fail: refuse_dispatch
|
||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
WORKFLOW_ITEM_UUID: "${item.uuid}"
|
WORKFLOW_ITEM_UUID: "${item.uuid}"
|
||||||
PARENT_LINK_TAG: auto-resolve-bugs
|
PARENT_LINK_TAG: auto-resolve-bugs
|
||||||
PARENT_LINK_RELATION: parent
|
PARENT_LINK_RELATION: parent
|
||||||
@@ -43,39 +42,30 @@ nodes:
|
|||||||
- id: tag-ai-resolve
|
- id: tag-ai-resolve
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/tag-set
|
cmd: scripts/workflow/tag-set
|
||||||
cp_write: true
|
|
||||||
depends_on: [parent-link-check]
|
depends_on: [parent-link-check]
|
||||||
on_fail: escalate
|
on_fail: escalate
|
||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
WORKFLOW_ITEM_UUID: "${item.uuid}"
|
WORKFLOW_ITEM_UUID: "${item.uuid}"
|
||||||
TAG_SET_ADD: ai-resolve
|
TAG_SET_ADD: ai-resolve
|
||||||
# Advance the bug; R2 then dispatches bug-verify-sonnet@1 at verify-pending.
|
# Advance the bug; R2 then dispatches bug-verify-sonnet@1 at verify-pending.
|
||||||
- id: state-tx
|
- id: state-tx
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/state-tx
|
cmd: scripts/workflow/state-tx
|
||||||
cp_write: true
|
|
||||||
depends_on: [tag-ai-resolve]
|
depends_on: [tag-ai-resolve]
|
||||||
on_fail: escalate
|
on_fail: escalate
|
||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
STATE_TX_NEW_STATE: verify-pending
|
STATE_TX_NEW_STATE: verify-pending
|
||||||
STATE_TX_FROM_STATES: "pending,open"
|
# NOTE (script-node credentials, M43): kind: script nodes are harness-exempt
|
||||||
# NOTE (script-node credentials): kind: script nodes are harness-exempt
|
# (WF-COMP-6) but now carry the AU-51 internal-service bearer token by
|
||||||
# (WF-COMP-6), but the flag-on script path composes the workflow-script/v1
|
# default — see spec/typed-workflows.md WF-COMP-26. The compiler emits
|
||||||
# composite (shared-variables/v1 + cp-service-token/v1; WF-COMP-31). The
|
# `secrets_required: [CP_SERVICE_TOKEN]` and
|
||||||
# cp-service-token/v1 layer restages the AU-51 internal-service bearer token
|
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN` on
|
||||||
# and exports CP_SERVICE_TOKEN_FILE=/run/agent/cp-service-token/token, which
|
# every script node; the dispatcher's `_run_script_task` legacy-key
|
||||||
# all three nodes above read via
|
# branch mounts the dispatcher-process-env CP_SERVICE_TOKEN value as a
|
||||||
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. The legacy
|
# file. All three nodes above read it via
|
||||||
# WF-COMP-26 mount (`secrets_required: [CP_SERVICE_TOKEN]` and
|
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. Each script
|
||||||
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN`, backed by
|
# still resolves its own actor identity (OIDC_IDENTITY_EMAIL or
|
||||||
# the dispatcher's legacy-key branch) is removed (WF-COMP-33 step 10). Each
|
|
||||||
# script still resolves its own actor identity (OIDC_IDENTITY_EMAIL or
|
|
||||||
# CP_Caller-Identity header — see scripts/workflow/tag-set and
|
# CP_Caller-Identity header — see scripts/workflow/tag-set and
|
||||||
# parent-link-check) for the write envelope.
|
# parent-link-check) for the write envelope.
|
||||||
#
|
|
||||||
# STATE_TX_FROM_STATES on the state-tx node is the compare-and-set guard: the
|
|
||||||
# live flow_state must be one of the listed states (or already the target),
|
|
||||||
# else state-tx exits 2 (unexpected_current_state). pending,open mirror
|
|
||||||
# `input.state` + `escalation_input_states`.
|
|
||||||
@@ -35,10 +35,8 @@ nodes:
|
|||||||
model: sonnet
|
model: sonnet
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "bug-verify/${item.uuid}"
|
AGENT_BRANCH: "bug-verify/${item.uuid}"
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
Change to that directory before doing any work.
|
Change to that directory before doing any work.
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
name: bug-verify-sonnet
|
|
||||||
version: 2
|
|
||||||
description: Read-only verification of an ai-resolve-tagged bug using Sonnet planning (CP-side apply-verified self-transition; WF-COMP-25)
|
|
||||||
runtime: planning-sonnet-repo
|
|
||||||
input:
|
|
||||||
state: verify-pending
|
|
||||||
tags_required: [ai-resolve]
|
|
||||||
tags_forbidden: []
|
|
||||||
required_artifacts: []
|
|
||||||
output:
|
|
||||||
# WF-COMP-25 / WF-COMP-33 — the CP resolves this pipe-separated string as
|
|
||||||
# the closed verdict set; the literal value is NEVER PATCHed. The agent's
|
|
||||||
# packed `verdict` value (see `outputs: [verdict]` on node `agent`) is
|
|
||||||
# what gets PATCHed onto the run's item `flow_state`.
|
|
||||||
state: 'verified|rejected'
|
|
||||||
scope_budget:
|
|
||||||
files_modified_max: 1
|
|
||||||
lines_diff_max: 400
|
|
||||||
wall_clock_seconds_max: 1800
|
|
||||||
nodes:
|
|
||||||
# WF-COMP-25 / WF-COMP-25a — the agent's only side effect is its
|
|
||||||
# declared output `verdict` (one of the two closed values); the CP
|
|
||||||
# applies the flow_state change. The agent does NOT call the CP and
|
|
||||||
# does NOT read any internal-service CP credential file.
|
|
||||||
- id: agent
|
|
||||||
kind: agent
|
|
||||||
outputs: [verdict]
|
|
||||||
template: planning-sonnet-repo
|
|
||||||
model: sonnet
|
|
||||||
runtime_env:
|
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "bug-verify/${item.uuid}"
|
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
|
||||||
prompt: |
|
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
|
||||||
Change to that directory before doing any work.
|
|
||||||
|
|
||||||
Verify the following bug report. This is a READ-ONLY investigation —
|
|
||||||
do NOT modify any source code. Your only file changes are the
|
|
||||||
verification artifact described below.
|
|
||||||
|
|
||||||
**Title**: ${item.title}
|
|
||||||
|
|
||||||
**Bug report**:
|
|
||||||
${item.body}
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. cd /workspace/project
|
|
||||||
2. Investigate the codebase to determine whether the reported bug
|
|
||||||
is real, reproducible, and correctly diagnosed. Do not make
|
|
||||||
code changes — this is verification, not a fix.
|
|
||||||
3. Write your findings to
|
|
||||||
spec/bugs/<slug>-verification.md (derive <slug> from this
|
|
||||||
item's own slug) with exactly these sections, in order:
|
|
||||||
- `## Reproduction` — concrete steps or evidence confirming
|
|
||||||
(or failing to confirm) the reported symptom
|
|
||||||
- `## Root-Cause Hypothesis` — the mechanism you believe
|
|
||||||
causes it, with file/line references
|
|
||||||
- `## Verdict` — exactly one of `verified` or `rejected`
|
|
||||||
- `## Rationale` — why you reached that verdict
|
|
||||||
4. Commit and push the verification artifact on this branch.
|
|
||||||
|
|
||||||
DELIVERING THE VERDICT — read carefully.
|
|
||||||
|
|
||||||
This node's declared output is the literal key `verdict`. The CP
|
|
||||||
reads your packed `verdict` value and PATCHes this work item's own
|
|
||||||
`flow_state` with it (`spec/typed-workflows.md` WF-COMP-25 / 25a,
|
|
||||||
`spec/workflow-variables.md` WV-36). You do NOT call the CP, you
|
|
||||||
do NOT run `cp-cli planner items update`, and you do NOT read any
|
|
||||||
internal-service CP credential file (the path that the legacy
|
|
||||||
`@1` flow used for its CP write is no longer mounted into your
|
|
||||||
container). The CP applies your verdict when `finalize_run` runs.
|
|
||||||
|
|
||||||
The closed verdict vocabulary is exactly two strings:
|
|
||||||
verified
|
|
||||||
rejected
|
|
||||||
|
|
||||||
Any other value — including an empty string, `VERIFIED`, `verified `,
|
|
||||||
`Verified`, `verify`, or a sentence — is rejected by WF-COMP-25a as
|
|
||||||
`self_transition_verdict_outside_set` and the run surfaces to a
|
|
||||||
human via `request_decision`. Do not invent a third outcome.
|
|
||||||
|
|
||||||
Write the verdict to your step folder with a quoted heredoc whose
|
|
||||||
terminator cannot occur in the body — example for verdict
|
|
||||||
`verified`:
|
|
||||||
|
|
||||||
cat > /variables/agent/verdict <<'__VERDICT_END__'
|
|
||||||
verified
|
|
||||||
__VERDICT_END__
|
|
||||||
|
|
||||||
The file MUST be strict UTF-8 with LF line endings; no trailing
|
|
||||||
whitespace; no header line; no body. A file that is not exactly the
|
|
||||||
one-line verdict (with optional final LF) is rejected as
|
|
||||||
`self_transition_verdict_outside_set` and surfaces to a human.
|
|
||||||
|
|
||||||
Do NOT use `wfvar set verdict <body>` — it passes the body as argv
|
|
||||||
and breaks on shell quoting.
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Investigate.
|
|
||||||
2. Decide `verified` or `rejected`. Record it in
|
|
||||||
`## Verdict` of the verification artifact AND in
|
|
||||||
`/variables/agent/verdict` — the two MUST agree.
|
|
||||||
3. Commit and push the verification artifact.
|
|
||||||
4. Call task_complete. Do NOT PATCH the CP yourself.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
name: cpw-smoke
|
|
||||||
version: 1
|
|
||||||
description: One-node cp_write smoke test for Option C (AU-55 per-step write token); exercises workflow-script-write/v1 end to end on a throwaway item
|
|
||||||
runtime: script
|
|
||||||
input:
|
|
||||||
state: scoped-tagged
|
|
||||||
tags_required: []
|
|
||||||
required_artifacts: []
|
|
||||||
output:
|
|
||||||
state: test-skeleton-ready
|
|
||||||
scope_budget:
|
|
||||||
files_modified_max: 1
|
|
||||||
lines_diff_max: 1
|
|
||||||
wall_clock_seconds_max: 300
|
|
||||||
agent_repo:
|
|
||||||
url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
nodes:
|
|
||||||
- id: state-tx
|
|
||||||
kind: script
|
|
||||||
cmd: scripts/workflow/state-tx
|
|
||||||
cp_write: true
|
|
||||||
on_fail: refuse_dispatch
|
|
||||||
runtime_env:
|
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
|
||||||
STATE_TX_NEW_STATE: test-skeleton-ready
|
|
||||||
STATE_TX_FROM_STATES: scoped-tagged
|
|
||||||
@@ -23,8 +23,6 @@ nodes:
|
|||||||
template: code-sonnet-tdd-repo
|
template: code-sonnet-tdd-repo
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
Implement the scope described in this work item. The description lists
|
Implement the scope described in this work item. The description lists
|
||||||
|
|||||||
@@ -18,7 +18,5 @@ nodes:
|
|||||||
template: code-fable-tdd-repo
|
template: code-fable-tdd-repo
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ nodes:
|
|||||||
- id: policy-pick
|
- id: policy-pick
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/policy-pick
|
cmd: scripts/workflow/policy-pick
|
||||||
cp_write: false
|
|
||||||
on_fail: escalate
|
on_fail: escalate
|
||||||
- id: agent
|
- id: agent
|
||||||
kind: agent
|
kind: agent
|
||||||
|
|||||||
@@ -12,20 +12,12 @@ scope_budget:
|
|||||||
files_modified_max: 2
|
files_modified_max: 2
|
||||||
lines_diff_max: 200
|
lines_diff_max: 200
|
||||||
wall_clock_seconds_max: 600
|
wall_clock_seconds_max: 600
|
||||||
agent_repo:
|
|
||||||
url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
nodes:
|
nodes:
|
||||||
- id: frozen-fixture-emit
|
- id: frozen-fixture-emit
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/frozen-fixture-emit
|
cmd: scripts/workflow/frozen-fixture-emit
|
||||||
cp_write: false
|
|
||||||
on_fail: refuse_dispatch
|
on_fail: refuse_dispatch
|
||||||
- id: state-tx
|
- id: state-tx
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/state-tx
|
cmd: scripts/workflow/state-tx
|
||||||
cp_write: true
|
|
||||||
on_fail: continue
|
on_fail: continue
|
||||||
runtime_env:
|
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
|
||||||
STATE_TX_NEW_STATE: test-skeleton-ready
|
|
||||||
STATE_TX_FROM_STATES: scoped-tagged
|
|
||||||
|
|||||||
@@ -18,7 +18,5 @@ nodes:
|
|||||||
template: code-minimax-tdd-repo
|
template: code-minimax-tdd-repo
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
||||||
|
|||||||
@@ -22,7 +22,5 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
model: claude-opus-5
|
model: claude-opus-5
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
@@ -119,7 +117,6 @@ nodes:
|
|||||||
- id: judge-gate
|
- id: judge-gate
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/verdict-gate
|
cmd: scripts/workflow/verdict-gate
|
||||||
cp_write: false
|
|
||||||
depends_on: [judge-haiku]
|
depends_on: [judge-haiku]
|
||||||
loop:
|
loop:
|
||||||
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]
|
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ nodes:
|
|||||||
labels: [airouter]
|
labels: [airouter]
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ nodes:
|
|||||||
template: best-practices-fable-low-repo
|
template: best-practices-fable-low-repo
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ nodes:
|
|||||||
model: sonnet
|
model: sonnet
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
# AR-38: this stage's deliverable is work items POSTed to the CP, not a
|
# AR-38: this stage's deliverable is work items POSTed to the CP, not a
|
||||||
# repo diff — the empty-deliverable audit would fail every success.
|
# repo diff — the empty-deliverable audit would fail every success.
|
||||||
AGENT_EMPTY_DELIVERABLE_CHECK: "false"
|
AGENT_EMPTY_DELIVERABLE_CHECK: "false"
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
name: scope-decompose-sonnet
|
|
||||||
version: 2
|
|
||||||
description: Scope and decompose a spec into tagged tasks using Sonnet planning (CP-side apply-verified; WF-COMP-33 / WIA-47)
|
|
||||||
runtime: spec-writing-sonnet-repo
|
|
||||||
input:
|
|
||||||
state: spec-reviewed-arch
|
|
||||||
tags_forbidden: []
|
|
||||||
required_artifacts: []
|
|
||||||
output:
|
|
||||||
state: scoped-tagged
|
|
||||||
scope_budget:
|
|
||||||
files_modified_max: 10
|
|
||||||
lines_diff_max: 500
|
|
||||||
wall_clock_seconds_max: 2400
|
|
||||||
nodes:
|
|
||||||
# WF-COMP-33 / WIA-47 / WV-36..37 — the agent's only side effect is the
|
|
||||||
# declared output `plan_manifest` plus the per-child item files under
|
|
||||||
# `items/<local_id>`; `finalize_run` reads, validates and applies them.
|
|
||||||
# The agent does NOT call the CP and does NOT read any internal-service
|
|
||||||
# CP credential file.
|
|
||||||
- id: agent
|
|
||||||
kind: agent
|
|
||||||
outputs: [plan_manifest]
|
|
||||||
template: spec-writing-sonnet-repo
|
|
||||||
model: sonnet
|
|
||||||
runtime_env:
|
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
|
||||||
# The deliverable is a directory of plan files on disk — NOT a git diff.
|
|
||||||
AGENT_EMPTY_DELIVERABLE_CHECK: "false"
|
|
||||||
prompt: |
|
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
|
||||||
Change to that directory before doing any work.
|
|
||||||
|
|
||||||
Find the spec file in /workspace/project/spec/ (the spec was
|
|
||||||
written in an earlier pipeline stage — look for a file whose name matches
|
|
||||||
the concept title '${item.title}' as a slug, e.g. spec/<slug>.md).
|
|
||||||
Read that spec carefully.
|
|
||||||
|
|
||||||
YOUR ONLY DELIVERABLE FOR THIS STAGE IS THE DECOMPOSE PLAN. You do
|
|
||||||
NOT call the CP, you do NOT run `cp-cli planner items decompose`,
|
|
||||||
and you do NOT read any internal-service CP credential file (the
|
|
||||||
path that the legacy `@1` flow used for its CP write is no longer
|
|
||||||
mounted into your container). The CP applies your plan from disk
|
|
||||||
at finalize time (spec/work-items-api.md WIA-47..47g). This node's
|
|
||||||
declared output is the literal key `plan_manifest`; the CP packs
|
|
||||||
the manifest plus every per-child item file in the same step folder.
|
|
||||||
|
|
||||||
On-disk layout (every path is relative to your step folder):
|
|
||||||
/variables/agent/plan_manifest # the manifest (plain text)
|
|
||||||
/variables/agent/items/<local_id> # one item file per child
|
|
||||||
|
|
||||||
`<local_id>` matches `^[a-z0-9][a-z0-9-]{0,31}$` — lowercase letters,
|
|
||||||
digits, and hyphen; 1–32 characters; no underscore. Use the literal
|
|
||||||
`$root` as the parent header on every item; the CP substitutes the
|
|
||||||
real item UUID server-side (WV-36 / WIA-47e).
|
|
||||||
|
|
||||||
Write each item file with a quoted heredoc whose terminator cannot
|
|
||||||
occur in the body — example for a child `c1`:
|
|
||||||
|
|
||||||
cat > /variables/agent/items/c1 <<'__DECOMPOSE_ITEM_END__'
|
|
||||||
parent: $root
|
|
||||||
title: Implement payment service FP-1..3
|
|
||||||
spec_ids: FP-1,FP-2,FP-3
|
|
||||||
test_files: tests/payments/test_fp.py
|
|
||||||
tags: airouter-eligible
|
|
||||||
---
|
|
||||||
Write the payment service implementation covering FP-1, FP-2, FP-3.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
- FP-1: idempotent payment capture
|
|
||||||
- FP-2: refund flow
|
|
||||||
__DECOMPOSE_ITEM_END__
|
|
||||||
|
|
||||||
Do NOT use `wfvar set <key> <body>` for plan files — it passes the
|
|
||||||
body as argv and breaks on backticks, quotes and `$(...)` and
|
|
||||||
truncates at ARG_MAX. The file-write / heredoc path is byte-faithful.
|
|
||||||
|
|
||||||
Manifest format — first line exactly `decompose/v1`, then one
|
|
||||||
`local_id` per line, no blank lines, no header, no body. Example for
|
|
||||||
the three-item plan above:
|
|
||||||
|
|
||||||
decompose/v1
|
|
||||||
c1
|
|
||||||
c2
|
|
||||||
c3
|
|
||||||
|
|
||||||
Item header grammar (every line is `key: value`):
|
|
||||||
- closed header set: {parent, title, spec_ids, test_files, tags}
|
|
||||||
- `parent` and `title` are REQUIRED on every item
|
|
||||||
- `parent` MUST be the literal `$root` (a work-item UUID is rejected
|
|
||||||
as `parent_must_be_root` — privilege-escalation guard)
|
|
||||||
- `title` is 1–200 bytes UTF-8, unique within one plan compared
|
|
||||||
case- and whitespace-insensitively
|
|
||||||
- `spec_ids` and `test_files` are comma-separated, at most 50
|
|
||||||
entries each; `spec_ids` entries match `^[A-Z][A-Z0-9-]{0,31}$`
|
|
||||||
(e.g. `FP-1`, `WV-36`)
|
|
||||||
- `test_files` entries have no leading `/`, no `..` segment, no SP
|
|
||||||
or `,`, at most 250 bytes each (e.g.
|
|
||||||
`tests/payments/test_fp.py`)
|
|
||||||
- `tags` is optional; every tag MUST be exactly `airouter-eligible`
|
|
||||||
(the only tag in DECOMPOSE_AGENT_TAG_ALLOWLIST — anything else
|
|
||||||
is `tag_not_allowed`). Omit the `tags:` line for tasks Qwen3.6
|
|
||||||
cannot handle alone.
|
|
||||||
- `---` ends the header block; every byte after it to EOF is the
|
|
||||||
body, verbatim (printable ASCII + UTF-8 bytes 0x80–0xFF, plus
|
|
||||||
LF and TAB as the only permitted control bytes; CR is rejected).
|
|
||||||
The body must be ≤ 16,384 bytes UTF-8.
|
|
||||||
- one header per item, no duplicates (`duplicate_header`)
|
|
||||||
- unknown keys are rejected (`unknown_header`)
|
|
||||||
- the file MUST be strict UTF-8, LF line endings (CRLF is rejected)
|
|
||||||
|
|
||||||
Plan fan-out: 1–20 `local_id` lines in the manifest, exactly one
|
|
||||||
`items/<local_id>` file per manifest line (an orphan file or a
|
|
||||||
missing file fails the apply as `orphan_item_file` /
|
|
||||||
`manifest_entry_without_file`).
|
|
||||||
|
|
||||||
Title / slug uniqueness: every planned title must be unique within
|
|
||||||
the plan (`duplicate_title`); every planned slug
|
|
||||||
(`decompose_validation.slugify(title)`, lowercase, non-alphanum
|
|
||||||
runs collapsed to `-`, truncated to 80 chars) must also be unique
|
|
||||||
(`duplicate_slug`). Two titles that differ only in case / whitespace
|
|
||||||
are NOT distinct.
|
|
||||||
|
|
||||||
"airouter-eligible" is the only tag you may use. Do NOT emit
|
|
||||||
`auto-dispatch`, `integrated`, `ai-resolve` or `airouter-skip` —
|
|
||||||
the validator rejects those at apply time.
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Read the spec carefully.
|
|
||||||
2. Decide the children (1–20). Draft a title, a one-paragraph
|
|
||||||
body, and the `spec_ids` / `test_files` for each.
|
|
||||||
3. Write `/variables/agent/plan_manifest` with a heredoc
|
|
||||||
(the heredoc terminator MUST be unique to that file — do not
|
|
||||||
reuse `__DECOMPOSE_ITEM_END__` across files).
|
|
||||||
4. Write each `/variables/agent/items/<local_id>` with a heredoc.
|
|
||||||
5. Confirm the manifest names exactly the files you wrote, and
|
|
||||||
that no extra `items/*` files exist (an orphan file fails the
|
|
||||||
apply).
|
|
||||||
6. Call task_complete. Do NOT PATCH the CP yourself.
|
|
||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
|
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
|
||||||
escalation:
|
escalation:
|
||||||
|
|||||||
@@ -22,7 +22,5 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
labels: [airouter]
|
labels: [airouter]
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
template: spec-writing-fable-low-repo
|
template: spec-writing-fable-low-repo
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ nodes:
|
|||||||
model: claude-opus-5
|
model: claude-opus-5
|
||||||
runtime_env:
|
runtime_env:
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "spec/${item.uuid}"
|
AGENT_BRANCH: "spec/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
The agent-runtimes project repo is cloned at /workspace/project.
|
The agent-runtimes project repo is cloned at /workspace/project.
|
||||||
@@ -129,7 +127,6 @@ nodes:
|
|||||||
- id: judge-gate
|
- id: judge-gate
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/verdict-gate
|
cmd: scripts/workflow/verdict-gate
|
||||||
cp_write: false
|
|
||||||
depends_on: [judge-haiku]
|
depends_on: [judge-haiku]
|
||||||
loop:
|
loop:
|
||||||
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]
|
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]
|
||||||
|
|||||||
@@ -12,30 +12,20 @@ scope_budget:
|
|||||||
files_modified_max: 1
|
files_modified_max: 1
|
||||||
lines_diff_max: 50
|
lines_diff_max: 50
|
||||||
wall_clock_seconds_max: 600
|
wall_clock_seconds_max: 600
|
||||||
agent_repo:
|
|
||||||
url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
nodes:
|
nodes:
|
||||||
- id: preflight-test-collection
|
- id: preflight-test-collection
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/preflight-test-collection
|
cmd: scripts/workflow/preflight-test-collection
|
||||||
cp_write: false
|
|
||||||
on_fail: refuse_dispatch
|
on_fail: refuse_dispatch
|
||||||
- id: preflight-fixture-lint
|
- id: preflight-fixture-lint
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/preflight-fixture-lint
|
cmd: scripts/workflow/preflight-fixture-lint
|
||||||
cp_write: false
|
|
||||||
on_fail: refuse_dispatch
|
on_fail: refuse_dispatch
|
||||||
- id: preflight-test-fail-mode
|
- id: preflight-test-fail-mode
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/preflight-test-fail-mode
|
cmd: scripts/workflow/preflight-test-fail-mode
|
||||||
cp_write: false
|
|
||||||
on_fail: continue
|
on_fail: continue
|
||||||
- id: state-tx
|
- id: state-tx
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/state-tx
|
cmd: scripts/workflow/state-tx
|
||||||
cp_write: true
|
|
||||||
on_fail: continue
|
on_fail: continue
|
||||||
runtime_env:
|
|
||||||
CP_URL: "https://agents.oreillyit.nz/api"
|
|
||||||
STATE_TX_NEW_STATE: test-validated
|
|
||||||
STATE_TX_FROM_STATES: test-write-pending
|
|
||||||
|
|||||||
@@ -22,6 +22,4 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
@@ -26,8 +26,6 @@ nodes:
|
|||||||
# WF-COMP-15: stage agents share one per-item work branch so their work
|
# WF-COMP-15: stage agents share one per-item work branch so their work
|
||||||
# accumulates across iterations.
|
# accumulates across iterations.
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
- id: review-sonnet
|
- id: review-sonnet
|
||||||
kind: agent
|
kind: agent
|
||||||
@@ -61,7 +59,6 @@ nodes:
|
|||||||
- id: review-gate
|
- id: review-gate
|
||||||
kind: script
|
kind: script
|
||||||
cmd: scripts/workflow/verdict-gate
|
cmd: scripts/workflow/verdict-gate
|
||||||
cp_write: false
|
|
||||||
depends_on: [review-sonnet]
|
depends_on: [review-sonnet]
|
||||||
loop:
|
loop:
|
||||||
body: [write-minimax, review-sonnet, review-gate]
|
body: [write-minimax, review-sonnet, review-gate]
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ nodes:
|
|||||||
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
|
||||||
# clones and the agent's output evaporates (E2E 2026-07-04).
|
# clones and the agent's output evaporates (E2E 2026-07-04).
|
||||||
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
|
||||||
AGENT_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
|
|
||||||
AGENT_UPSTREAM_BRANCH: "main"
|
|
||||||
AGENT_BRANCH: "task/${item.uuid}"
|
AGENT_BRANCH: "task/${item.uuid}"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are writing pytest tests for the following task.
|
You are writing pytest tests for the following task.
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
"""Every shipped kind: script node declares cp_write explicitly (WF-COMP-31d).
|
|
||||||
|
|
||||||
``cp_write: true`` selects the workflow-script-write/v1 composite (AU-55
|
|
||||||
per-step token); ``cp_write: false`` selects workflow-script-nocp/v1 (no CP
|
|
||||||
credential). The declaration must match what the script actually does:
|
|
||||||
state-tx / state-tx-verdict / tag-set write the run's own item (true); every
|
|
||||||
other script, including parent-link-check and decompose-and-queue (whose
|
|
||||||
routes the AU-57 credential cannot serve), is false.
|
|
||||||
|
|
||||||
Archived templates are included: the explicit declaration is the audit pin.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_DIR = Path(__file__).resolve().parents[2] / "task-templates"
|
|
||||||
|
|
||||||
_CP_WRITE_TRUE = {"state-tx", "state-tx-verdict", "tag-set"}
|
|
||||||
_CP_WRITE_FALSE = {
|
|
||||||
"verdict-gate", "policy-pick", "frozen-fixture-emit",
|
|
||||||
"preflight-test-collection", "preflight-fixture-lint", "preflight-test-fail-mode",
|
|
||||||
"parent-link-check", "decompose-and-queue",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _script_nodes() -> list[tuple[str, str, dict]]:
|
|
||||||
out = []
|
|
||||||
for p in sorted(_DIR.rglob("*.yaml")):
|
|
||||||
doc = yaml.safe_load(p.read_text()) or {}
|
|
||||||
if not isinstance(doc, dict):
|
|
||||||
continue
|
|
||||||
for n in doc.get("nodes") or []:
|
|
||||||
if isinstance(n, dict) and n.get("kind") == "script":
|
|
||||||
out.append((str(p.relative_to(_DIR)), n["id"], n))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
_NODES = _script_nodes()
|
|
||||||
_IDS = [f"{f}::{nid}" for f, nid, _ in _NODES]
|
|
||||||
|
|
||||||
|
|
||||||
def test_script_nodes_discovered():
|
|
||||||
files = {f for f, _, _ in _NODES}
|
|
||||||
assert {
|
|
||||||
"workflow/test-validate@1.yaml",
|
|
||||||
"workflow/frozen-fixture-emit@1.yaml",
|
|
||||||
"workflow/plan-review-loop@1.yaml",
|
|
||||||
"workflow/spec-review-loop@1.yaml",
|
|
||||||
"workflow/test-write-review-loop@1.yaml",
|
|
||||||
"workflow/feature-delivery-loop@1.yaml",
|
|
||||||
"workflow/_archived/bug-ancestor-auto-advance@1.yaml",
|
|
||||||
} <= files
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fname,nid,node", _NODES, ids=_IDS)
|
|
||||||
def test_script_node_declares_cp_write_explicitly_and_correctly(fname, nid, node):
|
|
||||||
assert "cp_write" in node, f"{fname}::{nid} must declare cp_write explicitly (WF-COMP-31d)"
|
|
||||||
assert isinstance(node["cp_write"], bool), f"{fname}::{nid} cp_write must be a bool"
|
|
||||||
script = node["cmd"].rsplit("/", 1)[-1]
|
|
||||||
assert script in _CP_WRITE_TRUE | _CP_WRITE_FALSE, (
|
|
||||||
f"{fname}::{nid} runs unclassified script {script!r}; classify it in "
|
|
||||||
"this test (does it call the CP?) and AU-57 before shipping"
|
|
||||||
)
|
|
||||||
assert node["cp_write"] is (script in _CP_WRITE_TRUE), (
|
|
||||||
f"{fname}::{nid}: cp_write must be {script in _CP_WRITE_TRUE} for {script}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_wf_comp_31d_cp_write_field_audit_per_framework_template():
|
|
||||||
"""Spec WF-COMP-31d framework-repo test handle: pin the declarations."""
|
|
||||||
got = {(f, nid): n["cp_write"] for f, nid, n in _NODES}
|
|
||||||
assert got[("workflow/test-validate@1.yaml", "state-tx")] is True
|
|
||||||
assert got[("workflow/frozen-fixture-emit@1.yaml", "state-tx")] is True
|
|
||||||
assert got[("workflow/frozen-fixture-emit@1.yaml", "frozen-fixture-emit")] is False
|
|
||||||
for nid in ("preflight-test-collection", "preflight-fixture-lint", "preflight-test-fail-mode"):
|
|
||||||
assert got[("workflow/test-validate@1.yaml", nid)] is False
|
|
||||||
assert got[("workflow/plan-review-loop@1.yaml", "judge-gate")] is False
|
|
||||||
assert got[("workflow/spec-review-loop@1.yaml", "judge-gate")] is False
|
|
||||||
assert got[("workflow/test-write-review-loop@1.yaml", "review-gate")] is False
|
|
||||||
assert got[("workflow/feature-delivery-loop@1.yaml", "policy-pick")] is False
|
|
||||||
arch = "workflow/_archived/bug-ancestor-auto-advance@1.yaml"
|
|
||||||
assert got[(arch, "parent-link-check")] is False
|
|
||||||
assert got[(arch, "tag-ai-resolve")] is True
|
|
||||||
assert got[(arch, "state-tx")] is True
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""cp-write-token/v1 context: env-only layer (WF-COMP-31c, AU-55b).
|
|
||||||
|
|
||||||
The per-step wfcw_ token is delivered by the dispatcher into the script
|
|
||||||
container only (Secret projection on K8s, create -> put_archive -> start on
|
|
||||||
Docker). The layer must therefore set the env contract and nothing else: no
|
|
||||||
ESO secret mount, no init script, no context files.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
|
|
||||||
|
|
||||||
def _load(kind_dir: str, name: str) -> dict:
|
|
||||||
return yaml.safe_load((_ROOT / kind_dir / name / "v1" / "harness.yaml").read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def test_cp_write_token_layer_sets_env_contract():
|
|
||||||
doc = _load("contexts", "cp-write-token")
|
|
||||||
assert doc["kind"] == "context"
|
|
||||||
assert doc["name"] == "cp-write-token"
|
|
||||||
assert doc["version"] == 1
|
|
||||||
assert doc["env"] == {"CP_WRITE_TOKEN_FILE": "/run/agent/cp-write-token/token"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_cp_write_token_layer_is_env_only():
|
|
||||||
doc = _load("contexts", "cp-write-token")
|
|
||||||
assert not doc.get("secrets_required"), "token is not an ESO mount (AU-55b)"
|
|
||||||
assert not doc.get("scripts"), "no init script: the backend delivers the file"
|
|
||||||
assert not doc.get("context_files")
|
|
||||||
assert not doc.get("write_targets")
|
|
||||||
assert doc.get("requires") == []
|
|
||||||
assert doc.get("provides") == []
|
|
||||||
assert not (_ROOT / "contexts" / "cp-write-token" / "v1" / "init.sh").exists()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"""state-tx script nodes must declare CP_URL, STATE_TX_NEW_STATE and STATE_TX_FROM_STATES.
|
|
||||||
|
|
||||||
scripts/workflow/state-tx fails with exit 2 when any is missing
|
|
||||||
(from_states_missing for STATE_TX_FROM_STATES).
|
|
||||||
CP_SERVICE_TOKEN_FILE is supplied by the workflow-script/v1 composite, not
|
|
||||||
runtime_env, and must not be declared here. STATE_TX_NEW_STATE must equal the
|
|
||||||
template's own output.state. STATE_TX_FROM_STATES is a comma list of acceptable
|
|
||||||
current flow_states and must be non-empty.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_DIR = Path(__file__).resolve().parents[2] / "task-templates" / "workflow"
|
|
||||||
|
|
||||||
# Every template with a state-tx node, discovered from disk so new @N variants
|
|
||||||
# are covered automatically.
|
|
||||||
_NAMES = sorted(
|
|
||||||
p.name
|
|
||||||
for p in _DIR.glob("*.yaml")
|
|
||||||
if any(
|
|
||||||
n.get("cmd") == "scripts/workflow/state-tx"
|
|
||||||
for n in (yaml.safe_load(p.read_text()).get("nodes") or [])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_state_tx_templates_discovered():
|
|
||||||
assert {
|
|
||||||
"test-validate@1.yaml",
|
|
||||||
"frozen-fixture-emit@1.yaml",
|
|
||||||
} <= set(_NAMES)
|
|
||||||
assert "bug-ancestor-auto-advance@1.yaml" not in _NAMES # archived (WT-ANC-15)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", _NAMES)
|
|
||||||
def test_state_tx_node_declares_required_runtime_env(name):
|
|
||||||
doc = yaml.safe_load((_DIR / name).read_text())
|
|
||||||
nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"]
|
|
||||||
assert len(nodes) == 1
|
|
||||||
env = nodes[0].get("runtime_env") or {}
|
|
||||||
assert env.get("CP_URL")
|
|
||||||
assert env.get("STATE_TX_NEW_STATE") == doc["output"]["state"]
|
|
||||||
assert "CP_SERVICE_TOKEN_FILE" not in env
|
|
||||||
assert not any(k.startswith("OIDC_") for k in env)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", _NAMES)
|
|
||||||
def test_state_tx_node_declares_from_states(name):
|
|
||||||
doc = yaml.safe_load((_DIR / name).read_text())
|
|
||||||
nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"]
|
|
||||||
env = nodes[0].get("runtime_env") or {}
|
|
||||||
raw = env.get("STATE_TX_FROM_STATES")
|
|
||||||
assert isinstance(raw, str) and raw.strip()
|
|
||||||
states = [s.strip() for s in raw.split(",")]
|
|
||||||
assert all(states), f"empty entry in STATE_TX_FROM_STATES {raw!r}"
|
|
||||||
assert doc["output"]["state"] not in states
|
|
||||||
# The template's own input state must be an acceptable from-state.
|
|
||||||
assert doc["input"]["state"] in states
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# CP_URL must be the ingress form, never the in-cluster Service on :8100.
|
|
||||||
#
|
|
||||||
# The deploy repo's networkpolicy-cp-internal-traefik-only.yaml admits :8100 only
|
|
||||||
# from dashboard / git-planner-backend / cert-expiry-checker and the traefik and
|
|
||||||
# cms-proxy namespaces. Agent/script pods are dropped, so a template pointing
|
|
||||||
# CP_URL at svc.cluster.local:8100 times out (30s) and, with on_fail: continue,
|
|
||||||
# silently loses the transition. The workflow scripts join CP_URL + "/v1/...",
|
|
||||||
# so the /api ingress path prefix is preserved.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
CP_URL_INGRESS = "https://agents.oreillyit.nz/api"
|
|
||||||
_FORBIDDEN_CP_URL_MARKERS = ("svc.cluster.local:8100", "controlplane.agent-runtimes.svc")
|
|
||||||
|
|
||||||
_ALL_TEMPLATES = sorted(_DIR.glob("*.yaml"))
|
|
||||||
|
|
||||||
|
|
||||||
def _cp_url_values(path: Path) -> list[str]:
|
|
||||||
doc = yaml.safe_load(path.read_text())
|
|
||||||
return [
|
|
||||||
(n.get("runtime_env") or {})["CP_URL"]
|
|
||||||
for n in (doc.get("nodes") or [])
|
|
||||||
if "CP_URL" in (n.get("runtime_env") or {})
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_cp_url_templates_discovered():
|
|
||||||
names = {p.name for p in _ALL_TEMPLATES if _cp_url_values(p)}
|
|
||||||
assert {
|
|
||||||
"test-validate@1.yaml",
|
|
||||||
"frozen-fixture-emit@1.yaml",
|
|
||||||
"scope-decompose-sonnet@1.yaml",
|
|
||||||
"scope-decompose-sonnet@2.yaml",
|
|
||||||
"bug-verify-sonnet@1.yaml",
|
|
||||||
"bug-verify-sonnet@2.yaml",
|
|
||||||
} <= names
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("path", _ALL_TEMPLATES, ids=lambda p: p.name)
|
|
||||||
def test_cp_url_is_ingress_not_in_cluster_service(path):
|
|
||||||
for value in _cp_url_values(path):
|
|
||||||
assert not any(m in value for m in _FORBIDDEN_CP_URL_MARKERS), (
|
|
||||||
f"{path.name}: CP_URL={value!r} targets the in-cluster CP service; agent pods "
|
|
||||||
"are blocked on :8100 by NetworkPolicy"
|
|
||||||
)
|
|
||||||
assert value == CP_URL_INGRESS, f"{path.name}: CP_URL={value!r}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("path", _ALL_TEMPLATES, ids=lambda p: p.name)
|
|
||||||
def test_no_template_mentions_in_cluster_cp_service(path):
|
|
||||||
text = path.read_text()
|
|
||||||
assert not any(m in text for m in _FORBIDDEN_CP_URL_MARKERS)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""workflow-script/v1 composite: exact layer order and credential env export.
|
|
||||||
|
|
||||||
Script nodes get their CP write credential only through this composite
|
|
||||||
(the legacy WF-COMP-26 mount is removed), so the layering is load-bearing.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
|
|
||||||
|
|
||||||
def _load(kind_dir: str, name: str) -> dict:
|
|
||||||
return yaml.safe_load((_ROOT / kind_dir / name / "v1" / "harness.yaml").read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_layers_shared_variables_cp_service_token_repo_readonly():
|
|
||||||
doc = _load("composites", "workflow-script")
|
|
||||||
assert doc["layers"] == [
|
|
||||||
{"context": "shared-variables/v1"},
|
|
||||||
{"context": "cp-service-token/v1"},
|
|
||||||
{"context": "repo-readonly/v1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_env_exports_cp_service_token_file():
|
|
||||||
doc = _load("composites", "workflow-script")
|
|
||||||
env: dict = dict(doc.get("env") or {})
|
|
||||||
for layer in doc["layers"]:
|
|
||||||
(kind, ref), = layer.items()
|
|
||||||
name = ref.split("/")[0]
|
|
||||||
env.update(_load(kind + "s", name).get("env") or {})
|
|
||||||
assert env["CP_SERVICE_TOKEN_FILE"] == "/run/agent/cp-service-token/token"
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
"""Workflow-script harness composition invariant (spec/typed-workflows.md WF-COMP-31
|
|
||||||
+ spec/workflow-variables.md WV-19 + spec/harness.md HD-14).
|
|
||||||
|
|
||||||
The two new framework layers — ``harnesses/contexts/shared-variables/v1/`` and
|
|
||||||
``harnesses/composites/workflow-script/v1/`` — must exist on the tree, must parse,
|
|
||||||
and the composite must resolve every layer it references to a real file on disk.
|
|
||||||
This test guards against the most common regression: a missing or renamed layer
|
|
||||||
that would only surface at the dispatcher's ``compose_harness`` call, where it
|
|
||||||
costs an entire task's worth of wasted dispatch round-trip.
|
|
||||||
|
|
||||||
Spec: /home/paul/dev/claude/projects/agent-runtimes/spec/workflow-variables.md
|
|
||||||
(WV-19, WV-20, WF-COMP-31), spec/harness.md (HD-14).
|
|
||||||
|
|
||||||
Not committed by the authoring agent per its instructions — written here so it
|
|
||||||
can run locally against the current tree.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_HARNESSES_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
|
|
||||||
# Composite layer entries use the singular kind as the dict key. The kind maps
|
|
||||||
# to a top-level directory by adding "s".
|
|
||||||
_KIND_TO_DIR = {
|
|
||||||
"context": "contexts",
|
|
||||||
"capability": "capabilities",
|
|
||||||
"composite": "composites",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _load_layer(kind_dir: str, name: str) -> dict:
|
|
||||||
p = _HARNESSES_ROOT / kind_dir / name / "v1" / "harness.yaml"
|
|
||||||
return yaml.safe_load(p.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_shared_variables_layer_exists_and_parses():
|
|
||||||
"""``harnesses/contexts/shared-variables/v1/harness.yaml`` must exist,
|
|
||||||
parse as YAML, and carry ``provides: [shared-variables]`` (the HD-14 tag).
|
|
||||||
|
|
||||||
Spec: spec/workflow-variables.md WV-19; spec/harness.md HD-14.
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("contexts", "shared-variables")
|
|
||||||
assert parsed["kind"] == "context"
|
|
||||||
assert parsed["name"] == "shared-variables"
|
|
||||||
assert parsed["version"] == 1
|
|
||||||
assert parsed["provides"] == ["shared-variables"], (
|
|
||||||
"WV-19: provides MUST contain the 'shared-variables' tag — HD-14's "
|
|
||||||
"extra_layers dedup key, and the tag the dispatcher's payload-marker "
|
|
||||||
"injection matches against."
|
|
||||||
)
|
|
||||||
# WV-20: this layer's OWN env: declaration sets the two WORKFLOW_VARIABLES_*
|
|
||||||
# vars (NOT the entrypoint, NOT the container backend).
|
|
||||||
env = parsed["env"]
|
|
||||||
assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables"
|
|
||||||
assert "WORKFLOW_VARIABLES_SELF" in env, (
|
|
||||||
"WV-20: WORKFLOW_VARIABLES_SELF must be set by the layer's env: "
|
|
||||||
"(templated per-node at harness-composition time on the dispatcher)"
|
|
||||||
)
|
|
||||||
# Agent-facing context: a CLAUDE.md must be mounted under /opt/harness/context/.
|
|
||||||
files = parsed.get("context_files", [])
|
|
||||||
targets = [f["target"] for f in files]
|
|
||||||
assert any(t.startswith("/opt/harness/context/shared-variables/") for t in targets), (
|
|
||||||
"WV-19: agent-facing context must be mounted under /opt/harness/context/"
|
|
||||||
"(the HC-7 namespacing point) so the agent sees it at startup."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_shared_variables_claude_md_present_and_substantial():
|
|
||||||
"""The agent-facing CLAUDE.md must be a non-trivial markdown document
|
|
||||||
(HC-7 says it is read by the agent, not a placeholder).
|
|
||||||
|
|
||||||
Spec: spec/workflow-variables.md WV-19; spec/harness.md HC-7.
|
|
||||||
"""
|
|
||||||
md = _HARNESSES_ROOT / "contexts" / "shared-variables" / "v1" / "CLAUDE.md"
|
|
||||||
assert md.exists(), "WV-19: shared-variables/v1 must ship a CLAUDE.md"
|
|
||||||
text = md.read_text(encoding="utf-8")
|
|
||||||
assert text.count("\n") >= 30, (
|
|
||||||
f"WV-19: the CLAUDE.md must explain the /variables/ contract to the "
|
|
||||||
f"agent — got {text.count(chr(10))} lines, expected >=30."
|
|
||||||
)
|
|
||||||
# Spot-check the four high-value pieces of content the agent MUST see:
|
|
||||||
assert "/variables/" in text, "must explain the filesystem root"
|
|
||||||
assert "WORKFLOW_VARIABLES_SELF" in text, "must document the env vars"
|
|
||||||
assert "wfvar" in text, "must point at the CLI helper (WV-21)"
|
|
||||||
assert "reserved" in text.lower(), "must list the reserved-name families (WV-6)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_composite_resolves():
|
|
||||||
"""``harnesses/composites/workflow-script/v1/harness.yaml`` must declare
|
|
||||||
exactly the two layers the spec names (WF-COMP-31) and every referenced
|
|
||||||
file must exist on disk.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31 ("Internal composite layered from
|
|
||||||
``shared-variables/v1`` + ``cp-service-token/v1``").
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
assert parsed["kind"] == "composite"
|
|
||||||
assert parsed["name"] == "workflow-script"
|
|
||||||
assert parsed["version"] == 1
|
|
||||||
assert isinstance(parsed["layers"], list) and len(parsed["layers"]) >= 2, (
|
|
||||||
"WF-COMP-31: the composite must layer at least shared-variables/v1 + "
|
|
||||||
"cp-service-token/v1; missing one breaks the credential env seam."
|
|
||||||
)
|
|
||||||
|
|
||||||
refs = []
|
|
||||||
for layer in parsed["layers"]:
|
|
||||||
assert isinstance(layer, dict) and len(layer) == 1, (
|
|
||||||
f"each layer entry must be a single-key dict (kind: ref), got {layer}"
|
|
||||||
)
|
|
||||||
kind_name, ref = next(iter(layer.items()))
|
|
||||||
assert kind_name in _KIND_TO_DIR, f"unknown layer kind {kind_name!r}"
|
|
||||||
assert "/" in ref, f"layer ref must be '<name>/v<N>', got {ref!r}"
|
|
||||||
name, ver = ref.split("/")
|
|
||||||
assert ver.startswith("v") and ver[1:].isdigit(), (
|
|
||||||
f"layer ref version must match v<N>, got {ver!r}"
|
|
||||||
)
|
|
||||||
kind_dir = _KIND_TO_DIR[kind_name]
|
|
||||||
layer_path = _HARNESSES_ROOT / kind_dir / name / ver / "harness.yaml"
|
|
||||||
assert layer_path.exists(), (
|
|
||||||
f"WF-COMP-31: composite references a layer that does not exist on "
|
|
||||||
f"the tree: {layer_path.relative_to(_HARNESSES_ROOT)}"
|
|
||||||
)
|
|
||||||
layer_data = yaml.safe_load(layer_path.read_text(encoding="utf-8"))
|
|
||||||
assert layer_data["name"] == name, (
|
|
||||||
f"name mismatch in {layer_path}: file says {layer_data['name']!r}, "
|
|
||||||
f"ref says {name!r}"
|
|
||||||
)
|
|
||||||
refs.append((kind_name, name, ver, layer_data))
|
|
||||||
|
|
||||||
# WF-COMP-31: the two SPECIFIED layers must both be present.
|
|
||||||
ref_names = {(kind, name) for kind, name, _, _ in refs}
|
|
||||||
assert ("context", "shared-variables") in ref_names, (
|
|
||||||
"WF-COMP-31: workflow-script/v1 MUST layer shared-variables/v1 "
|
|
||||||
"(WV-19 — the agent-facing /variables/ contract)"
|
|
||||||
)
|
|
||||||
assert ("context", "cp-service-token") in ref_names, (
|
|
||||||
"WF-COMP-31 / AU-51 credential env seam: workflow-script/v1 MUST "
|
|
||||||
"layer cp-service-token/v1 so CP_SERVICE_TOKEN_FILE is in the "
|
|
||||||
"entrypoint process env before shell_runner runs."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_composite_brings_no_model_or_endpoint_layer():
|
|
||||||
"""``kind: script`` nodes run via the shell runner — the composite MUST
|
|
||||||
NOT bring agent-only layers (no model, endpoint, CLAUDE.md context that
|
|
||||||
drives the agent runner, etc.). This is a structural regression gate: a
|
|
||||||
future refactor that adds e.g. ``anthropic-cloud-paul-oauth/v1`` here
|
|
||||||
would silently widen the script-node attack surface.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31 ("the flag-on kind: script
|
|
||||||
harness ... MUST NOT bring agent-only layers").
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
forbidden_substrings = (
|
|
||||||
# cloud-auth contexts the agent runner needs
|
|
||||||
"anthropic-cloud", "minimax", "qwen-code", "deepseek-code",
|
|
||||||
"alibaba-model-studio", "fable-low-effort", "z-ai",
|
|
||||||
# the model-bearing capability / context layers
|
|
||||||
"code-methodology", "spec-writing", "planning", "integration",
|
|
||||||
# harness composites that bundle a model
|
|
||||||
"code-", "planning-", "spec-writing-", "best-practices-",
|
|
||||||
"sonnet-", "opus-", "minimax-", "haiku-", "airouter-", "fable-",
|
|
||||||
)
|
|
||||||
layer_names = [
|
|
||||||
next(iter(layer.values())).split("/")[0]
|
|
||||||
for layer in parsed["layers"]
|
|
||||||
]
|
|
||||||
bad = [
|
|
||||||
n for n in layer_names
|
|
||||||
if any(sub in n for sub in forbidden_substrings)
|
|
||||||
]
|
|
||||||
assert not bad, (
|
|
||||||
f"WF-COMP-31: workflow-script/v1 MUST NOT bring agent-only layers; "
|
|
||||||
f"found: {bad}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cp_service_token_layer_already_sets_cp_service_token_file_env():
|
|
||||||
"""WF-COMP-31 'AU-51 credential env seam': cp-service-token/v1 must set
|
|
||||||
``CP_SERVICE_TOKEN_FILE`` in its env: declaration (the layer's init.sh
|
|
||||||
restages the ESO-mounted secret to an agent-readable 0600 copy at the
|
|
||||||
path CP_SERVICE_TOKEN_FILE points at). Verified here at the YAML level
|
|
||||||
rather than at runtime — the spec's claim is structural.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31; spec/harness.md AU-51/AU-51b
|
|
||||||
(bug b3a96acc).
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("contexts", "cp-service-token")
|
|
||||||
env = parsed["env"]
|
|
||||||
assert "CP_SERVICE_TOKEN_FILE" in env, (
|
|
||||||
"WF-COMP-31 'AU-51 credential env seam': cp-service-token/v1 must "
|
|
||||||
"declare CP_SERVICE_TOKEN_FILE in its env: block; without it the "
|
|
||||||
"shell_runner subprocess does not inherit the AU-51 credential."
|
|
||||||
)
|
|
||||||
fpath = env["CP_SERVICE_TOKEN_FILE"]
|
|
||||||
assert fpath.endswith("/token"), (
|
|
||||||
f"CP_SERVICE_TOKEN_FILE must point at the restaged 0600 file, not "
|
|
||||||
f"the raw 0400 ESO mount — got {fpath!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_composite_forge_credentials_are_read_only():
|
|
||||||
"""Any layer in workflow-script/v1 that carries a forge credential MUST
|
|
||||||
declare ``write_targets: []`` (HC-11): a script node may read the project
|
|
||||||
repo but never push. The write-capable gitea-ssh/v1 layer must not be
|
|
||||||
present (bug 45a48685).
|
|
||||||
|
|
||||||
Spec: spec/harness.md HC-11, spec/typed-workflows.md WF-COMP-34.
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
from tests.invariants.forge_credentials import _layer_has_forge_credentials
|
|
||||||
|
|
||||||
for layer in parsed["layers"]:
|
|
||||||
kind_name, ref = next(iter(layer.items()))
|
|
||||||
name = ref.split("/")[0]
|
|
||||||
assert name != "gitea-ssh", "workflow-script/v1 must not layer the write-capable gitea-ssh/v1 (bug 45a48685)"
|
|
||||||
kind_dir = _KIND_TO_DIR[kind_name]
|
|
||||||
sub = _load_layer(kind_dir, name)
|
|
||||||
if _layer_has_forge_credentials(sub):
|
|
||||||
assert sub.get("write_targets") == [], (
|
|
||||||
f"workflow-script/v1's layer {name} carries a forge credential "
|
|
||||||
f"but does not declare write_targets: [] (HC-11 read-only)."
|
|
||||||
)
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"""workflow-script-nocp/v1 and workflow-script-write/v1 (Option C, WF-COMP-31b/31c).
|
|
||||||
|
|
||||||
Layer order is load-bearing: shared-variables first, repo-readonly LAST
|
|
||||||
(WF-COMP-34), and neither composite may carry the AU-51 internal bearer
|
|
||||||
(cp-service-token/v1) or the write-capable gitea-ssh/v1. Only the write
|
|
||||||
composite carries the AU-55 per-step token layer.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from tests.invariants.forge_credentials import _layer_has_forge_credentials
|
|
||||||
|
|
||||||
_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
_KIND_TO_DIR = {"context": "contexts", "capability": "capabilities", "composite": "composites"}
|
|
||||||
|
|
||||||
_NOCP = [{"context": "shared-variables/v1"}, {"context": "repo-readonly/v1"}]
|
|
||||||
_WRITE = [
|
|
||||||
{"context": "shared-variables/v1"},
|
|
||||||
{"context": "cp-write-token/v1"},
|
|
||||||
{"context": "repo-readonly/v1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _load(kind_dir: str, name: str) -> dict:
|
|
||||||
return yaml.safe_load((_ROOT / kind_dir / name / "v1" / "harness.yaml").read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def _merged_env(doc: dict) -> dict:
|
|
||||||
env: dict = dict(doc.get("env") or {})
|
|
||||||
for layer in doc["layers"]:
|
|
||||||
(kind, ref), = layer.items()
|
|
||||||
env.update(_load(_KIND_TO_DIR[kind], ref.split("/")[0]).get("env") or {})
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"name,expected", [("workflow-script-nocp", _NOCP), ("workflow-script-write", _WRITE)]
|
|
||||||
)
|
|
||||||
def test_composite_layers_exact_order_repo_readonly_last(name, expected):
|
|
||||||
doc = _load("composites", name)
|
|
||||||
assert doc["kind"] == "composite"
|
|
||||||
assert doc["name"] == name
|
|
||||||
assert doc["version"] == 1
|
|
||||||
assert doc["layers"] == expected
|
|
||||||
assert doc["layers"][-1] == {"context": "repo-readonly/v1"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["workflow-script-nocp", "workflow-script-write"])
|
|
||||||
def test_composite_never_carries_internal_bearer_or_write_capable_forge_key(name):
|
|
||||||
doc = _load("composites", name)
|
|
||||||
names = [next(iter(layer.values())).split("/")[0] for layer in doc["layers"]]
|
|
||||||
assert "cp-service-token" not in names
|
|
||||||
assert "gitea-ssh" not in names
|
|
||||||
for layer in doc["layers"]:
|
|
||||||
(kind, ref), = layer.items()
|
|
||||||
sub = _load(_KIND_TO_DIR[kind], ref.split("/")[0])
|
|
||||||
if _layer_has_forge_credentials(sub):
|
|
||||||
assert sub.get("write_targets") == [], f"{ref} must be read-only (HC-11)"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["workflow-script-nocp", "workflow-script-write"])
|
|
||||||
def test_every_referenced_layer_exists_and_names_match(name):
|
|
||||||
doc = _load("composites", name)
|
|
||||||
for layer in doc["layers"]:
|
|
||||||
(kind, ref), = layer.items()
|
|
||||||
lname, ver = ref.split("/")
|
|
||||||
path = _ROOT / _KIND_TO_DIR[kind] / lname / ver / "harness.yaml"
|
|
||||||
assert path.exists(), path
|
|
||||||
assert yaml.safe_load(path.read_text())["name"] == lname
|
|
||||||
|
|
||||||
|
|
||||||
def test_nocp_composite_exports_no_cp_credential_env():
|
|
||||||
env = _merged_env(_load("composites", "workflow-script-nocp"))
|
|
||||||
assert "CP_SERVICE_TOKEN_FILE" not in env
|
|
||||||
assert "CP_WRITE_TOKEN_FILE" not in env
|
|
||||||
assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables"
|
|
||||||
|
|
||||||
|
|
||||||
def test_write_composite_env_contract():
|
|
||||||
env = _merged_env(_load("composites", "workflow-script-write"))
|
|
||||||
assert env["CP_WRITE_TOKEN_FILE"] == "/run/agent/cp-write-token/token"
|
|
||||||
assert "CP_SERVICE_TOKEN_FILE" not in env
|
|
||||||
assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables"
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_write_composite_layers_and_env_contract():
|
|
||||||
"""Spec WF-COMP-31c test handle (framework repo)."""
|
|
||||||
doc = _load("composites", "workflow-script-write")
|
|
||||||
assert doc["layers"] == _WRITE
|
|
||||||
assert _merged_env(doc)["CP_WRITE_TOKEN_FILE"] == "/run/agent/cp-write-token/token"
|
|
||||||
Reference in New Issue
Block a user