32 Commits

Author SHA1 Message Date
Paul O'Reilly
c67d9736fd R3b [fw]: delete legacy workflow-script/v1 composite and cp-service-token/v1 context (dbc5efc6)
Control plane 0.0.5.1890 no longer falls back to either. Invariant tests now
assert both stay gone.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-19 01:41:44 +13:00
Paul O'Reilly
d0424879c2 R2: remove AU-51 bearer layer from planning/spec-writing model composites; delete retired @1 templates (dbc5efc6)
The @2 templates use CP-side verdict/WIA decompose and never read the bearer.
Triggers already pin version 2.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-19 00:24:55 +13:00
Paul O'Reilly
37a94dcd07 fw: A8 template migration (drop on_fail: continue) and cp-docs-token/v1 layer (be110e25, 1d7ef09a)
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-17 20:52:35 +13:00
Paul O'Reilly
44334a2143 docs: end-session 2026-10-13 (CONTEXT.md, session log, benchmarks/ + model-slug note in CLAUDE.md) 2026-10-13 14:47:57 +13:00
Paul O'Reilly
418d634afa benchmarks: add Artificial Analysis snapshot and placeholder M3.1-Flash estimate
- artificial-analysis/2026-10-13.csv: Anthropic, Qwen, MiniMax-M3 rows with
  cost per task and derived cost per intelligence point
- minimax-m3-1-flash-unverified.md: operator-supplied KingBench figures, unverified
- estimates.csv: TODO placeholder for M3.1 Flash (0.9x M3 cost, index scaled by
  KingBench ratio) -- replace with measured/published values
2026-10-13 14:33:53 +13:00
Paul O'Reilly
d7decc1dec minimax: move to MiniMax-M3.1-Flash-Preview; drop bare minimax/minimax-m2 slugs
- new context minimax-m31-flash/v1 (model pin; no thinking-budget init, Flash
  ignores it); four composites switch from minimax-m3-max/v1 (kept for rollback)
- models + registry for minimax-m31-flash-{max,medium}; removes the dangling
  model-registry/minimax.yaml (endpoint "minimax" never existed)
- tiers, harness-roles, minimax-* task templates and the *-review-loop@1
  workflows use the Flash slugs (MiniMax silently maps unknown names to M3, so
  the provider-id translation lands in agent-runtimes)
2026-10-13 10:55:03 +13:00
c8c9e7f7d0 Merge pull request 'workflow-script-nocp-norepo/v1: secret-free script composite' (#5) from feat/script-composite-norepo into main 2026-10-12 03:21:37 +00:00
Paul O'Reilly
57374c8bfb feat(harness): workflow-script-nocp-norepo/v1 secret-free script composite (e993c9fa)
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-11 22:49:52 +13:00
Paul O'Reilly
e11b448f00 feat(agent-repo): opt-in AR-42 archive-and-reset of a stale task branch (f910e2ba)
init.sh now archives and resets an existing task/<uuid> branch to the current
base when AGENT_BRANCH_RESET_ON_START=true. Default off: all nodes of a
workflow share the branch, so only the workflow's first node may set it.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-09 14:13:46 +13:00
Paul O'Reilly
e64d0a1822 feat(workflow): cpw-smoke@1 one-node cp_write template for Option C smoke run (690b330f)
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-05 16:18:18 +13:00
Paul O'Reilly
a1c708ac96 fix(harness): add -hatchmate composites for sonnet, haiku, deepseekv4flash (bug 678d4e8e)
Hatchmate templates resolving to claude-sonnet-5, claude-haiku-4-5 or
airouter-deepseekv4flash mounted only gitea-ssh/v1 and could not clone
hatchmate/hatchmate-agents. Mirror the existing -hatchmate composites:
non-hatchmate base layers plus gitea-ssh-hatchmate/v1 after gitea-ssh/v1 (HC-26).

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-04 21:13:32 +13:00
Paul O'Reilly
887578f0eb feat(templates): explicit cp_write on every kind: script node + audit test (WF-COMP-31d, item 26754227)
cp_write: true  -> state-tx (test-validate@1, frozen-fixture-emit@1) and, in
                   the archived bug-ancestor-auto-advance@1, tag-set + state-tx.
cp_write: false -> every other script node (verdict-gate, policy-pick,
                   preflight-*, frozen-fixture-emit, parent-link-check).
tests/invariants/test_cp_write_field_set_correctly.py pins the set.

HOLD: do not merge until the CP release that adds WorkflowNode.cp_write.
The current CP schema is extra="forbid" and rejects the new key
(verified: nodes.0.cp_write extra_forbidden), so on the old CP these
template edits are NOT inert.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-04 09:36:04 +13:00
Paul O'Reilly
08ca057687 feat(harness): workflow-script-nocp/v1 + workflow-script-write/v1 composites (Option C, WF-COMP-31b/31c, item 26754227)
nocp: shared-variables/v1 + repo-readonly/v1 (no CP credential).
write: shared-variables/v1 + cp-write-token/v1 + repo-readonly/v1.
repo-readonly/v1 is last in both (WF-COMP-34); neither layers
cp-service-token/v1 or gitea-ssh/v1.

HOLD: do not merge until the CP release (AU-55/55a/56/57, WF-COMP-31b engine/
dispatch/compiler composite-set changes) and the dispatcher image
(cp-write-token delivery, AU-55b) are deployed. The dispatcher advertises every
composite found in the CRS-synced harnesses/ tree (dispatcher/poller.py
_collect_supported_harnesses), so merging this to origin/main IS the
advertisement event.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-04 09:35:34 +13:00
Paul O'Reilly
3717fc8167 feat(harness): cp-write-token/v1 context layer (Option C, WF-COMP-31c, item 26754227)
Env-only layer exporting CP_WRITE_TOKEN_FILE=/run/agent/cp-write-token/token.
The per-step wfcw_ token is delivered by the dispatcher into the script
container only (AU-55b), so the layer has no secrets_required, init script or
files. Not referenced by any composite yet; safe to merge at any time.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-04 09:35:07 +13:00
Paul O'Reilly
ea7c11265b fix(repo-readonly): own read-only deploy key; workflow-script drops write-capable gitea-ssh/v1
repo-readonly/v1 now declares write_targets: [], an ssh_hosts alias and its
own secrets_required (account platform-readonly.space:platform, a read-only
Gitea deploy key), and stages the key before cloning. workflow-script/v1 no
longer layers gitea-ssh/v1, so script nodes cannot push. Invariants updated:
credential layers in the composite must declare write_targets: [].

Bug 45a48685-23e9-4605-af1f-d93306108cef

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-02 20:09:30 +13:00
Paul O'Reilly
d850a4da68 fix(repo-readonly): clone when /workspace/project is empty, not only when absent (bug eede60c3)
harness_init pre-creates an empty /workspace/project, so the -d guard always skipped the clone.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-02 17:21:35 +13:00
Paul O'Reilly
4b41236e26 feat(harness): repo-readonly/v1 + gitea-ssh layer in workflow-script/v1; agent_repo on test-validate/frozen-fixture-emit (WF-COMP-34, bug 3bee78fa/a3ba44bc) 2026-10-02 16:18:21 +13:00
Paul O'Reilly
8d7b3af77d fix(templates): add AGENT_UPSTREAM_REPO_URL/BRANCH to all workflow templates using the agent fork
Fresh agent branches were seeded from the stale fork main (AR-39 fell through to fork).
Work item 18efa01f-f941-4f16-bb67-9cafbb6d668a.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-02 15:03:34 +13:00
Paul O'Reilly
c91813de96 fix(agent-repo): AR-39b run base selector by absolute path; seed scope-decompose@2 from upstream
init.sh invoked 'python3 -m entrypoint.agent_repo_base' from /workspace/project, so a project
repo's own entrypoint/ shadowed the image's and the selector crashed. Invoke the image copy by
path. scope-decompose-sonnet@2 gains AGENT_UPSTREAM_REPO_URL/BRANCH so fresh spec branches seed
from upstream main instead of the stale fork main.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-02 14:54:37 +13:00
Paul O'Reilly
fce343cea1 test: drop archived bug-ancestor-auto-advance@1 from state-tx/CP_URL discovery sets
Template moved to _archived/ (c7da48d7); glob discovery no longer sees it.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 19:37:36 +13:00
Paul O'Reilly
71363a77bb merge: archive bug-ancestor-auto-advance@1 (replaced by CRS patch_item rule bug-milestone-auto-resolve)
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 18:19:23 +13:00
Paul O'Reilly
1e236b07d0 merge: workflow-script/v1, state-tx runtime_env + FROM_STATES, ingress CP_URL
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 18:19:22 +13:00
Paul O'Reilly
e51146fc30 fix: task templates use ingress CP_URL (https://agents.oreillyit.nz/api), not svc.cluster.local:8100
The deploy repo NetworkPolicy admits :8100 only from named apps and the traefik/cms-proxy
namespaces; agent/script pods time out, so state-tx / cp-cli calls silently lost transitions.
Invariant now rejects the in-cluster form and pins the ingress URL.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 18:14:30 +13:00
Paul O'Reilly
782eadb615 feat: STATE_TX_FROM_STATES on state-tx nodes; workflow-script composite invariant; refresh legacy-mount NOTE
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 17:02:29 +13:00
Paul O'Reilly
9e2dd681a7 merge: mm/fw-script (workflow-script/v1 composite) 2026-10-01 17:02:01 +13:00
Paul O'Reilly
c5a5293edc fix: declare runtime_env on state-tx nodes in test-validate@1 and frozen-fixture-emit@1
state-tx exits 2 without CP_URL and STATE_TX_NEW_STATE. Each template's
target state is its own output.state (test-validated / test-skeleton-ready).
Adds an invariant test.

Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-10-01 16:08:39 +13:00
Paul O'Reilly
b549a28187 feat(harnesses): shared-variables/v1 context + workflow-script/v1 composite (WF-COMP-31, WV-19) (03878b12-86b5-4b88-86d5-7182ae1c471f) 2026-10-01 15:31:48 +13:00
Paul O'Reilly
d727e17825 chore(workflow-templates): archive bug-ancestor-auto-advance@1.yaml per WT-ANC-15 step 2 (c7da48d7-1688-422b-8108-1119e819f9ea) 2026-10-01 15:25:27 +13:00
Paul O'Reilly
58612d9734 feat: AR-39b — scope-decompose-sonnet@2 + bug-verify-sonnet@2 (WV-36 file-write plan, WF-COMP-25a self-transition) (31553852-e6c7-40e3-8020-b3b65094c1cc) 2026-10-01 14:24:26 +13:00
Paul O'Reilly
da23760e93 fix(models): airouter-qwen3 default_flags.model Qwen3.6 -> Qwen3.8 (live id; agent-runtimes c3551e3a)
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
2026-09-30 11:46:24 +13:00
f64be1d467 fix(gitea-ssh-hatchmate): correct account_ref space id (hatchmate, not platform) (#4)
Merge PR #4
2026-09-18 02:18:46 +00:00
c89cb5beab Merge pull request 'feat(forge-credential): hatchmate working-repo key layer + 4 composite forks' (#3) from feat/hatchmate-per-project-fork-key into main
Merge PR #3
2026-09-18 02:05:48 +00:00
79 changed files with 1547 additions and 322 deletions

View File

@@ -49,8 +49,12 @@ task-templates/ # dispatch-task --template <name> YAML files
workflows/ # Workflow DAG definitions
model-registry/ # Model scoring YAMLs (capability dimensions)
models/ # Model endpoint YAMLs (URLs, auth, parameters)
benchmarks/ # Reference data for the scores/costs pass (NOT read by any loader):
# artificial-analysis/ snapshot + README, unverified/estimate files
```
**Model slugs:** a registry slug is not a provider model id. MiniMax silently maps unknown model names to MiniMax-M3, so each `models/<slug>.yaml` must carry the real id in `default_flags.model` (agent-runtimes translates slug -> id on every runner). One `models/` + `model-registry/` file per effort level (e.g. `minimax-m31-flash-{max,medium}`).
## Harness Conventions
### harness.yaml fields

7
CONTEXT.md Normal file
View File

@@ -0,0 +1,7 @@
# CONTEXT.md — agent-runtime-framework
## Active
- **MiniMax Flash migration done; scores/costs are placeholders.** `minimax-m31-flash-{max,medium}` (models/ + model-registry/) and context `minimax-m31-flash/v1` are live on the CP (d7decc1). Registry scores are copies of the M3 entries with costs just below them. Planner FUTURE `629829d3` (agent-runtimes) tracks replacing them and the stop-gap `benchmarks/estimates.csv` M3.1-Flash row (0.9x M3 cost, index 61.5 — implausible). See `benchmarks/artificial-analysis/README.md` for the AA snapshot, slug mapping and caveats.
- **Unverified end-to-end:** dispatch a real hatchmate task and check the model the API reports is `MiniMax-M3.1-Flash-Preview` (agent-runtimes 0.0.5.1817 translates slug -> provider id on all runners).
- Untracked, left alone: `.claude/` (worktrees), `tests/__init__.py`.

View File

@@ -0,0 +1,35 @@
model,creator,context,intelligence_index,cost_per_task_usd,cost_per_intelligence_point_usd,median_tokens_per_s
Claude Opus 5.5 (max with fallback),Anthropic,1M,58,5.98,0.1031,96
Claude Sonnet 5.5 (max with fallback),Anthropic,1M,56,5.46,0.0975,135
Claude Opus 5.5 (xhigh with fallback),Anthropic,1M,56,3.46,0.0618,81
Claude Opus 5.5 (high with fallback),Anthropic,1M,54,1.82,0.0337,82
Claude Fable 5.1 (max with fallback),Anthropic,1M,53,7.63,0.1440,67
Claude Fable 5.1 (xhigh with fallback),Anthropic,1M,53,5.98,0.1128,62
Claude Sonnet 5.5 (xhigh with fallback),Anthropic,1M,52,2.01,0.0387,103
Claude Opus 5.5 (medium with fallback),Anthropic,1M,51,1.34,0.0263,79
Claude Fable 5.1 (high with fallback),Anthropic,1M,51,3.91,0.0767,52
Claude Fable 5.1 (medium with fallback),Anthropic,1M,49,2.98,0.0608,54
Claude Fable 5.1 (low with fallback),Anthropic,1M,47,2.37,0.0504,56
Claude Sonnet 5.5 (high with fallback),Anthropic,1M,47,0.88,0.0187,96
Claude Haiku 5.5 (max),Anthropic,1M,43,0.21,0.0049,236
Claude Opus 5.5 (low with fallback),Anthropic,1M,42,0.55,0.0131,80
Claude Haiku 5.5 (xhigh),Anthropic,1M,41,0.12,0.0029,187
Claude Sonnet 5.5 (medium with fallback),Anthropic,1M,41,0.48,0.0117,95
Claude Haiku 5.5 (high),Anthropic,1M,38,0.08,0.0021,175
Claude Sonnet 5.5 (low with fallback),Anthropic,1M,36,0.35,0.0097,100
Claude Haiku 5.5 (medium),Anthropic,1M,34,0.05,0.0015,171
Claude Haiku 5.5 (low),Anthropic,1M,29,0.02,0.0007,164
MiniMax-M3,MiniMax,1M,29,0.51,0.0176,107
Qwen3.8 Max (0902),Alibaba,984k,45,5.41,0.1202,36
Qwen3.8 2.4T A95B,Alibaba,262k,40,2.16,0.0540,37
Qwen3.8-Flash-Next,Alibaba,256k,40,0.37,0.0092,51
Qwen3.8 27B (xhigh),Alibaba,256k,34,1.01,0.0297,46
Qwen3.8 27B (medium),Alibaba,256k,28,1.13,0.0404,50
Qwen3.8 27B (low),Alibaba,256k,26,1.05,0.0404,53
Qwen3.7 Plus,Alibaba,1M,25,0.22,0.0088,55
Qwen3.8 27B (non-reasoning),Alibaba,256k,20,2.49,0.1245,52
Qwen3.5 397B A17B,Alibaba,262k,18,0.47,0.0261,88
Qwen3.6 35B A3B,Alibaba,262k,18,0.48,0.0267,129
Qwen3.5 122B A10B,Alibaba,262k,16,0.32,0.0200,129
Qwen3.5 9B,Alibaba,262k,11,0.21,0.0191,58
Qwen3 Coder Next,Alibaba,256k,9,0.78,0.0867,73
1 model creator context intelligence_index cost_per_task_usd cost_per_intelligence_point_usd median_tokens_per_s
2 Claude Opus 5.5 (max with fallback) Anthropic 1M 58 5.98 0.1031 96
3 Claude Sonnet 5.5 (max with fallback) Anthropic 1M 56 5.46 0.0975 135
4 Claude Opus 5.5 (xhigh with fallback) Anthropic 1M 56 3.46 0.0618 81
5 Claude Opus 5.5 (high with fallback) Anthropic 1M 54 1.82 0.0337 82
6 Claude Fable 5.1 (max with fallback) Anthropic 1M 53 7.63 0.1440 67
7 Claude Fable 5.1 (xhigh with fallback) Anthropic 1M 53 5.98 0.1128 62
8 Claude Sonnet 5.5 (xhigh with fallback) Anthropic 1M 52 2.01 0.0387 103
9 Claude Opus 5.5 (medium with fallback) Anthropic 1M 51 1.34 0.0263 79
10 Claude Fable 5.1 (high with fallback) Anthropic 1M 51 3.91 0.0767 52
11 Claude Fable 5.1 (medium with fallback) Anthropic 1M 49 2.98 0.0608 54
12 Claude Fable 5.1 (low with fallback) Anthropic 1M 47 2.37 0.0504 56
13 Claude Sonnet 5.5 (high with fallback) Anthropic 1M 47 0.88 0.0187 96
14 Claude Haiku 5.5 (max) Anthropic 1M 43 0.21 0.0049 236
15 Claude Opus 5.5 (low with fallback) Anthropic 1M 42 0.55 0.0131 80
16 Claude Haiku 5.5 (xhigh) Anthropic 1M 41 0.12 0.0029 187
17 Claude Sonnet 5.5 (medium with fallback) Anthropic 1M 41 0.48 0.0117 95
18 Claude Haiku 5.5 (high) Anthropic 1M 38 0.08 0.0021 175
19 Claude Sonnet 5.5 (low with fallback) Anthropic 1M 36 0.35 0.0097 100
20 Claude Haiku 5.5 (medium) Anthropic 1M 34 0.05 0.0015 171
21 Claude Haiku 5.5 (low) Anthropic 1M 29 0.02 0.0007 164
22 MiniMax-M3 MiniMax 1M 29 0.51 0.0176 107
23 Qwen3.8 Max (0902) Alibaba 984k 45 5.41 0.1202 36
24 Qwen3.8 2.4T A95B Alibaba 262k 40 2.16 0.0540 37
25 Qwen3.8-Flash-Next Alibaba 256k 40 0.37 0.0092 51
26 Qwen3.8 27B (xhigh) Alibaba 256k 34 1.01 0.0297 46
27 Qwen3.8 27B (medium) Alibaba 256k 28 1.13 0.0404 50
28 Qwen3.8 27B (low) Alibaba 256k 26 1.05 0.0404 53
29 Qwen3.7 Plus Alibaba 1M 25 0.22 0.0088 55
30 Qwen3.8 27B (non-reasoning) Alibaba 256k 20 2.49 0.1245 52
31 Qwen3.5 397B A17B Alibaba 262k 18 0.47 0.0261 88
32 Qwen3.6 35B A3B Alibaba 262k 18 0.48 0.0267 129
33 Qwen3.5 122B A10B Alibaba 262k 16 0.32 0.0200 129
34 Qwen3.5 9B Alibaba 262k 11 0.21 0.0191 58
35 Qwen3 Coder Next Alibaba 256k 9 0.78 0.0867 73

View File

@@ -0,0 +1,28 @@
# Artificial Analysis benchmark snapshot
Reference data for the model-registry scores/costs pass. NOT read by any loader
(RegistryEntry ignores unknown fields, so this lives beside the registry, not in it).
- **Source:** https://artificialanalysis.ai/leaderboards/models, extracted from the rendered
table on 2026-10-13 (Anthropic, Alibaba/Qwen and MiniMax rows only; 34 rows with a score and a cost).
- **File:** `2026-10-13.csv` — `intelligence_index` is the AA Intelligence Index; `cost_per_task_usd` is the
site's "Cost per Task (USD)".
- **`cost_per_intelligence_point_usd`** = `cost_per_task_usd / intelligence_index`. Derived here, not a site
column. Chosen as the cost factor to record (user decision, 2026-10-13).
- Rows the site marks with `*` (partial results, no cost shown) are omitted.
- Names are the site's labels. "with fallback" on most Claude rows is the site's wording; effort level is the
bracketed suffix.
- Attribution: data © Artificial Analysis. Re-extract rather than editing by hand.
## Mapping to registry slugs (informational, not authoritative)
| Registry slug | AA row(s) | Note |
|---|---|---|
| `minimax-m3`, `minimax-m3-max` | MiniMax-M3 (index 29, $0.51, 0.0176) | AA lists one M3 row with no effort level; both slugs share it |
| `minimax-m31-flash-{max,medium}` | none (see `../estimates.csv`) | MiniMax-M3.1-Flash-Preview is NOT on the leaderboard yet (checked 2026-10-13; no model page). **TODO(revisit):** `../estimates.csv` holds a placeholder estimate (cost 0.9x M3, index scaled by the KingBench ratio) — replace with measured values |
| `airouter-qwen38`, `airouter-qwen38-max` | Qwen3.8 family | `default_flags.model: "Qwen3.8"` does not say which size; AA has Qwen3.8 27B (low/medium/xhigh), 2.4T A95B, Max (0902), Flash-Next — confirm before mapping |
| `claude-fable-5-1` | Claude Fable 5.1 (low..max) | registry sets `reasoning_effort: low` (inert on the claude runner) |
| `claude-sonnet-5`, `claude-opus-5`, `claude-haiku-4-5` | Sonnet 5.5, Opus 5.5, Haiku 5.5 | slug/model-id names predate the 5.5 releases (`default_flags.model` is `claude-sonnet-5` etc.); verify which API model each actually calls before using these rows |
Cost per task is not comparable to `cost_per_1k_tokens`: it folds in how many tokens the model spends. Use it
for `cost_efficiency` scoring, not for the per-token price fields.

2
benchmarks/estimates.csv Normal file
View File

@@ -0,0 +1,2 @@
model,creator,context,intelligence_index,cost_per_task_usd,cost_per_intelligence_point_usd,status,basis
MiniMax-M3.1-Flash-Preview (effort unspecified),MiniMax,1M,61.5,0.46,0.0075,ESTIMATE-TODO,"cost = 0.9 x MiniMax-M3 AA cost per task ($0.51); index = M3 AA index (29) x KingBench-3 ratio (66.25/31.25 = 2.12); both inputs unverified/placeholder"
1 model creator context intelligence_index cost_per_task_usd cost_per_intelligence_point_usd status basis
2 MiniMax-M3.1-Flash-Preview (effort unspecified) MiniMax 1M 61.5 0.46 0.0075 ESTIMATE-TODO cost = 0.9 x MiniMax-M3 AA cost per task ($0.51); index = M3 AA index (29) x KingBench-3 ratio (66.25/31.25 = 2.12); both inputs unverified/placeholder

View File

@@ -0,0 +1,23 @@
# MiniMax-M3.1-Flash-Preview — ad-hoc, UNVERIFIED figures
Supplied by the operator on 2026-10-13 with no source URL. Not from Artificial Analysis (the model is not on
that leaderboard yet — see `artificial-analysis/README.md`). Do not use for registry scores until confirmed.
| Item | Figure | Note |
|---|---|---|
| KingBench 3, M3.1 Flash | 66.25% (53/80) | "community and third-party testing"; arithmetic checks (53/80 = 66.25%) |
| KingBench 3, M3 baseline | 31.25% (25/80) | arithmetic checks; the source also mentions "alternative baseline figures depending on the evaluation set" |
| M3 SWE-bench Pro | ~59% | reference baseline for M3, described as official |
| M3 "Browse.com" | 83.5 | benchmark name looks mistyped (BrowseComp?) — confirm before citing |
| M3 Terminal Bench 2.1 | "strong" | no number given |
Caveats: unknown effort level for the KingBench runs (Flash always thinks; server default is max); KingBench 3 is
small (80 items) so one item is 1.25 points; none of these figures share a scale with the AA Intelligence Index
(M3 = 29 there), so they cannot be mixed into one cost-per-intelligence figure.
## TODO(revisit) — placeholder estimate in use (2026-10-13)
`estimates.csv` carries a stop-gap row for M3.1 Flash: cost per task = 90% of M3's ($0.51 -> $0.46) and
intelligence index = M3's AA index (29) scaled by the KingBench-3 ratio (66.25/31.25 = 2.12) = 61.5. That index
would rank it above Claude Opus 5.5 max (58), which is implausible for a Flash-class model — treat both numbers as
rough placeholders. Replace with (a) the Artificial Analysis row when the model is listed, or (b) our own measured
cost per task at each effort level (fixed task set via `ask-minimax-run --output-format json`).

View File

@@ -6,6 +6,6 @@ description: "Best practices review agent on MiniMax M3 (max effort) + repo clon
layers:
- context: best-practices/v1
- context: minimax/v1
- context: minimax-m3-max/v1
- context: minimax-m31-flash/v1
- context: gitea-ssh/v1
- context: agent-repo/v1

View File

@@ -0,0 +1,20 @@
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

View File

@@ -0,0 +1,15 @@
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

View File

@@ -0,0 +1,15 @@
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

View File

@@ -9,7 +9,7 @@ layers:
# personal-subscription OAuth — integration runs are frequent/mechanical
# and were draining the interactive 5-hour window (2026-09-02).
- context: minimax/v1
- context: minimax-m3-max/v1
- context: minimax-m31-flash/v1
- context: gitea-ssh/v1
- context: agent-repo/v1
- context: direct-push/v1

View File

@@ -8,8 +8,3 @@ layers:
- context: anthropic-cloud-paul-oauth/v1
- context: gitea-ssh/v1
- context: agent-repo/v1
# AU-51/AU-51b: this composite backs bug-verify-sonnet@1, whose agent node
# self-transitions its own item's flow_state (PATCH /v1/work-items/{id})
# as its final step — see work item 88ecd23e ("agent self-transition"
# pattern, spec/typed-workflows.md). Needs a CP write credential.
- context: cp-service-token/v1

View File

@@ -8,6 +8,6 @@ layers:
# MiniMax M3 — scaffolding is mechanical (stub files so tests import);
# moved off the personal Anthropic subscription 2026-09-02.
- context: minimax/v1
- context: minimax-m3-max/v1
- context: minimax-m31-flash/v1
- context: gitea-ssh/v1
- context: agent-repo/v1

View File

@@ -8,6 +8,3 @@ layers:
- context: anthropic-cloud-paul-oauth/v1
- context: gitea-ssh/v1
- context: agent-repo/v1
# AU-51/AU-51b: this composite backs scope-decompose-sonnet@1, which
# calls back POST /v1/work-items/{id}/decompose — needs a credential.
- context: cp-service-token/v1

View File

@@ -9,6 +9,6 @@ layers:
# 2026-09-02 (the "credential layer swappable" swap this composite was
# designed for).
- context: minimax/v1
- context: minimax-m3-max/v1
- context: minimax-m31-flash/v1
- context: gitea-ssh/v1
- context: agent-repo/v1

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +1,16 @@
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

View File

@@ -0,0 +1,16 @@
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

View File

@@ -65,6 +65,41 @@ if [ -n "${AGENT_REPO_URL:-}" ] && [ -n "${AGENT_BRANCH:-}" ]; then
echo "Cloning agent working repo: $AGENT_REPO_URL (branch: $AGENT_BRANCH)"
if git clone --depth 1 --branch "$AGENT_BRANCH" "$AGENT_REPO_URL" /workspace/project 2>/dev/null; then
echo "Cloned existing branch $AGENT_BRANCH (continuing prior work)"
# AR-42: archive the stale task branch and reset it to the current base.
# Opt-in per node (AGENT_BRANCH_RESET_ON_START=true, set by the workflow
# template on its FIRST node only): every node of a workflow shares this
# one branch, so an unconditional reset would wipe earlier nodes' commits.
if [ "${AGENT_BRANCH_RESET_ON_START:-}" = "true" ]; then
mkdir -p /workspace/.agent-output
AR42_TOOL=/opt/agent/agent_repo_base.py
AR42_FAIL() {
echo "ERROR: AR-42 $1" >&2
printf '{"error": "ar42_archive_and_reset_failed", "detail": %s}\n' \
"$(printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()[:500]))')" \
> /workspace/.agent-output/init-error.json
exit 1
}
AR42_SEL=$(python3 "$AR42_TOOL" select-base \
--workdir /workspace/project \
--branch "$AGENT_BRANCH" \
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \
--upstream-branch "${AGENT_UPSTREAM_BRANCH:-main}" \
--reference-dir /workspace/reference/main \
--out /workspace/.agent-output/branch_base.json 2>&1) \
|| AR42_FAIL "select-base failed: $AR42_SEL"
AR42_SHA=$(printf '%s\n' "$AR42_SEL" | sed -n 's/^sha=//p' | head -1)
[ -n "$AR42_SHA" ] || AR42_FAIL "select-base returned no sha: $AR42_SEL"
AR42_OUT=$(python3 "$AR42_TOOL" archive-and-reset \
--workdir /workspace/project \
--branch "$AGENT_BRANCH" \
--base-sha "$AR42_SHA" \
--fork-url "$AGENT_REPO_URL" \
--archive-prefix archive \
--base-metadata /workspace/.agent-output/branch_base.json \
--out /workspace/.agent-output/branch_base.json 2>&1) \
|| AR42_FAIL "archive-and-reset failed: $AR42_OUT"
echo "AR-42: $AGENT_BRANCH reset to base $AR42_SHA"
fi
else
echo "Branch $AGENT_BRANCH does not exist — seeding fresh branch from upstream reference"
# Full clone (not --depth 1) so we get a working remote for finalize.sh push.
@@ -75,7 +110,7 @@ if [ -n "${AGENT_REPO_URL:-}" ] && [ -n "${AGENT_BRANCH:-}" ]; then
# finalize.sh to merge into ci_metadata.json (AR-25). Falls back
# to the legacy inline AR-14a logic if the CLI is unavailable.
mkdir -p /workspace/.agent-output
AR39_OUT=$(python3 -m entrypoint.agent_repo_base select-base \
AR39_OUT=$(python3 /opt/agent/agent_repo_base.py select-base \
--workdir /workspace/project \
--branch "$AGENT_BRANCH" \
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \

View File

@@ -0,0 +1,48 @@
# Step documents (cp-docs-token)
This step can read and write planner documents through `python3 -m lib.cp_cli`
in step-docs mode (PLN-CLI-16). The mode is active because `CP_DOCS_TOKEN_FILE`
and `CP_DOCS_CA_FILE` are set and the listener URL is in `CP_DOCS_URL`.
## Credential
- The token is a file at `$CP_DOCS_TOKEN_FILE`. cp-cli reads it on every call.
- Never print, copy, log or pass the token on a command line.
- Do not use `~/.cpconfig`; step-docs mode does not read it.
## Allowed commands
Only these commands work; anything else exits 2 `cp_docs_mode_command_not_allowed`:
```
python3 -m lib.cp_cli planner items documents list <item-uuid>
python3 -m lib.cp_cli planner items documents get <item-uuid> <name> [--json]
python3 -m lib.cp_cli planner items documents put <item-uuid> <name> --file <path> (--create-only | --if-match <rev>)
python3 -m lib.cp_cli planner items documents related [--json]
```
Item arguments are full UUIDs. Do not pass a project id.
## Scope
- You may write only documents on this step's own item.
- You may read documents on this step's item, its parent and its siblings.
- `related` prints `item_uuid`, `parent_uuid`, `sibling_uuids` and `siblings_truncated`.
## Writes need a precondition
Exactly one of these is required on every `put` (neither or both exits 2):
- `--create-only` creates a new document (sends `If-None-Match: *`).
- `--if-match <rev>` updates an existing one (sends `If-Match`). Take `<rev>`
from `get --json` (the ETag revision); surrounding quotes are stripped.
## Errors
- 412 `precondition_failed`: the document changed or already exists. Re-run
`get --json`, merge your change into the current content, and `put` again
with the new revision.
- 428 (precondition missing): add `--create-only` or `--if-match`.
- 429 `step_docs_write_budget_exhausted` or `step_docs_rate_limited`: stop
writing; do not retry in a loop.
- 404 `not_found`, 400 `invalid_document_name`: fix the item uuid or name.

View File

@@ -0,0 +1,16 @@
kind: context
name: cp-docs-token
version: 1
description: "Step-docs mode env contract for lib.cp_cli (PLN-CLI-16): per-step wfcd_ token file and CA file delivered by the dispatcher; document access scoped to this step's item (spec/dispatcher.md D-40, spec/authentication.md AU-59)"
requires: []
provides: [cp-docs-token]
# Env-only layer: the dispatcher delivers the token and CA files into the
# container; this layer sets only the paths. No secrets_required, no init script.
env:
CP_DOCS_TOKEN_FILE: /run/agent/cp-docs/token
CP_DOCS_CA_FILE: /run/agent/cp-docs/ca.crt
context_files:
- source: ./CLAUDE.md
target: /opt/harness/context/cp-docs-token/CLAUDE.md

View File

@@ -1,30 +0,0 @@
kind: context
name: cp-service-token
version: 1
description: "CP-internal service bearer token — authenticates agent-container write calls back to the control plane (AU-51/AU-51b, bug b3a96acc)"
requires: []
provides: []
# AU-51b: the ESO-mounted secret is root-owned mode 0400 (init.sh runs as
# root, before the gosu drop to the agent user, uid 1000) — unreadable by
# the agent process directly. Confirmed live 2026-09-03: a real agent
# session hit exactly this ("cp-cli invocation can't proceed... token is
# root-owned mode 0400 so it's not readable by the agent user"). Mirrors
# anthropic-cloud-paul-oauth/v1's pattern: init.sh restages a 0600
# agent-owned copy outside the ESO mount. CP_SERVICE_TOKEN_FILE points at
# the restaged copy, not the raw mount.
#
# The value at this path MUST match the CP's CP_INTERNAL_BEARER_TOKEN
# (same account: cp-decompose-service-token, account_id
# 1d963673-6ac9-4f85-875a-2ce5323e76ad, owner (cp, cp)).
env:
CP_SERVICE_TOKEN_FILE: /run/agent/cp-service-token/token
scripts:
init: "./init.sh"
secrets_required:
- name: cp-service-token
account_ref: "1d963673-6ac9-4f85-875a-2ce5323e76ad"
mount_path: /run/agent/secrets/cp-service-token
mode: "0400"

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# cp-service-token init — restage the ESO-mounted secret to an agent-owned
# copy.
#
# Threat model: keep the ESO mount root-only (mode 0400) so the agent user
# cannot directly `cat` /run/agent/secrets/cp-service-token/*. init.sh runs
# as root (in uid-wrapper.sh, before the gosu drop) and writes a restaged
# copy at /run/agent/cp-service-token/token with mode 0600 owned by the
# agent user. This mirrors anthropic-cloud-paul-oauth/v1/init.sh's pattern.
#
# Bug discovered live 2026-09-03: without this restaging step, cp-cli
# (invoked by the agent user, uid 1000) could not read the raw 0400
# root-owned mount at all — "cp_cli invocation can't proceed... token is
# root-owned mode 0400 so it's not readable by the agent user". The context
# originally set CP_SERVICE_TOKEN_FILE directly at the raw ESO mount path,
# which is the mistake this init.sh fixes.
set -euo pipefail
ESO_TOKEN="/run/agent/secrets/cp-service-token/value"
if [ ! -r "$ESO_TOKEN" ]; then
echo "ERROR: $ESO_TOKEN not readable. Check ESO ExternalSecret acct-1d963673." >&2
exit 1
fi
AGENT_USER="${AGENT_USER:-agent}"
DEST_DIR="/run/agent/cp-service-token"
mkdir -p "$DEST_DIR"
DEST_FILE="$DEST_DIR/token"
cp "$ESO_TOKEN" "$DEST_FILE"
chown "$AGENT_USER:" "$DEST_DIR" "$DEST_FILE" 2>/dev/null || true
chmod 0700 "$DEST_DIR"
chmod 0600 "$DEST_FILE"
echo "cp-service-token: restaged into $DEST_FILE (0600 $AGENT_USER:$AGENT_USER)"

View File

@@ -0,0 +1,25 @@
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

View File

@@ -23,6 +23,6 @@ scripts:
secrets_required:
- name: gitea-ssh-hatchmate
account_ref: "hatchmate.space:platform"
account_ref: "hatchmate.space:hatchmate"
mount_path: /run/agent/secrets/gitea-ssh-hatchmate
mode: "0400"

View File

@@ -0,0 +1,17 @@
kind: context
name: minimax-m31-flash
version: 1
description: "Pin MiniMax-M3.1-Flash-Preview (1M context) — layer AFTER minimax/v1 (auth + base URL come from there). Thinking depth is set per dispatch via --effort (models/*.yaml default_flags.reasoning_effort), not here."
requires: []
provides: []
# Replaces minimax-m3-max/v1 (kept for rollback). Differences:
# - Flash always thinks and ignores MAX_THINKING_TOKENS/budget_tokens (effort is
# the only depth control, server default max), so there is no init.sh.
# - No [1m] suffix: Flash is 1M natively (verified 2026-10-13, modelUsage.contextWindow).
# - CLAUDE_CODE_AUTO_COMPACT_WINDOW stays: an unknown model id otherwise gets
# Claude Code's 200K default and compacts early.
env:
ANTHROPIC_MODEL: "MiniMax-M3.1-Flash-Preview"
ANTHROPIC_SMALL_FAST_MODEL: "MiniMax-M3.1-Flash-Preview"
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "1000000"

View File

@@ -0,0 +1,39 @@
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"

View File

@@ -0,0 +1,92 @@
#!/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 ==="

View File

@@ -0,0 +1,146 @@
# 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.

View File

@@ -0,0 +1,20 @@
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

View File

@@ -1,27 +0,0 @@
# Session Log -- 2026-05-04
## Summary
Implemented 5 planned fixes for BUG-5 (`.agent-output/` silent drops) and BUG-20 (MiniMax finalize no-push): pre_test.sh harness hook, output validation in finalize, push retry with debug tracing, spec-planning workflow output tags, and wrong-path detection with agent correction re-invoke. Fork cleanup PR merged, agent-monitor PR #44 merged.
## Decisions
- Fork cleanup workflow (PR #1 on agent-runtimes-agents): weekly cron deletes `task-*` branches inactive for 7+ days; main reset to orphan commit if no commits for 7+ days. Supports dry-run via workflow_dispatch. **Merged** (2026-05-04 10:39 UTC by admin).
- Gitea API token auth requires HTTP Basic Auth (`auth=("user","pass")`) not Bearer token — Bearer returns 401 even with valid access token. Use `httpx.get(..., auth=("user","pass"))` pattern.
- Shallow clone (`--depth 1`) for task branches in fork cleanup to avoid bloating with full history.
## Gotchas Discovered
- **[Gitea Gotchas]** Gitea API endpoint `/login` is `404 Not Found` — use `/users/<username>/access_tokens` to create tokens, or use HTTP Basic Auth directly against any `/api/v1/` endpoint with `auth=("user","pass")`.
- **[Gitea Gotchas]** PR state `closed` with `merged: true` means the PR was merged (not just closed). Gitea distinguishes merged vs closed states.
- **[Gitea Gotchas]** Fork PRs use the parent repo's Actions (has_actions=true on parent, `has_actions: false` on fork). Fork cleanup workflow runs on the fork's GHA because fork enables Actions independently.
- **[tokens]** Token file at `~/.config/agent-runtimes/tokens.json` uses flat structure: `{"access_token": "...", "expires_at": N}`. Token was valid (not expired at session continuation time: now 1777891716 vs expiry 1777891804).
## Key Context
- **agent-runtime-framework** `8bbf6cb`: pre_test.sh created (harness-level test folder revert), finalize.sh gains `set -x` tracing, push retry (`AGENT_PUSH_RETRIES`), output validation (`AGENT_EXPECTED_OUTPUT`), wrong-path detection + `.correction-prompt.txt` sentinel for entrypoint re-invoke. All 17 spec-planning workflow nodes tagged with `output: {path, min_bytes}`.
- **agent-runtimes-agents** fork: `fork-cleanup` branch merged to main (PR #1 by ai_enablement, merged by admin 2026-05-04). Workflow file at `.gitea/workflows/cleanup.yaml`.
- **agent-runtimes** `148d9d3`: PR #44 merged — `scripts/agent-monitor` gains `provider` and `model_full` columns.
- **agent-runtimes** local uncommitted: M22 Phase 3 manifests work (`controlplane/api/manifests.py`, `controlplane/db/manifest_store.py`, migration `20260504_0017_m16_phase3_manifests.py`, `controlplane/manifests/`). Not related to this session's focus.
- Two ops gates pending before M22 Phase 9 live cutover: CI build `agent-runtimes-init:1.0.0` image; `agent-session` Role deployed via `homelab/agent-runtimes-deploy` + ArgoCD sync.
## Process Notes
- When verifying PR state via Gitea API: parse JSON directly (don't assume specific keys — use `json.dumps(..., indent=2)` to inspect full structure).
- Fork cleanup workflow commits to `agent-runtimes-agents` fork main; parent `agent-runtimes` main unchanged. Fork's main tracked via `agent-runtimes-agents` remote in local `agent-runtime-framework` checkout.
- Fork cleanup's `fetch-depth: 0` is needed for commit date inspection even with shallow clones elsewhere.

View File

@@ -0,0 +1,32 @@
# Session Log -- 2026-10-13
## Summary
Moved the framework's MiniMax content to MiniMax-M3.1-Flash-Preview (new context, Flash models/registry, bare `minimax`/`minimax-m2` slugs removed; d7decc1), then added a `benchmarks/` directory with an Artificial Analysis snapshot, unverified M3.1 Flash figures, and a flagged placeholder estimate (418d634). Both pushed to `main`.
## Decisions
- Decision: new context `harnesses/contexts/minimax-m31-flash/v1` (env pin `MiniMax-M3.1-Flash-Preview`, no init.sh) replaces `minimax-m3-max/v1` in scaffolding-repo, integration-direct, test-writing-repo, best-practices-minimax-repo; the old context stays for rollback -- Rationale: Flash ignores MAX_THINKING_TOKENS; effort comes from `--effort` via `default_flags.reasoning_effort`.
- Decision: deleted `model-registry/minimax.yaml` (endpoint `minimax` never existed) and repointed the six `task-templates/minimax-*.yaml` and the three `*-review-loop@1` `model: minimax-m2` nodes to `minimax-m31-flash-max` -- Rationale: MiniMax silently maps unknown model names to M3.
- Decision: benchmark data lives in `benchmarks/` beside the registry, not inside registry entries -- Rationale: `RegistryEntry` (pydantic default extra=ignore) silently drops unknown fields, and the loader never reads this dir.
- Decision: cost factor recorded is "cost per intelligence point" = AA cost per task / AA Intelligence Index (derived; the site has no such column) -- Rationale: operator request.
- Decision: M3.1 Flash placeholder = 0.9x M3 cost per task ($0.46) and M3's index scaled by the KingBench-3 ratio (29 x 2.12 = 61.5), flagged `ESTIMATE-TODO` -- Rationale: operator stop-gap; 61.5 exceeds Opus 5.5 max (58), so implausible. Planner FUTURE item 629829d3 (agent-runtimes) tracks replacing it.
## Gotchas Discovered
- **[minimax]** Symptom: unknown model names (`minimax`, `minimax-m31-flash-max`) return HTTP 200 with `model: MiniMax-M3` -- Fix: send the provider id; translation now done by agent-runtimes ef822703 (deployed 0.0.5.1817). Check the echoed `model`.
- **[artificial-analysis]** Symptom: WebFetch of artificialanalysis.ai/models returns only headline numbers (tables are JS-rendered) -- Fix: open the page in Chrome (claude-in-chrome), wait ~4 s, read `document.querySelector('table')` rows via javascript_tool (269 rows on /leaderboards/models; columns: model, context, creator, index, cost per task, tokens/s, latency).
- **[artificial-analysis]** MiniMax-M3.1-Flash-Preview is not on the leaderboard (no row; /models/minimax-m3-1-* 404 as of 2026-10-13); only `MiniMax-M3` (index 29, $0.51/task) exists for MiniMax.
- **[benchmarks]** Cost vs intelligence correlates strongly only within one model family across effort levels (Claude: r 0.93-0.99, cost x4-6 per +10 index points); across vendors it is weak (r 0.53) and Qwen3.8 27B is inverted (cost does not fall with lower effort), so cross-vendor cost estimates for M3.1 Flash are unreliable.
- **[git]** The framework checkout was on the user's feature branch `feat/hatchmate-per-project-fork-key`; moving my uncommitted work to `main` needed `git stash push -- <paths>` (untracked new files stay in place), `checkout main`, ff-pull, `stash pop`.
## Open Questions
- Which Qwen3.8 size do `airouter-qwen38*` models actually call? (AA has 27B, 2.4T A95B, Max, Flash-Next.)
- Registry model ids `claude-sonnet-5` / `claude-opus-5` / `claude-haiku-4-5` predate the 5.5 releases AA lists -- verify before mapping AA rows to them.
- Source for the KingBench 3 figures; scoring rule to map AA index / cost per intelligence point onto the registry's 0-10 dimensions.
## Key Context
- Files: `benchmarks/artificial-analysis/{2026-10-13.csv,README.md}` (34 rows: Anthropic, Qwen, MiniMax-M3), `benchmarks/minimax-m3-1-flash-unverified.md`, `benchmarks/estimates.csv`.
- Live: CRS checkout for this repo is at d7decc1 on the controlplane (verified); 418d634 adds only benchmarks (nothing reads it).
- Registry scores/costs for `minimax-m31-flash-{max,medium}` are still placeholders (copies of the M3 entries; costs just under them).
## Process Notes
- Before rebasing onto a feature-branch checkout, check `git branch --show-current`; land work on `main` explicitly.
- Record derived metrics with their derivation (README) and mark stop-gap numbers with a TODO in the data file name/status column AND a durable planner item.

View File

@@ -5,5 +5,6 @@ roles:
claude-sonnet-5: code-sonnet-tdd-repo
claude-opus-5: code-opus-tdd-repo
claude-haiku-4-5: code-haiku-tdd-repo
minimax: code-minimax-tdd-repo
minimax-m31-flash-max: code-minimax-tdd-repo
minimax-m31-flash-medium: code-minimax-tdd-repo
claude-fable-5: code-fable-tdd-repo

View File

@@ -0,0 +1,21 @@
# agent-runtime-framework — model-registry/ (CRS tag: model-registry)
# Max-effort Flash; replaces minimax-m3-max in planning/draft-heavy and explicit-model nodes.
# PLACEHOLDER scores/costs: scores are copied from the M3 entry this replaces and
# costs are set just below it so cheapest-wins (tiers.yaml) selects Flash over
# the retained M3 entries. Revisit with observed scores and published pricing.
name: minimax-m31-flash-max
endpoint: minimax-m31-flash-max
provider: minimax
family: minimax
scores:
complexity: 10
test_pass_rate: 9
creativity: 10
spec_adherence: 9
cost_efficiency: 5
context_utilisation: 10
cost_per_1k_tokens:
input: 0.0025
output: 0.0125
source: benchmark
harness_version: null

View File

@@ -0,0 +1,21 @@
# agent-runtime-framework — model-registry/ (CRS tag: model-registry)
# Medium-effort Flash; replaces minimax-m3 in the build/integrate/spec-test/coding tiers.
# PLACEHOLDER scores/costs: scores are copied from the M3 entry this replaces and
# costs are set just below it so cheapest-wins (tiers.yaml) selects Flash over
# the retained M3 entries. Revisit with observed scores and published pricing.
name: minimax-m31-flash-medium
endpoint: minimax-m31-flash-medium
provider: minimax
family: minimax
scores:
complexity: 8
test_pass_rate: 9
creativity: 8
spec_adherence: 9
cost_efficiency: 6
context_utilisation: 9
cost_per_1k_tokens:
input: 0.00125
output: 0.00625
source: benchmark
harness_version: null

View File

@@ -1,15 +0,0 @@
name: minimax
endpoint: minimax
provider: minimax
scores:
complexity: 10
test_pass_rate: 9
creativity: 10
spec_adherence: 9
cost_efficiency: 3
context_utilisation: 10
cost_per_1k_tokens:
input: 0.0
output: 0.0
source: benchmark
harness_version: null

View File

@@ -22,7 +22,7 @@ tiers:
context_utilisation: 9
qualifies_today:
- deepseek-v4flash # benchmark-inflated 9s; hold to allowed_models ceiling
- minimax
- minimax-m31-flash-max
- claude-opus-5
note: >
WF-SCH-14 allowed_models ceiling remains authoritative until M7 observed-score
@@ -37,7 +37,8 @@ tiers:
spec_adherence: 9
test_pass_rate: 9
qualifies_today:
- minimax
- minimax-m31-flash-max
- minimax-m31-flash-medium
- claude-sonnet-5
- claude-opus-5
@@ -51,7 +52,8 @@ tiers:
qualifies_today:
- airouter-qwen3
- airouter-deepseekv4flash
- minimax
- minimax-m31-flash-max
- minimax-m31-flash-medium
- claude-sonnet-5
- claude-opus-5
- claude-fable-5

View File

@@ -6,7 +6,7 @@ endpoint:
api_key_env: AIROUTER_API_KEY
runner: agentic
default_flags:
model: "Qwen3.6"
model: "Qwen3.8" # live airouter id (c3551e3a); 3.6 was stale
provider_context_ref: "airouter/v1"
provider_secret_ref: "AIROUTER_API_KEY"
session_capable: false

View File

@@ -0,0 +1,21 @@
# agent-runtime-framework — models/ (CRS tag: models)
# MiniMax-M3.1-Flash-Preview at reasoning_effort max. Always thinks (thinking
# cannot be disabled; effort is the only depth control). The claude runner maps
# default_flags.reasoning_effort to `--effort` (agent-runtimes ME-6a). Verified
# 2026-10-13: the container MiniMax key reaches this model on both
# api.minimax.io and api.minimaxi.chat; 1M context window.
# Endpoint name MUST equal the registry name (MR-12).
name: minimax-m31-flash-max
provider: minimax
family: minimax
endpoint:
type: cloud_api
base_url: "https://api.minimax.io/anthropic"
api_key_env: MINIMAX_API_KEY
runner: claude
default_flags:
model: "MiniMax-M3.1-Flash-Preview"
reasoning_effort: max
provider_context_ref: "minimax/v1"
provider_secret_ref: "MINIMAX_API_KEY"
session_capable: false

View File

@@ -0,0 +1,21 @@
# agent-runtime-framework — models/ (CRS tag: models)
# MiniMax-M3.1-Flash-Preview at reasoning_effort medium. Always thinks (thinking
# cannot be disabled; effort is the only depth control). The claude runner maps
# default_flags.reasoning_effort to `--effort` (agent-runtimes ME-6a). Verified
# 2026-10-13: the container MiniMax key reaches this model on both
# api.minimax.io and api.minimaxi.chat; 1M context window.
# Endpoint name MUST equal the registry name (MR-12).
name: minimax-m31-flash-medium
provider: minimax
family: minimax
endpoint:
type: cloud_api
base_url: "https://api.minimax.io/anthropic"
api_key_env: MINIMAX_API_KEY
runner: claude
default_flags:
model: "MiniMax-M3.1-Flash-Preview"
reasoning_effort: medium
provider_context_ref: "minimax/v1"
provider_secret_ref: "MINIMAX_API_KEY"
session_capable: false

View File

@@ -1,6 +1,6 @@
name: minimax-code-repo
description: "MiniMax code agent — repo clone, agent-repo branch persistence"
model: minimax
model: minimax-m31-flash-max
harness: code-minimax-repo/v1
requires_tags: [claude-code, coding-agent, git-access, project-repo]
required_params: [repo_url, agent_repo_url]

View File

@@ -1,6 +1,6 @@
name: minimax-code-tdd
description: "MiniMax code agent with TDD enforcement — tests are read-only and must pass before the task finishes"
model: minimax
model: minimax-m31-flash-max
harness: code-minimax-tdd-repo/v1
requires_tags: [coding-agent, git-access, project-repo, tdd-gate]
required_params: [repo_url, agent_repo_url]

View File

@@ -1,6 +1,6 @@
name: minimax-planning
description: "MiniMax planning agent — specs, reviews, architecture docs"
model: minimax
model: minimax-m31-flash-max
harness: planning-minimax-repo/v1
requires_tags: [claude-code, planning-agent, git-access, project-repo]
required_params: [repo_url, agent_repo_url]

View File

@@ -1,6 +1,6 @@
name: minimax-security-review
description: "MiniMax security review agent — security audit via Anthropic-compatible proxy"
model: minimax
model: minimax-m31-flash-max
harness: security-review-minimax-repo/v1
requires_tags: [claude-code, security-reviewer, best-practices, git-access, project-repo]
required_params: [repo_url, agent_repo_url]

View File

@@ -1,6 +1,6 @@
name: minimax-tdd-impl
description: "MiniMax TDD implementation agent — tests locked read-only, cannot be modified by agent"
model: minimax
model: minimax-m31-flash-max
harness: minimax-tdd-impl/v1
requires_tags: [claude-code, coding-agent, git-access, project-repo]
required_params: [repo_url, agent_repo_url]

View File

@@ -1,6 +1,6 @@
name: minimax-test-writer
description: "MiniMax test writing agent — comprehensive tests with edge cases"
model: minimax
model: minimax-m31-flash-max
harness: test-writing-minimax-repo/v1
requires_tags: [claude-code, test-writer, best-practices, git-access, project-repo]
required_params: [repo_url, agent_repo_url]

View File

@@ -31,9 +31,10 @@ nodes:
- id: parent-link-check
kind: script
cmd: scripts/workflow/parent-link-check
cp_write: false
on_fail: refuse_dispatch
runtime_env:
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
CP_URL: "https://agents.oreillyit.nz/api"
WORKFLOW_ITEM_UUID: "${item.uuid}"
PARENT_LINK_TAG: auto-resolve-bugs
PARENT_LINK_RELATION: parent
@@ -42,30 +43,39 @@ nodes:
- id: tag-ai-resolve
kind: script
cmd: scripts/workflow/tag-set
cp_write: true
depends_on: [parent-link-check]
on_fail: escalate
runtime_env:
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
CP_URL: "https://agents.oreillyit.nz/api"
WORKFLOW_ITEM_UUID: "${item.uuid}"
TAG_SET_ADD: ai-resolve
# Advance the bug; R2 then dispatches bug-verify-sonnet@1 at verify-pending.
- id: state-tx
kind: script
cmd: scripts/workflow/state-tx
cp_write: true
depends_on: [tag-ai-resolve]
on_fail: escalate
runtime_env:
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
CP_URL: "https://agents.oreillyit.nz/api"
STATE_TX_NEW_STATE: verify-pending
# NOTE (script-node credentials, M43): kind: script nodes are harness-exempt
# (WF-COMP-6) but now carry the AU-51 internal-service bearer token by
# default — see spec/typed-workflows.md WF-COMP-26. The compiler emits
# `secrets_required: [CP_SERVICE_TOKEN]` and
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN` on
# every script node; the dispatcher's `_run_script_task` legacy-key
# branch mounts the dispatcher-process-env CP_SERVICE_TOKEN value as a
# file. All three nodes above read it via
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. Each script
# still resolves its own actor identity (OIDC_IDENTITY_EMAIL or
STATE_TX_FROM_STATES: "pending,open"
# NOTE (script-node credentials): kind: script nodes are harness-exempt
# (WF-COMP-6), but the flag-on script path composes the workflow-script/v1
# composite (shared-variables/v1 + cp-service-token/v1; WF-COMP-31). The
# cp-service-token/v1 layer restages the AU-51 internal-service bearer token
# and exports CP_SERVICE_TOKEN_FILE=/run/agent/cp-service-token/token, which
# all three nodes above read via
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. The legacy
# WF-COMP-26 mount (`secrets_required: [CP_SERVICE_TOKEN]` and
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN`, backed by
# 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
# 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`.

View File

@@ -23,6 +23,8 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
AGENT_BRANCH: "task/${item.uuid}"
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
escalation:

View File

@@ -22,6 +22,8 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
AGENT_BRANCH: "task/${item.uuid}"
labels: [airouter]
prompt: |

View File

@@ -1,101 +0,0 @@
name: bug-verify-sonnet
version: 1
description: Read-only verification of an ai-resolve-tagged bug using Sonnet planning
runtime: planning-sonnet-repo
input:
state: verify-pending
tags_required: [ai-resolve]
tags_forbidden: []
required_artifacts: []
output:
state: 'verified|rejected'
scope_budget:
files_modified_max: 1
lines_diff_max: 400
wall_clock_seconds_max: 1800
nodes:
# NOTE (2026-09-09, work item 88ecd23e): the `verdict-tx` script node that
# used to follow this one has been REMOVED. `kind: script` nodes are
# harness-exempt (WF-COMP-6 / MN-38) — they run in their own empty
# container with no credentialed way to clone the agent's branch or read
# /workspace/verdict, so verdict-tx never actually worked (it always ran
# against its own empty /workspace). Replaced with the "agent
# self-transition" pattern (spec/typed-workflows.md): this node's own
# `cp-service-token/v1` layer (via the `planning-sonnet-repo` composite)
# gives it CP write credentials, so the agent PATCHes its own item's
# flow_state directly as its final step, instead of relying on a second
# node to read a hand-off file. finalize_run's output.state auto-advance
# is intentionally disabled for this template (its `output.state` is the
# pipe-separated "verified|rejected" — finalize.py treats any
# pipe-separated output.state as schema-validation-only, never a literal
# value to PATCH, so it never clobbers the value set below).
- id: agent
kind: agent
template: planning-sonnet-repo
model: sonnet
runtime_env:
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
AGENT_BRANCH: "bug-verify/${item.uuid}"
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
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.
5. Transition this work item's own flow_state to your verdict by
PATCHing the control plane directly — you have a scoped CP
write credential for exactly this (`CP_SERVICE_TOKEN_FILE`,
`CP_URL`). Set `flow_state` to exactly `verified` or exactly
`rejected` (matching the `## Verdict` you wrote above) — no other
value is accepted. Run:
```
python3 - <<'PY'
import json, os, urllib.request, uuid
VERDICT = "verified" # or "rejected" — must match your ## Verdict
ITEM_UUID = "${item.uuid}"
token = open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()
url = os.environ["CP_URL"].rstrip("/") + f"/v1/work-items/{ITEM_UUID}"
body = json.dumps({
"flow_state": VERDICT,
"description": f"bug-verify-sonnet@1: verdict={VERDICT}",
}).encode()
req = urllib.request.Request(url, data=body, method="PATCH", headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
})
with urllib.request.urlopen(req, timeout=30) as resp:
print(resp.status, resp.read().decode())
PY
```
Replace `VERDICT` with your actual verdict before running. This is
the ONLY code you may run outside the investigation itself — do
not use this credential for anything else.
6. Call task_complete only after the PATCH above returns 200.

View File

@@ -0,0 +1,107 @@
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.

View File

@@ -0,0 +1,26 @@
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

View File

@@ -23,6 +23,8 @@ nodes:
template: code-sonnet-tdd-repo
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: "task/${item.uuid}"
prompt: |
Implement the scope described in this work item. The description lists

View File

@@ -18,5 +18,7 @@ nodes:
template: code-fable-tdd-repo
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: "task/${item.uuid}"
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"

View File

@@ -23,6 +23,7 @@ nodes:
- id: policy-pick
kind: script
cmd: scripts/workflow/policy-pick
cp_write: false
on_fail: escalate
- id: agent
kind: agent

View File

@@ -12,12 +12,19 @@ scope_budget:
files_modified_max: 2
lines_diff_max: 200
wall_clock_seconds_max: 600
agent_repo:
url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
nodes:
- id: frozen-fixture-emit
kind: script
cmd: scripts/workflow/frozen-fixture-emit
cp_write: false
on_fail: refuse_dispatch
- id: state-tx
kind: script
cmd: scripts/workflow/state-tx
on_fail: continue
cp_write: true
runtime_env:
CP_URL: "https://agents.oreillyit.nz/api"
STATE_TX_NEW_STATE: test-skeleton-ready
STATE_TX_FROM_STATES: scoped-tagged

View File

@@ -18,5 +18,7 @@ nodes:
template: code-minimax-tdd-repo
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: "task/${item.uuid}"
prompt: "Implement the task. ${metadata.automation.test_files}${metadata.automation.spec_ids}Task description: ${artifacts.item_title}. ${artifacts.item_body}"

View File

@@ -22,5 +22,7 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
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}"

View File

@@ -22,6 +22,8 @@ nodes:
model: claude-opus-5
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: "task/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.
@@ -44,7 +46,7 @@ nodes:
- id: review-minimax
kind: agent
role: plan-review
model: minimax-m2
model: minimax-m31-flash-max
runtime_env:
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
AGENT_BRANCH: "task/${item.uuid}"
@@ -117,6 +119,7 @@ nodes:
- id: judge-gate
kind: script
cmd: scripts/workflow/verdict-gate
cp_write: false
depends_on: [judge-haiku]
loop:
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]

View File

@@ -21,6 +21,8 @@ nodes:
labels: [airouter]
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: "spec/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.

View File

@@ -21,6 +21,8 @@ nodes:
template: best-practices-fable-low-repo
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: "spec/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.

View File

@@ -1,66 +0,0 @@
name: scope-decompose-sonnet
version: 1
description: Scope and decompose a spec into tagged tasks using Sonnet planning
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:
- id: agent
kind: agent
# Decomposition is spec-test-tier work (claude/stage-routing.md): Sonnet,
# not airouter. 2026-07-04 E2E: Qwen3.6 empty-delivered this stage (task
# "succeeded" touching only .gitignore, zero work items created) — the
# template NAME never set the model; the node model: field does.
template: spec-writing-sonnet-repo
# cli defaults to "claude" — the anthropic-cloud OAuth composite drives
# the claude CLI; "cli: agentic" is the OpenAI-compatible runner and
# fails fast with "OPENAI_BASE_URL required" (E2E 2026-07-04).
model: sonnet
runtime_env:
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
AGENT_BRANCH: "spec/${item.uuid}"
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
# repo diff — the empty-deliverable audit would fail every success.
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.
Write a decomposition plan to /workspace/.agent-output/decompose.json with
exactly this structure:
{
"project_id": "${item.project_id}",
"space_id": "${item.space_id}",
"parent_item_uuid": "${item.uuid}",
"tasks": [
{
"title": "...",
"description": "...",
"flow_state": "needs-tests",
"spec_ids": ["WT-EVAL-1", "..."],
"test_files": ["tests/triggers/test_schema.py", "..."],
"tags": ["airouter-eligible"]
}
]
}
Include "airouter-eligible" in tags only for single-file, self-contained
tasks that Qwen3.6 can handle alone. Omit tags for complex multi-file tasks.
After writing decompose.json, run this command to create the work items:
python3 -m lib.cp_cli planner items decompose ${item.uuid} --file /workspace/.agent-output/decompose.json

View File

@@ -0,0 +1,143 @@
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.

View File

@@ -22,6 +22,8 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
AGENT_BRANCH: "task/${item.uuid}"
prompt: "Implement the failing test. Test file: ${artifacts.test_file}. Skeleton: ${artifacts.impl_skeleton}."
escalation:

View File

@@ -22,5 +22,7 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
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}"

View File

@@ -22,6 +22,8 @@ nodes:
labels: [airouter]
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: "spec/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.

View File

@@ -22,6 +22,8 @@ nodes:
template: spec-writing-fable-low-repo
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: "spec/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.

View File

@@ -27,6 +27,8 @@ nodes:
model: claude-opus-5
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: "spec/${item.uuid}"
prompt: |
The agent-runtimes project repo is cloned at /workspace/project.
@@ -52,7 +54,7 @@ nodes:
- id: review-minimax
kind: agent
role: spec-review
model: minimax-m2
model: minimax-m31-flash-max
runtime_env:
AGENT_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git"
AGENT_BRANCH: "spec/${item.uuid}"
@@ -127,6 +129,7 @@ nodes:
- id: judge-gate
kind: script
cmd: scripts/workflow/verdict-gate
cp_write: false
depends_on: [judge-haiku]
loop:
body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate]

View File

@@ -12,20 +12,28 @@ scope_budget:
files_modified_max: 1
lines_diff_max: 50
wall_clock_seconds_max: 600
agent_repo:
url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
nodes:
- id: preflight-test-collection
kind: script
cmd: scripts/workflow/preflight-test-collection
cp_write: false
on_fail: refuse_dispatch
- id: preflight-fixture-lint
kind: script
cmd: scripts/workflow/preflight-fixture-lint
cp_write: false
on_fail: refuse_dispatch
- id: preflight-test-fail-mode
kind: script
cmd: scripts/workflow/preflight-test-fail-mode
on_fail: continue
cp_write: false
- id: state-tx
kind: script
cmd: scripts/workflow/state-tx
on_fail: continue
cp_write: true
runtime_env:
CP_URL: "https://agents.oreillyit.nz/api"
STATE_TX_NEW_STATE: test-validated
STATE_TX_FROM_STATES: test-write-pending

View File

@@ -22,4 +22,6 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
AGENT_BRANCH: "task/${item.uuid}"

View File

@@ -21,11 +21,13 @@ nodes:
role: test-write-review # M36 v0.6 (bug 69370eda fix): role + model replaces
# the ungrounded `template: minimax-test-writer`
# (a composite that never existed in the catalog).
model: minimax-m2
model: minimax-m31-flash-max
runtime_env:
# WF-COMP-15: stage agents share one per-item work branch so their work
# accumulates across iterations.
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}"
- id: review-sonnet
kind: agent
@@ -59,6 +61,7 @@ nodes:
- id: review-gate
kind: script
cmd: scripts/workflow/verdict-gate
cp_write: false
depends_on: [review-sonnet]
loop:
body: [write-minimax, review-sonnet, review-gate]

View File

@@ -22,6 +22,8 @@ nodes:
# see the same tree). Without AGENT_REPO_URL the agent-repo layer never
# 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_UPSTREAM_REPO_URL: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git"
AGENT_UPSTREAM_BRANCH: "main"
AGENT_BRANCH: "task/${item.uuid}"
prompt: |
You are writing pytest tests for the following task.

0
tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,56 @@
"""cp-docs-token/v1 context: env-only step-docs layer (PLN-CLI-16, D-40, AU-59).
The per-step wfcd_ token and CA file are delivered by the dispatcher. The layer
sets the env contract and ships one context doc; no ESO mount, no init script,
and no reference to the cp-service-token layer.
"""
from __future__ import annotations
from pathlib import Path
import yaml
_LAYER = (
Path(__file__).resolve().parents[2] / "harnesses" / "contexts" / "cp-docs-token" / "v1"
)
def _doc() -> dict:
return yaml.safe_load((_LAYER / "harness.yaml").read_text())
def test_cp_docs_token_layer_parses_with_env_keys():
doc = _doc()
assert doc["kind"] == "context"
assert doc["name"] == "cp-docs-token"
assert doc["version"] == 1
assert doc["requires"] == []
assert doc["provides"] == ["cp-docs-token"]
assert doc["env"] == {
"CP_DOCS_TOKEN_FILE": "/run/agent/cp-docs/token",
"CP_DOCS_CA_FILE": "/run/agent/cp-docs/ca.crt",
}
def test_cp_docs_token_layer_declares_no_secrets_and_no_init():
doc = _doc()
assert not doc.get("secrets_required")
assert not doc.get("scripts")
assert not (_LAYER / "init.sh").exists()
def test_cp_docs_token_layer_context_file_mapping():
doc = _doc()
assert doc["context_files"] == [
{
"source": "./CLAUDE.md",
"target": "/opt/harness/context/cp-docs-token/CLAUDE.md",
}
]
assert (_LAYER / "CLAUDE.md").is_file()
def test_cp_docs_token_layer_never_mentions_cp_service_token():
for path in _LAYER.rglob("*"):
if path.is_file():
assert "cp-service-token" not in path.read_text(), path

View File

@@ -0,0 +1,87 @@
"""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

View File

@@ -0,0 +1,37 @@
"""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()

View File

@@ -0,0 +1,32 @@
"""Track R / R3b: the legacy AU-51 bearer composite and layer stay deleted.
``workflow-script/v1`` (the legacy script composite) and the
``cp-service-token/v1`` context were removed once the control plane stopped
falling back to them (agent-runtimes 0.0.5.1890). Nothing may re-introduce
either, and no live composite may layer the bearer context.
"""
from pathlib import Path
import yaml
_ROOT = Path(__file__).resolve().parents[2]
_HARNESSES = _ROOT / "harnesses"
def test_legacy_script_composite_directory_is_gone():
assert not (_HARNESSES / "composites" / "workflow-script").exists()
def test_cp_service_token_context_directory_is_gone():
assert not (_HARNESSES / "contexts" / "cp-service-token").exists()
def test_no_composite_layers_cp_service_token():
offenders = []
for path in sorted((_HARNESSES / "composites").glob("*/v*/harness.yaml")):
doc = yaml.safe_load(path.read_text()) or {}
for layer in doc.get("layers") or []:
if str(layer.get("context", "")).startswith("cp-service-token"):
offenders.append(str(path.relative_to(_ROOT)))
assert offenders == []

View File

@@ -0,0 +1,114 @@
"""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@2.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)

View File

@@ -0,0 +1,94 @@
"""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"