Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions AGENT-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ owns product work. Shipped declarations must not contain a developer's absolute
```kdl
agent "<identity>" {
identity "<identity>"
name "<human-facing name>"
description "<enduring responsibility>"
host "<host>"
role "worker"
type "service"
Expand Down Expand Up @@ -83,6 +85,8 @@ Supported agent children are:
| Node | Meaning |
|---|---|
| `identity "…"` | Overrides the positional/path-derived identity. |
| `name "…"` | Optional, non-unique human-facing name. It never routes or selects work. |
| `description "…"` | Optional enduring responsibility boundary. It never grants authority. |
| `host "…"` | Execution host. The canonical folder path and content should agree. |
| `role "…"` | Optional metadata with no execution behavior. |
| `type "service"` | Optional; `service` is the only accepted value and the default. |
Expand All @@ -103,6 +107,51 @@ Canonical declarations normally omit `type`. Unknown non-render children may be
extension syntax, and required behavior must never depend on them. `schedule` is explicitly reserved and
rejected. Unknown render directives are errors.

## Stable identity and presentation

The positional or child `identity` remains the stable automation ID. It alone
owns bus routing, supervisor edges, task IDs, durable state paths, resources,
authorization, and lifecycle reconciliation. Existing positional and child
identity grammar remains valid; roster JSON continues to expose the stable bus
ID as `identity`.

`name` and `description` are optional presentation metadata. Explicit values
must be non-empty, trimmed, single-line, and free of control characters. Limits
are 160 Unicode scalars for `name` and 1,000 for `description`; omission means
absent. Names need not be unique and are never st2 message, status, resource,
authorization, or lifecycle aliases. The retired sibling `name` file is not a
fallback source.

The roster exposes separate nullable `name` and `description` fields. Mutable
canonical KDL can be edited source-preservingly with:

```console
st2 rename <stable-id> <name>
st2 rename <stable-id> --clear
st2 describe <stable-id> <description>
st2 describe <stable-id> --clear
```

An agent may edit itself; a declared supervisor ancestor may edit a descendant;
an operator with no `ST_AGENT` may edit a selected mutable catalog. Peer edits
fail. JSON, TOML, and declarations marked `meta { managed-by "nix" }` are
readable but refuse these live authoring commands. Concurrent commands serialize
through the catalog lock and preserve both accepted field edits.

Every managed PTY receives the owned tags
`agent.presentation.schema=1`, `agent.actor.path=<host>.<identity>`, and the
optional `agent.presentation.description`. Only the primary `agent` PTY maps
Agent Spec `name` to native `displayName`; secondary PTYs retain their existing
task-specific display behavior. st2 reconciles this envelope through exact PTY
IDs with one atomic metadata patch, preserves unrelated tags, clears removed
owned fields, emits one coherent `metadata_change` event per real transition,
and emits no event for a no-op. Presentation reconciliation never replaces the
PTY process or its durable Agent Spec state.

This contract is proved model-free by
[`agent-presentation-contract`](./cells/agent-presentation-contract/) and
[`agent-presentation-continuity`](./cells/agent-presentation-continuity/).

The restart defaults are 3 attempts per 60 seconds, no delay, and `mode "delay"`. Durations accept bare
seconds or `ms`, `s|sec|secs`, `m|min|mins`, `h|hr|hrs`, and `d|day|days`. `mode "delay"` keeps retrying with
the window reset; `mode "fail"` parks the task after attempts are exhausted and sends one best-effort
Expand Down
8 changes: 6 additions & 2 deletions CATALOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- Generated by bin/generate-catalog.sh from cell KDL and evidence/*.tsv. Do not edit. -->
# Current eval catalog and evidence

This is the sole current corpus surface: **58 included cells** (18 model-free,
This is the sole current corpus surface: **60 included cells** (20 model-free,
40 model-backed) and **4 retired exclusions**. Inventory fields are derived from the active
KDL and executable judge scripts. Latest accepted PASS evidence remains distinct from the append-only run
history: a failed last run is visible without being advertised as accepted evidence, and cells with no
Expand All @@ -17,6 +17,8 @@ harness-native loader plus canonical hook file.
| Cell | Harness | Model(s) / effort | Model seats | Cost | Timeout | Held-out judges | Latest accepted PASS | Last recorded run |
|---|---|---|---:|---|---|---:|---|---|
| `adopt-only-migration` | model-free | — | 0 | none | `90s` | 6 | — | **NO STRUCTURED RUN** |
| `agent-presentation-continuity` | model-free | — | 0 | none | `90s` | 5 | — | **NO STRUCTURED RUN** |
| `agent-presentation-contract` | model-free | — | 0 | none | `90s` | 8 | — | **NO STRUCTURED RUN** |
| `agent-spec-resource-bindings` | model-free | — | 0 | none | `60s` | 4 | — | **NO STRUCTURED RUN** |
| `assignment-contract-cold-assignment` | Codex | `gpt-5.6-sol` / medium | 2 | medium | `1200s` | 3 | — | **NO STRUCTURED RUN** |
| `assignment-contract-cold-focus` | Codex | `gpt-5.6-sol` / medium | 2 | medium | `1200s` | 3 | — | **NO STRUCTURED RUN** |
Expand Down Expand Up @@ -95,6 +97,8 @@ while the last-run column makes a recorded failure distinct from a cell with no
## Harness hook exclusions

- `adopt-only-migration` / `cell` — deterministic native adoption/replacement lifecycle probe; no harness seat
- `agent-presentation-continuity` / `cell` — deterministic native presentation continuity probe; no harness seat
- `agent-presentation-contract` / `cell` — deterministic native Agent Spec and PTY presentation probe; no harness seat
- `agent-spec-resource-bindings` / `cell` — deterministic native Agent Spec Resource-envelope probe; no harness seat
- `context-resource-continuity` / `cell` — deterministic native context/resource restart probe; no harness seat
- `docs` / `judge:cold-reader` — one-shot offline Claude print grader; no bus identity, DING, or hook surface
Expand All @@ -114,7 +118,7 @@ while the last-run column makes a recorded failure distinct from a cell with no
- `targeted-reconcile-isolation` / `cell` — deterministic selected reconcile isolation probe; no bus-connected agent
- `two-networks-coexist` / `cell` — deterministic catalog-isolation probe; no harness seat

These **19** rows are generated from structured exclusions. A cell-level row is required for
These **21** rows are generated from structured exclusions. A cell-level row is required for
every derived model-free cell. The one-shot docs cold reader is a model-backed grader, but not a bus-connected
agent: it has no st2 identity, DING sidecar, or lifecycle hooks.

Expand Down
20 changes: 20 additions & 0 deletions cells/agent-presentation-continuity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# agent-presentation-continuity

Model-free acceptance coverage for adding, changing, and clearing Agent Spec
presentation metadata without changing stable identity or the live process
generation.

The cell begins with the existing positional stable ID and a conflicting
legacy sibling `name` file. It proves that Agent Spec is authoritative: absent
presentation does not fall back to the sibling file, and declared `name` and
`description` appear in the roster after an in-place catalog
edit. Across add, repeat, change, and clear reconciliations it requires the
same PTY ID, PID, creation timestamp, and single `session_start` event.

The same transitions must preserve unread and archived messages, context,
decisions, resources, presence, and terminal transcript. Cleanup uses the
existing explicit retirement lifecycle and leaves no PTY state.

This cell does not retire the existing `identity` grammar. Schema versioning
and breaking grammar changes belong to compoundingtech/st2#127; this cell is
the additive evidence for compoundingtech/st2#128.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Additive Agent Spec presentation changes preserve stable identity, runtime generation, and durable state.
eval {
copy "./fixture"
max-timeout "90s"

run "continuity" {
command "bash ./continuity.sh"
}

judges {
judge "AUTHORITY - Agent Spec presentation ignores the retired sibling name file" {
exec "grep -Fqx PRESENTATION-AUTHORITY-GREEN-a128 $RUNS_DIR/continuity.out"
}
judge "ROSTER - add, change, repeat, and clear project exact presentation fields" {
exec "grep -Fqx PRESENTATION-ROSTER-GREEN-a128 $RUNS_DIR/continuity.out"
}
judge "GENERATION - every presentation-only reconcile preserves PTY id, pid, createdAt, and one session start" {
exec "grep -Fqx PRESENTATION-GENERATION-GREEN-a128 $RUNS_DIR/continuity.out"
}
judge "DURABILITY - inbox, archive, context, decisions, resources, presence, and transcript survive" {
exec "grep -Fqx PRESENTATION-DURABILITY-GREEN-a128 $RUNS_DIR/continuity.out"
}
judge "CLEANUP - explicit retirement leaves no retained PTY state" {
exec "grep -Fqx PRESENTATION-CLEANUP-GREEN-a128 $RUNS_DIR/continuity.out"
}
}
}
160 changes: 160 additions & 0 deletions cells/agent-presentation-continuity/fixture/continuity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
set -euo pipefail

root="${CATALOG:?CATALOG must be set}"
net="$root/net"
spec="$net/agents/pc/worker/agent.kdl"
original="$root/worker.original.kdl"
pty_id="pc.worker"
export PTY_ROOT="$net/pty"
export XDG_STATE_HOME="$root/state"

cp "$spec" "$original"

pty_at() {
env -u PTY_SESSION PTY_ROOT="$PTY_ROOT" pty "$@"
}

session() {
pty_at list --json | jq -cer --arg id "$pty_id" '.[] | select(.name == $id)'
}

generation() {
session | jq -c '{name,pid,createdAt}'
}

start_count() {
jq -s '[.[] | select(.type == "session_start")] | length' "$PTY_ROOT/$pty_id.events.jsonl"
}

wait_running() {
for _ in $(seq 1 100); do
test "$(session | jq -r '.status')" = running && return 0
sleep 0.05
done
echo "presentation fixture did not become ready" >&2
return 1
}

retire() {
grep -Fq 'retired #true' "$spec" || sed -i '/role "worker"/a\\ retired #true' "$spec"
}

cleanup() {
if test -f "$spec"; then
retire 2>/dev/null || true
st2 up --once --catalog "$net" --host pc >/dev/null 2>&1 || true
fi
pty_at kill "$pty_id" >/dev/null 2>&1 || true
pty_at rm "$pty_id" >/dev/null 2>&1 || true
cp "$original" "$spec" 2>/dev/null || true
}
trap cleanup EXIT

st2 validate --catalog "$net" --host pc --strict >/dev/null
st2 agents --catalog "$net" --host pc --json >"$root/roster-absent.json"
jq -e '
length == 2 and
(.[] | select(.identity == "pc.worker") | .name == null and .description == null)
' "$root/roster-absent.json" >/dev/null
echo "PRESENTATION-AUTHORITY-GREEN-a128"

st2 up --once --catalog "$net" --host pc >"$root/launch.out"
grep -Fq 'launched (1): pc.worker' "$root/launch.out"
wait_running
before="$(generation)"
test "$(start_count)" -eq 1

st2 status "$pty_id" --set busy --catalog "$net" --host pc --as "$pty_id" >/dev/null
printf '%s\n' CONTEXT-NOW-a128 | st2 context write "$pty_id" --catalog "$net" --as "$pty_id"
st2 context append "$pty_id" --catalog "$net" --as "$pty_id" \
--decision DECISION-a128 --why DECISION-WHY-a128
st2 resource add https://example.invalid/presentation-a128 \
--catalog "$net" --as "$pty_id" --title PRESENTATION-RESOURCE-a128 \
--tag presentation,continuity --relation output >"$root/resource-ref"

st2 message send "$pty_id" --catalog "$net" --host pc --as pc.sender \
--subject ARCHIVED-a128 >/dev/null <<'MSG'
ARCHIVED-BODY-a128
MSG
archived_path=("$net/agents/pc/worker/resources/inbox/"*.md)
test "${#archived_path[@]}" -eq 1
archived_name="$(basename "${archived_path[0]}")"
st2 message archive "$pty_id" "$archived_name" --catalog "$net" --host pc \
--as "$pty_id" >/dev/null
st2 message send "$pty_id" --catalog "$net" --host pc --as pc.sender \
--subject UNREAD-a128 >/dev/null <<'MSG'
UNREAD-BODY-a128
MSG

for _ in $(seq 1 100); do
pty_at peek --plain "$pty_id" 2>/dev/null | grep -Fq PRESENTATION-TRANSCRIPT-a128 && break
sleep 0.05
done
pty_at peek --plain "$pty_id" | grep -Fq PRESENTATION-TRANSCRIPT-a128

add_fields() {
sed -i '/role "worker"/a\\ description "Owns durable identity acceptance"\n name "Evidence Worker"' "$spec"
}

assert_generation() {
test "$(generation)" = "$before"
test "$(start_count)" -eq 1
}

add_fields
st2 validate --catalog "$net" --host pc --strict >/dev/null
st2 up --once --catalog "$net" --host pc >"$root/add.out"
grep -Fq 'adopted (1): worker' "$root/add.out"
test -z "$(sed -n '/launched (/p;/torn down (/p' "$root/add.out")"
assert_generation
st2 agents --catalog "$net" --host pc --json >"$root/roster-added.json"
jq -e '
.[] | select(.identity == "pc.worker") |
.name == "Evidence Worker" and
.description == "Owns durable identity acceptance"
' "$root/roster-added.json" >/dev/null

st2 up --once --catalog "$net" --host pc >"$root/repeat.out"
grep -Fq 'adopted (1): worker' "$root/repeat.out"
assert_generation

sed -i 's/name "Evidence Worker"/name "Shared Presentation"/' "$spec"
sed -i 's/description "Owns durable identity acceptance"/description "Owns changed identity acceptance"/' "$spec"
st2 up --once --catalog "$net" --host pc >"$root/change.out"
assert_generation
st2 agents --catalog "$net" --host pc --json >"$root/roster-changed.json"
jq -e '
.[] | select(.identity == "pc.worker") |
.name == "Shared Presentation" and
.description == "Owns changed identity acceptance"
' "$root/roster-changed.json" >/dev/null

sed -i '/^ name "/d;/^ description "/d' "$spec"
st2 up --once --catalog "$net" --host pc >"$root/clear.out"
assert_generation
st2 agents --catalog "$net" --host pc --json >"$root/roster-cleared.json"
jq -e '
.[] | select(.identity == "pc.worker") |
.name == null and .description == null
' "$root/roster-cleared.json" >/dev/null
echo "PRESENTATION-ROSTER-GREEN-a128"
echo "PRESENTATION-GENERATION-GREEN-a128"

test "$(st2 status "$pty_id" --catalog "$net" --host pc --as "$pty_id")" = busy
st2 context read "$pty_id" --catalog "$net" --as "$pty_id" | grep -Fq CONTEXT-NOW-a128
st2 context read "$pty_id" --catalog "$net" --as "$pty_id" --decisions | grep -Fq DECISION-WHY-a128
st2 resource ls --catalog "$net" --as "$pty_id" | grep -Fq https://example.invalid/presentation-a128
test -f "$net/agents/pc/worker/resources/archive/$archived_name"
grep -Fq ARCHIVED-BODY-a128 "$net/agents/pc/worker/resources/archive/$archived_name"
grep -Fq UNREAD-BODY-a128 "$net/agents/pc/worker/resources/inbox/"*.md
pty_at peek --plain "$pty_id" | grep -Fq PRESENTATION-TRANSCRIPT-a128
echo "PRESENTATION-DURABILITY-GREEN-a128"

retire
st2 up --once --catalog "$net" --host pc >/dev/null
pty_at rm "$pty_id" >/dev/null 2>&1 || true
cp "$original" "$spec"
trap - EXIT
test "$(pty_at list --json | jq 'length')" -eq 0
echo "PRESENTATION-CLEANUP-GREEN-a128"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
agent "sender" {
identity "sender"
host "pc"
role "fixture"
retired #true
workspace "$CATALOG/workspace"
command "true"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
agent "worker" {
identity "worker"
host "pc"
role "worker"
workspace "$CATALOG/workspace"
env {
ST_AGENT "pc.worker"
}
command "exec bash \"$CATALOG/../task.sh\""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RETIRED-SIBLING-NAME-MUST-NOT-SURFACE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fixture
5 changes: 5 additions & 0 deletions cells/agent-presentation-continuity/fixture/task.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail

printf 'PRESENTATION-TRANSCRIPT-a128\n'
exec sleep 300
29 changes: 29 additions & 0 deletions cells/agent-presentation-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# agent-presentation-contract

Model-free acceptance coverage for stable Agent Spec identity and mutable
presentation metadata across the native st2 and PTY boundary.

Two live agents deliberately share one display name. Stable IDs remain the
only st2 routing, authorization, task, and lifecycle keys, while PTY human
lookup follows its exact-ID-first and fail-closed ambiguity contract. Changing
or clearing `name` and `description` updates the roster and managed
PTY metadata without changing the PTY ID, PID, creation timestamp, or process
generation. Repeating an unchanged reconciliation emits no metadata event.
Name and description accept at most 160 and 1,000 Unicode scalars respectively;
empty, untrimmed, multiline, and control text is refused.

Every managed PTY receives the same owned presentation schema, actor path,
and description tags. Only the primary agent PTY receives Agent Spec
`name` as its native display name; a secondary task keeps its task-specific
display behavior across presentation changes.

The same black-box fixture exercises source-preserving presentation authoring:
operator, self, and supervisor edits succeed; peer edits, Nix-owned sources,
and JSON/TOML declarations fail with classified receipts. Concurrent edits to
different presentation fields serialize through the catalog lock and preserve
both accepted values.

An explicit exact-ID primary-process kill is the lifecycle control: normal
reconciliation must report the primary as restarted and create a new generation
without disturbing its secondary task or sibling agent. Explicit retirement
remains the cleanup boundary.
Loading