Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
ca67caf
FEAT-013: managed session creation and lifecycle (spec + plan)
May 24, 2026
817fb48
FEAT-013: normalize SESSION_NAME_CONFLICT + pending-marker terminology
May 24, 2026
a0ab4a0
FEAT-013: tasks.md + post-tasks readiness audit + remediation
May 24, 2026
e7f2c89
FEAT-013: pre-implement walk integration + readiness audit
May 25, 2026
20c2525
FEAT-013 Phase 1 (T001-T004): skeleton + migration v9 + examples + fi…
May 25, 2026
bad699a
FEAT-013: align data-model.md + tasks.md with Phase 1 implementation
May 25, 2026
ab72150
FEAT-013 Phase 2 (T005-T015): foundational building blocks
May 25, 2026
39dbb5f
FEAT-013: post-Phase-2 alignment (N17 asyncio→threading, N18 PyYAML d…
May 25, 2026
821fff6
FEAT-013 Phase 3a (T016-T021): US1 contract + integration tests
May 25, 2026
83285b8
FEAT-013 Phase 3b (T022): service.create_layout — synchronous orchest…
May 25, 2026
e3af4d0
FEAT-013: post-Phase-3b alignment (N20-N23)
May 25, 2026
26b6b1e
FEAT-013: close all 21 release-gate checklists (534 → 0 open)
May 25, 2026
1b85389
FEAT-013 Phase 3c (T023/T024/T025): handlers + dispatcher registration
May 25, 2026
60b4048
FEAT-013: post-Phase-3c alignment (N25 + N26 + N27)
May 25, 2026
d1120e6
FEAT-013: post-Phase-3c alignment (N28-N31 + historical-note refresh)
May 25, 2026
95e4b72
FEAT-013: post-Phase-3c alignment (N32 — final historical-note refresh)
May 25, 2026
84432d9
FEAT-013 Phase 4a (T031/T032/T033): M2-M5 handlers + sync event emission
May 25, 2026
4d51add
FEAT-013: post-Phase-4a alignment (N33 — state_priority ordering in M…
May 25, 2026
3271d12
FEAT-013 Phase 4b (T029/T030 + T026/T027): background spawn task + lo…
May 25, 2026
a364cf7
FEAT-013: post-Phase-4b alignment (N34 + N35)
May 25, 2026
afcffa8
FEAT-013 Phase 4c (T028 + T034): US2 integration + FEAT-004 scan filter
May 25, 2026
4a234cc
FEAT-013: post-Phase-4c alignment (N36 + N37 — panes/scan.py → discov…
May 25, 2026
780dbd7
specify: harden worktree path resolution in get-last-worktree.sh + se…
May 25, 2026
421aa3d
FEAT-013 Phase 5a (T042/T043/T044/T045 + T035/T036/T037/T040): US3 li…
May 25, 2026
7c52871
FEAT-013: post-Phase-5a alignment (N38 + N39 — M6/M7 contract conform…
May 25, 2026
191b867
FEAT-013 Phase 5b (T046/T047/T049 + T038/T039): recovery reconcile + …
May 25, 2026
ce2bfef
FEAT-013 Phase 5c (T048 + T041): M6/M7/M8 dispatcher + US3 integratio…
May 25, 2026
309ab25
FEAT-013 Phase 6 (T050-T056): polish — sweep + edges + docs + perf SLAs
May 25, 2026
3649320
FEAT-013 hardening: concurrency fixes (C1, C2, H2, M1, M2)
May 25, 2026
7dee0e9
FEAT-013 hardening: peer detection, validation, polish (H1, H4-H7, M3…
May 25, 2026
2dbf2ae
FEAT-013 wiring: daemon boot + FR-013 retry (C3, C4, C6 — partial C5)
May 25, 2026
dc82dc2
FEAT-013 T057: production tmux spawn backend + adapter verbs
Jun 1, 2026
0229408
FEAT-013: record T058/T059 (recovery + remove/recreate prod wiring gaps)
Jun 1, 2026
ed3c0ab
FEAT-013 T057b (part 1): real US1 integration tests + register TmuxEr…
Jun 1, 2026
eb365a3
FEAT-010: integration coverage for route execution + restart dedupe
Jun 1, 2026
46bb9d6
FEAT-013 T057b (part 2): §R8 launch-exit detection
Jun 1, 2026
0a8da1e
FEAT-013 T057b (part 3): synchronous session-name conflict pre-check
Jun 1, 2026
15c347d
FEAT-013 T058: wire production recovery list-panes channel
Jun 1, 2026
1ab6e40
FEAT-013 T059: wire production remove/recreate tmux backends
Jun 1, 2026
18d425d
FEAT-013 review fixes (Batch A): peer-detection spoof + host_only lea…
Jun 1, 2026
93a6983
FEAT-013 review fixes (Batch B): spawn timeout + atomic capacity + sp…
Jun 1, 2026
924f819
FEAT-013 review fixes (Batch C): recreate_pane robustness
Jun 1, 2026
1af3a6c
FEAT-013 review fixes (Batch D): list pagination + per-container index
Jun 1, 2026
e92a2c4
FEAT-013 review fixes (Batch E): kill_pane idempotency + adapter import
Jun 1, 2026
f920270
FEAT-013 review fixes (Batch F): recovery/sweep/shutdown/template
Jun 1, 2026
82dcbf3
Merge remote-tracking branch 'origin/main' into 013-managed-session-l…
Jun 1, 2026
8d3e009
FEAT-013: update managed dispatch tests for FEAT-014 app-contract v1.1
Jun 1, 2026
9010e9c
FEAT-013 docs: align spec/contracts with hardened implementation
Jun 1, 2026
6e3e059
FEAT-013 docs: resolve /speckit.analyze cross-artifact findings
Jun 1, 2026
a8d416a
FEAT-013 docs: fix dangling research section ref (analyze X1)
Jun 1, 2026
69efd4e
FEAT-013: bump CLI MAX_SUPPORTED_SCHEMA_VERSION 8->9 + update schema/…
Jun 1, 2026
314664b
FEAT-013 docs: complete T002 with the schema-version dual-bump (analy…
Jun 1, 2026
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
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/014-app-dashboard-extensions"
"feature_directory": "specs/013-managed-session-lifecycle"
}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
`specs/014-app-dashboard-extensions/plan.md`.
`specs/013-managed-session-lifecycle/plan.md`.
<!-- SPECKIT END -->

# AgentTower Agent Context
Expand Down
20 changes: 20 additions & 0 deletions docs/app-contract-client-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,23 @@ for a working reference client.
- Check `capability_flags` before calling an optional method introduced
in a later minor (none exist at v1.0 — `capability_flags == {}`).
- Surface unknown closed-set codes gracefully; never hard-fail on them.

## 8. FEAT-013 managed-session methods

FEAT-013 adds 8 new methods to the `app.*` namespace — `app.managed_*`
— for operator-driven creation of multi-agent tmux layouts inside bench
containers. They are **required** surfaces at `app_contract_version =
"1.0"` (not advertised in `capability_flags`; reached through the
additive-evolution rule).

See **[`docs/managed-sessions.md`](managed-sessions.md)** for the full
operator reference: templates, launch profiles, lifecycle states, the
M1–M8 method table, closed-set error codes, and the YAML override
directories.

Quick method list:

- `app.managed_layout_create` / `app.managed_layout_list` / `app.managed_layout_detail` — layout creation + read
- `app.managed_pane_list` / `app.managed_pane_detail` — pane read
- `app.managed_pane_remove` / `app.managed_pane_recreate` — destructive lifecycle
- `app.managed_pane_promote_from_adopted` — reserved stub (returns `not_implemented`)
59 changes: 59 additions & 0 deletions docs/managed-sessions-quickstart-walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Managed Sessions Quickstart Walkthrough (T052)

This document records how to run the [`specs/013-managed-session-lifecycle/quickstart.md`](../specs/013-managed-session-lifecycle/quickstart.md) walkthrough end-to-end against a real `agenttowerd` and a real bench container, plus the in-process verification harness that stands in for it during CI.

T052's intent: prove the quickstart matches observed behavior, and capture any drift between the spec/contracts and what the daemon actually does.

---

## In-process verification (CI)

The full quickstart sequence is exercised in-process by these tests, which use canned spawn-pipeline backends instead of a real tmux/docker channel:

| Test file | Quickstart section covered |
|---|---|
| `tests/integration/test_story1_create_standard_layout.py` | §US1 (pending Phase 4c production tmux backend; module-level skip) |
| `tests/integration/test_story2_auto_prepare_operations.py` | §US2 — every step from "Verify in agent surfaces" through FR-015 FIFO + FR-021 redaction shape |
| `tests/integration/test_story3_lifecycle_operations.py` | §US3 — remove + recreate (with chain-traversal via M5) + adopted-pane protection |
| `tests/integration/test_managed_edge_cases.py` | §Edge cases table (bullets 1, 5, 7, 9, 11 explicitly; others covered by contract tests) |
| `tests/contract/test_managed_dispatch.py` | Dispatcher reachability + per-method envelope shape (M1-M8) |
| `tests/contract/test_managed_perf_sla.py` | SC-001 + SC-008 + SC-009 wall-clock SLAs (in-process bounds) |

Together these cover every observable behavior the quickstart asserts. Drift between quickstart prose and tests should produce a test-failure first; if you see drift only at quickstart-run time, **fix the code** (the quickstart is the spec-side truth, not the snapshot).

---

## Production walkthrough (manual)

For a real end-to-end demo against a running `agenttowerd` plus a real bench container, follow the quickstart in order:

1. Verify preconditions (§Preconditions): `agenttowerd` running, socket reachable, a bench container available, two operator YAML files in `~/.config/opensoft/agenttower/launch_commands/`.
2. Run §US1 step-by-step. Confirm `state == "ready"` within SC-001's 2-minute budget.
3. Run §US2 §"Verify in agent surfaces" — confirm `app.agent.list` returns the 3 managed agents with `origin == "managed"`.
4. Run §US3 §"Remove and recreate a managed pane" — confirm tmux kill happens, recreate produces a `predecessor_id`-linked row, adopted pane attempt returns `managed_pane_protected_adopted`.
5. Run §US3 §"Daemon restart (SC-008)" — stop the daemon, confirm tmux panes alive, start the daemon, hit `app.managed_layout_detail` within ~5s, confirm `state == "ready"`.
6. Run §Edge cases — at minimum exercise the `managed_session_name_conflict` and `managed_layout_capacity_exceeded` paths.

Production end-to-end requires:

- The tmux spawn backend composition (`tmux_create.py` + `pending_marker.py` + FEAT-004 docker-exec channel) — documented as a follow-up in `src/agenttower/managed_sessions/spawn_backends.py`.
- The daemon-boot wiring of `spawn_layout_in_background` (handler kick-off after `create_layout` returns) — same follow-up.
- The daemon-boot wiring of `recovery.reconcile()` (run before the socket accepts requests per SC-008) — documented in `src/agenttower/managed_sessions/recovery.py`'s module docstring.
- The daemon-boot wiring of `pending_marker.sweep()` (60-second periodic) — documented in `src/agenttower/managed_sessions/pending_marker.py`.

All four wiring follow-ups share the same DaemonContext field additions; they're tracked together as the "daemon-boot wiring follow-up" outside of FEAT-013's natural per-task scope.

---

## Drift report (last run)

| Date | Run by | Result | Notes |
|---|---|---|---|
| _(none yet — quickstart is exercised in-process via the test suites listed above; manual production walkthrough is gated on the daemon-boot wiring follow-up)_ | | | |

When the production walkthrough is run (after the daemon-boot wiring follow-up lands), add a row above with the date, runner, pass/fail, and any drift between the quickstart prose and observed behavior. Then either:

- The quickstart is canonical → file a code fix for the divergence.
- The behavior is canonical → file a spec amendment + re-run.

Per T052: drift is a signal to fix code, not the spec.
253 changes: 253 additions & 0 deletions docs/managed-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Managed Session Creation and Lifecycle (FEAT-013)

Operator-facing reference for AgentTower's **managed-session** surface:
how to create a multi-agent tmux layout inside a bench container, how
the lifecycle states behave, where the operator YAML configuration
lives, and which CLI / app-contract methods are available.

This is a companion to:

- [`specs/013-managed-session-lifecycle/spec.md`](../specs/013-managed-session-lifecycle/spec.md) — feature requirements.
- [`specs/013-managed-session-lifecycle/quickstart.md`](../specs/013-managed-session-lifecycle/quickstart.md) — synthetic-client walkthrough (US1/US2/US3 end-to-end).
- [`specs/013-managed-session-lifecycle/contracts/managed-methods.md`](../specs/013-managed-session-lifecycle/contracts/managed-methods.md) — wire-shape contracts for M1–M8.
- [`docs/app-contract-client-guide.md`](app-contract-client-guide.md) — the client-facing index for all `app.*` methods (including the new `app.managed_*` set added by this feature).

---

## Overview

FEAT-013 adds operator-driven creation of standard multi-agent tmux
layouts. Instead of adopting existing panes one-by-one through
`app.agent.register_from_pane`, the operator picks a **template** (e.g.
"1 master + 2 slaves") and AgentTower:

1. Creates the tmux panes via `tmux new-session` / `split-window` (no
`send-keys` for the first-line command — Principle III safety).
2. Registers each created pane as a FEAT-006 agent so the existing
route / queue / event / log surfaces work uniformly across managed
and adopted agents.
3. Tracks each pane through a 5-state lifecycle (`creating` → `ready` /
`degraded` / `failed` → `removed`) with audit-grade events on every
transition.
4. Survives daemon restarts: managed layouts are recovered from durable
SQLite storage and reattached to surviving tmux panes within 5
seconds of the socket opening (SC-008 + SC-009).

---

## Templates

Two built-in templates ship in code; operator-overridable YAML files
extend the set without re-compiling the daemon.

### Built-ins

| Name | Panes | Roles |
|---|---|---|
| `1m+2s` | 3 | 1 master + 2 slaves |
| `2m+2s` | 4 | 2 masters + 2 slaves |

### Override directory

```text
~/.config/opensoft/agenttower/managed_templates/*.yaml
```

The daemon does NOT auto-create this directory; the operator creates
it when adding the first override. Sample template YAMLs live in the
repo under `examples/managed_templates/` for discovery (NOT installed
by the daemon — per FR-024's no-auto-create rule).

### YAML schema

```yaml
name: my-custom # unique; operator file with same name wins
# over a built-in default
panes:
- role: master
capability: orchestrator
label_pattern: "m{ordinal}" # {ordinal} → 1, 2, ...
default_launch_command_ref: claude-master # see Launch profiles
- role: slave
capability: worker
label_pattern: "s{ordinal}"
default_launch_command_ref: claude-worker
```

---

## Launch command profiles

Argv-shape command definitions used to start each agent. The argv form
is mandatory — single-string shell-parsed commands are rejected (the
shell-interpolation hazard is the reason FEAT-013 exists).

### Override directory

```text
~/.config/opensoft/agenttower/launch_commands/*.yaml
```

Sample profile YAMLs live under `examples/launch_commands/` for
discovery.

### YAML schema

```yaml
name: claude-master
command: ["claude", "--model", "opus", "--system-prompt-file", "master.md"]
env:
ANTHROPIC_LOG: warn
working_dir: /workspace
```

- `command` — argv (list of strings); the tmux `new-session -d -s ... --
<cmd...>` invocation passes these AS-IS, no shell parsing.
- `env` — optional; merged into the pane's environment via tmux's
`-e KEY=VALUE` flag.
- `working_dir` — optional; the ONLY field where any shell escaping
happens (via `shlex.quote`), because tmux's `-c` working-directory
flag goes through the shell.

Operator-supplied env-var **values** matching the closed substring set
`*TOKEN*` / `*SECRET*` / `*KEY*` / `*PASSWORD*` (case-insensitive) are
redacted in lifecycle event payloads (FR-021). Argv and `working_dir`
are NOT redacted (operator-visible failure diagnostics rely on them).

---

## Lifecycle states

Both `managed_pane` and `managed_layout` rows track one of five states:

| State | Meaning |
|---|---|
| `creating` | Pane is being spawned, agent is being registered, logs are being attached. Pending-managed marker is set on the tmux pane title so the FEAT-004 scan skips it. |
| `ready` | Pane exists in tmux, agent is registered with FEAT-006, log attach attempted (success or recoverable failure). Marker cleared. |
| `degraded` | Pane exists but is partly unhealthy: launch command exited within 1s, log attach failed, or agent went unhealthy after `ready`. Recovery is via **recreate**. |
| `failed` | Pane is unusable until recreated. `failed_stage` is populated. Audit retained indefinitely; a fresh recreated row may take the same label. |
| `removed` | Operator-initiated removal; tmux pane was killed, routes/log attachments cleaned. Terminal. Audit retained indefinitely. |

`failed_stage` is one of six closed-set values when set:
`pane_create` / `launch_command` / `registration` / `log_attach` /
`tmux_kill` / `recovery_reattach`. The full state graph (transitions,
disallowed transitions, recovery rules) lives in
[`contracts/state-machine.md`](../specs/013-managed-session-lifecycle/contracts/state-machine.md).

---

## Method list

Eight methods total, available in **both** namespaces. The legacy
`managed.*` namespace is reachable from host CLI and bench-container
thin clients (with peer scoping); the `app.managed_*` namespace is
host-only via the FEAT-011 gate.

| Method (legacy) | Method (app) | What it does |
|---|---|---|
| `managed.layout.create` | `app.managed_layout_create` | Create a managed layout from a template. Returns immediately after row insertion; tmux spawn runs in a background task. (M1) |
| `managed.layout.list` | `app.managed_layout_list` | Paginated list of managed layouts. Ordered by `(state_priority ASC, created_at DESC)` — operational-state first. (M2) |
| `managed.layout.detail` | `app.managed_layout_detail` | Full layout view including all panes (optionally terminal). Surfaces `failed_stage` at both layout and per-pane levels. (M3) |
| `managed.pane.list` | `app.managed_pane_list` | Paginated list of managed panes. (M4) |
| `managed.pane.detail` | `app.managed_pane_detail` | Single-pane detail with optional `predecessor_chain` recursion. (M5) |
| `managed.pane.remove` | `app.managed_pane_remove` | Kill underlying tmux pane + clean up routes/logs + transition to `removed`. Preserves audit history. (M6) |
| `managed.pane.recreate` | `app.managed_pane_recreate` | Produce a new pane row linked via `predecessor_id` + `chain_depth+1`. Predecessor must be in `removed` or `failed`. (M7) |
| `managed.pane.promote_from_adopted` | `app.managed_pane_promote_from_adopted` | **STUB** — always returns `not_implemented` with `reserved_since="FEAT-013"`. Reserved for a later feature. (M8) |

Full request / response shapes for every method are in
[`contracts/managed-methods.md`](../specs/013-managed-session-lifecycle/contracts/managed-methods.md).

---

## Example: create a layout

```json
{
"method": "app.managed_layout_create",
"container_id": "bench-alpha",
"template_name": "1m+2s",
"tmux_session_name": "session-quickstart",
"launch_command_overrides": {
"master:m1": "claude-master",
"slave:s1": "claude-worker",
"slave:s2": "claude-worker"
},
"idempotency_key": "operator-clicked-create-12345"
}
```

Response (immediate, before tmux spawn completes):

```json
{
"ok": true,
"app_contract_version": "1.0",
"result": {
"layout_id": "01HZ...",
"state": "creating",
"intended_pane_count": 3,
"panes": [
{"pane_id": "01HZ-p1", "role": "master", "label": "m1", "state": "creating"},
{"pane_id": "01HZ-p2", "role": "slave", "label": "s1", "state": "creating"},
{"pane_id": "01HZ-p3", "role": "slave", "label": "s2", "state": "creating"}
],
"replay": false
}
}
```

Poll `app.managed_layout_detail` until `state == "ready"` (or subscribe
to lifecycle events via `app.event.list`).

---

## Closed-set error codes (FEAT-013 additions)

13 new error codes added on top of FEAT-011's 27-entry registry (40
total). Full details in
[`contracts/error-codes.md`](../specs/013-managed-session-lifecycle/contracts/error-codes.md).

| Code | Method(s) | When |
|---|---|---|
| `managed_template_not_found` | M1 | `template_name` doesn't resolve via built-ins or operator overrides. |
| `managed_launch_command_not_found` | M1 / M7 | `launch_command_overrides` references an unknown profile. |
| `managed_session_name_conflict` | M1 | `tmux_session_name` already exists in the target container. No silent suffixing. |
| `managed_pane_label_conflict` | M1 | Two non-terminal panes collide on `(container_id, label)`. |
| `managed_layout_capacity_exceeded` | M1 | Daemon at 40-layout cap (FR-025). |
| `managed_layout_not_found` | M3 | Unknown `layout_id`. |
| `managed_pane_not_found` | M4 / M5 / M6 / M7 | Unknown `pane_id` (or `predecessor_pane_id`). |
| `managed_pane_protected_adopted` | M6 / M7 | Target pane exists in `agents` (adopted) but NOT in `managed_pane` (FR-012). |
| `managed_pane_illegal_transition` | M6 | E.g., trying to remove a pane in `creating` state. |
| `managed_pane_illegal_recreate_source` | M7 | Predecessor is `ready` / `degraded` / `creating` (must be `removed` / `failed`). |
| `managed_pane_recreate_chain_too_deep` | M7 | `predecessor.chain_depth >= 15` (limit is 16; FR-023). |
| `managed_pane_concurrent_recreate` | M7 | Another recreate of the same predecessor is in flight (FR-027). |
| `container_not_found` | M1 / M6 / M7 | `container_id` is unknown to the FEAT-003 registry. |

---

## Scope notes (MVP)

**Out of scope** (FR-018): non-tmux backends, semantic task planning,
cross-host orchestration, adopted-to-managed pane promotion, and
cancellation of in-flight layout creation.

**Indefinite retention** (FR-021): managed-layout and managed-pane
audit records are preserved indefinitely in MVP. Pruning is deferred to
a later feature.

**Authorization** (spec §Assumptions): MVP is socket-access-based —
any caller with access to the host daemon's local socket can create
managed layouts. Per-user or per-container ACL is a later hardening
feature. `app.managed_*` is host-only via FEAT-011's gate; legacy
`managed.*` is peer-scoped (a bench-container thin client may only act
on its own container).

---

## See also

- Spec: [`specs/013-managed-session-lifecycle/spec.md`](../specs/013-managed-session-lifecycle/spec.md)
- Quickstart: [`specs/013-managed-session-lifecycle/quickstart.md`](../specs/013-managed-session-lifecycle/quickstart.md)
- Contracts: [`specs/013-managed-session-lifecycle/contracts/`](../specs/013-managed-session-lifecycle/contracts/)
- Research decisions: [`specs/013-managed-session-lifecycle/research.md`](../specs/013-managed-session-lifecycle/research.md)
- Data model: [`specs/013-managed-session-lifecycle/data-model.md`](../specs/013-managed-session-lifecycle/data-model.md)
22 changes: 22 additions & 0 deletions examples/launch_commands/bash-placeholder.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Example launch command profile.
#
# Copy this file to ~/.config/opensoft/agenttower/launch_commands/ and
# adjust `name:`, `command:`, `env:`, `working_dir:` to your needs.
#
# Per research §R9, `command:` MUST be a list of strings (argv) — never
# a single shell string. The daemon passes argv directly to tmux's
# new-session / split-window invocations, so shell interpolation does
# not apply (Principle III safety).
#
# `env:` is optional. Per FR-021 the daemon redacts environment-variable
# values whose key matches `*TOKEN*` / `*SECRET*` / `*KEY*` / `*PASSWORD*`
# (case-insensitive substring) in the JSONL lifecycle event payloads
# retained indefinitely. Command argv and working_dir are not redacted.
#
# `working_dir:` is optional and applied via tmux's `-c` flag (no shell).

name: bash-placeholder
command: ["bash", "-lc", "echo 'agent ready'; exec bash"]
env:
AGENTTOWER_ROLE: example
working_dir: /workspace
Loading
Loading