Przeglądaj źródła

integration: README — goal-era workflows, both-binaries harness, deployment assumptions

First-project/goal/task walkthrough rewritten for the flattened era
(goals, mandates/modes, enqueue-after; subtask machinery and the
plan-11 options gone), monitoring sections cover the goal control room
and the dashboard pile, block answering documents the dual-row goal
blocks with the agreed verdict wordings, the integration section
documents the 83-check both-real-binaries run (fresh DB, pushable
origin, shared workspace root, SADDLE_CALLBACK_BASE_URL +
SADDLE_GOAL_RESUME_S), and a new Deployment assumptions section pins
the shared /workspace volume across saddles, the DB
tear-down-and-recreate (flattened M0001) and the restart-both-sides
rule.
clanker 1 miesiąc temu
rodzic
commit
0f48fa48f8
1 zmienionych plików z 95 dodań i 38 usunięć
  1. 95 38
      README.md

+ 95 - 38
README.md

@@ -16,7 +16,7 @@ Browser ←→ clanker-saddle (Spry app: SQLite queue, Statum UI, dispatcher)
                any OpenAI-compatible chat-completions endpoint
                any OpenAI-compatible chat-completions endpoint
 ```
 ```
 
 
-The web app owns projects, tasks, dependencies and the saddle registry.
+The web app owns projects, goals, flat tasks and the saddle registry.
 Work acquisition is ask-based (plan 10): daemons sleep when idle and ask
 Work acquisition is ask-based (plan 10): daemons sleep when idle and ask
 `POST /api/saddle/next-task` (per-saddle bearer token); the allocation is
 `POST /api/saddle/next-task` (per-saddle bearer token); the allocation is
 one write transaction that pairs the asking saddle with the best
 one write transaction that pairs the asking saddle with the best
@@ -111,6 +111,7 @@ podman run -d --name clanker-web --userns=keep-id --network clanker-net \
     -e SPRY_SEED_ADMIN_USERNAME=admin -e SPRY_SEED_ADMIN_PASSWORD=change-me \
     -e SPRY_SEED_ADMIN_USERNAME=admin -e SPRY_SEED_ADMIN_PASSWORD=change-me \
     -e SADDLE_LLM_BASE_URL=http://127.0.0.1:9999 \
     -e SADDLE_LLM_BASE_URL=http://127.0.0.1:9999 \
     -e SADDLE_PROJECTS_ROOT=/workspace \
     -e SADDLE_PROJECTS_ROOT=/workspace \
+    -e SADDLE_CALLBACK_BASE_URL=http://clanker-web:8080 \
     localhost/clanker-saddle:0.1
     localhost/clanker-saddle:0.1
 podman run -d --name clanker-saddle --userns=keep-id --network clanker-net \
 podman run -d --name clanker-saddle --userns=keep-id --network clanker-net \
     -p 7100:7100 -v "$PWD/workspace:/workspace:Z" \
     -p 7100:7100 -v "$PWD/workspace:/workspace:Z" \
@@ -121,7 +122,10 @@ Register the saddle with its **network** address (`clanker-saddle:7100`)
 — the web app talks to the daemon container-to-container, while the LLM
 — the web app talks to the daemon container-to-container, while the LLM
 base URL is dialled by the daemon itself (a mock can run inside the
 base URL is dialled by the daemon itself (a mock can run inside the
 saddle container via `podman exec -d clanker-saddle …`, reachable at
 saddle container via `podman exec -d clanker-saddle …`, reachable at
-`http://127.0.0.1:9999`). Put the SQLite store on a volume
+`http://127.0.0.1:9999`). `SADDLE_CALLBACK_BASE_URL` (or the
+`web-config.json` `saddle_callback_base_url` key) must be the app's
+address **as the daemon dials it** — every registered daemon pushes
+telemetry, task callbacks and notes there. Put the SQLite store on a volume
 (`SPRY_DB_PATH` defaults to `/var/lib/clanker-saddle/clanker-saddle.sqlite`)
 (`SPRY_DB_PATH` defaults to `/var/lib/clanker-saddle/clanker-saddle.sqlite`)
 and mount git checkouts under `/workspace`. On SELinux hosts the bind
 and mount git checkouts under `/workspace`. On SELinux hosts the bind
 mounts need `:Z`, and rootless podman needs `--userns=keep-id` for the
 mounts need `:Z`, and rootless podman needs `--userns=keep-id` for the
@@ -163,15 +167,22 @@ revokes it).
 ## Monitoring agent progress
 ## Monitoring agent progress
 
 
 - **`/tasks`** — live queue: status chips (`draft → queued → assigned →
 - **`/tasks`** — live queue: status chips (`draft → queued → assigned →
-  working → blocked → done/failed`), dependency states and ready flags.
+  working → blocked → done/failed`), mode badges, goal links and the
+  standalone/by-goal filter.
 - **`/tasks/{id}`** — the control room; it refreshes on every state
 - **`/tasks/{id}`** — the control room; it refreshes on every state
   pulse while a saddle works: comments thread (yours and the agent's),
   pulse while a saddle works: comments thread (yours and the agent's),
-  blocks, subtasks, session history, and the agent's final summary.
+  blocks, goal context, session history, and the agent's final summary.
 - **`/tasks/{id}/terminal`** — the full bash session log: the raw PTY
 - **`/tasks/{id}/terminal`** — the full bash session log: the raw PTY
   stream the daemon pushes (chunk batches land in `session_raw_log` and
   stream the daemon pushes (chunk batches land in `session_raw_log` and
   stream over SSE byte-exact), with the typed entry view as the fallback
   stream over SSE byte-exact), with the typed entry view as the fallback
-  for legacy sessions.
-- **`/`** — dashboard: saddle cards, queue summary, recent activity.
+  for legacy sessions. The session picker honours `?session_id=` across
+  every run a task ever had.
+- **`/goals/{id}`** — the goal control room: lifecycle head, locked
+  brief, plan expander with revision history, task strip, the
+  supervision narrative and the completion review.
+- **`/`** — dashboard: the "needs your attention" pile (goal AND task
+  blocks interleaved, newest-first) above the activity timeline, saddle
+  cards and queue summary.
 
 
 ## Answering block questions
 ## Answering block questions
 
 
@@ -184,12 +195,14 @@ the dispatcher wakes the affine saddle, and the daemon's ask returns
 the assignment carrying the verdict as its `resume_notice` (the stable
 the assignment carrying the verdict as its `resume_notice` (the stable
 wordings — "Human selected: …", "Plan approved. Proceed with
 wordings — "Human selected: …", "Plan approved. Proceed with
 implementation.", "Review approved. Continue with the task." &c.) with
 implementation.", "Review approved. Continue with the task." &c.) with
-a **fresh session id**; the parked session closes as `resumed`. `saddle
-plan` parks the same way with a type=`plan` block ("Plan submitted for
-review"): Accept applies the markdown to the task's plan, Reject
-returns "Plan feedback received:\n…" — and the per-task auto-approve
-option (or an ancestor's subtask auto-approve toggle) answers plan
-blocks instantly web-side.
+a **fresh session id**; the parked session closes as `resumed`. A goal
+agent's blocks attach to the GOAL (dual-rowed: the goal block plus a
+quiet mirror on the run task) — one answer resolves both and resumes
+the run. `saddle plan` parks with the goal's plan-review block:
+Approve answers "Plan approved. Proceed with implementation." and
+applies the markdown as the goal's plan; the feedback textarea returns
+the agent to planning to revise. The completion review answers "Review
+approved. Continue with the task." to complete the goal.
 
 
 ## Browser notifications (Web Push)
 ## Browser notifications (Web Push)
 
 
@@ -206,9 +219,10 @@ To enable notifications in a browser: open the dashboard and press
 service worker (`GET /sw.js`, scope `/`) and subscribes with the app's
 service worker (`GET /sw.js`, scope `/`) and subscribes with the app's
 VAPID key. "Turn off notifications" reverses it. Per task, **Notify
 VAPID key. "Turn off notifications" reverses it. Per task, **Notify
 me** / **Don't notify me** (on the task page's status card) opts you
 me** / **Don't notify me** (on the task page's status card) opts you
-into that task's first start, failure and completion; questions, RFCs,
-review requests and the suspended-waiting-on-drafts condition notify
-every subscribed user. Clicking a notification opens the task's page.
+into that task's first start, failure and completion; task questions,
+RFCs and review requests notify every subscribed user, and goal blocks
+plus goal success/failure ALWAYS notify every subscribed user. Clicking
+a notification opens the task's or goal's page.
 Delivery is best-effort: failed sends retry on the dispatcher tick and
 Delivery is best-effort: failed sends retry on the dispatcher tick and
 endpoints answering 410/404 are pruned automatically.
 endpoints answering 410/404 are pruned automatically.
 
 
@@ -246,19 +260,32 @@ accumulated project knowledge.
 
 
 ## Integration testing
 ## Integration testing
 
 
-`test/integration_test.py` drives the whole loop against a running
-app + daemon pair (88 checks, Statum-style PASS/FAIL): explore task,
-dependency gating, then the Part 2 feature tour — `saddle plan`,
-`saddle query` with piped stdin, progress updates, a multiple-choice
-block answered through the web UI, an RFC block with a markdown body,
-the `saddle review` banner approved via the UI, a quoted user comment
-delivered over /notify, terminal SSE (replay + live), the bearer-token
-ingest API and the stm-markdown renderer. Host run:
+`test/integration_test.py` drives BOTH real binaries — this app and a
+real saddle daemon over a real git checkout — through the full goal-era
+arc (83 checks, Statum-style PASS/FAIL): goal create → start → pre-plan
+question (one answer resolving the goal row AND the mirrored run-task
+row) → plan reject-with-feedback → revision → approval on the agreed
+verdict words → task fan-out through the real `/api/saddle/task/*`
+callbacks → the worker's ephemeral worktree (auto-pruned, branch kept)
+→ the coalesced terminal-event resume → merge + branch delete +
+push-before-review → completion review accept → goal complete, plus the
+`/status` `goal_branches` cache, SSE over the agent's raw terminal, the
+`goal_events` narrative, the legacy ingest API surface and the
+stm-markdown renderer. Host run (the harness prepares a `demo` repo
+with a pushable bare origin when the workspace is empty; the database
+must be FRESH — the harness refuses a used one):
 
 
 ```bash
 ```bash
-mkdir -p /tmp/clanker-e2e/workspace && cd /tmp/clanker-e2e/workspace \
-    && git init demo && cd demo && git commit --allow-empty -m one \
-    && git commit --allow-empty -m two
+export SPRY_SEED_ADMIN_USERNAME=admin SPRY_SEED_ADMIN_PASSWORD=adminpass123
+export SADDLE_LLM_BASE_URL=http://127.0.0.1:9999 SADDLE_LLM_MODEL=mock
+export SADDLE_PROJECTS_ROOT=/tmp/clanker-e2e/workspace
+export SADDLE_CALLBACK_BASE_URL=http://localhost:8080
+export SPRY_DB_PATH=/tmp/clanker-e2e/web/clanker-saddle.sqlite
+export SADDLE_GOAL_RESUME_S=2   # shrink the coalescing window for tests
+./builddir/clanker-saddle 8080 &
+
+SADDLE_WORKSPACE_ROOT=/tmp/clanker-e2e/workspace \
+    ../saddle/builddir/src/saddle --port 7100 &
 
 
 WEB=http://localhost:8080 SADDLE=http://localhost:7100 \
 WEB=http://localhost:8080 SADDLE=http://localhost:7100 \
 WORKSPACE=/tmp/clanker-e2e/workspace \
 WORKSPACE=/tmp/clanker-e2e/workspace \
@@ -266,13 +293,17 @@ DB_PATH=/tmp/clanker-e2e/web/clanker-saddle.sqlite \
     python3 test/integration_test.py
     python3 test/integration_test.py
 ```
 ```
 
 
-For a container run point `WEB`/`SADDLE` at the published ports, pass
-`SADDLE_HOST=clanker-saddle:7100`, the container's SQLite file as
-`DB_PATH`, and start the in-container mock via `MOCK_CMD='podman exec -d
-clanker-saddle python3 /usr/libexec/saddle/mock_llm.py --port 9999'`
-(the test appends the same scripted responses to whatever command
-`MOCK_CMD` prefixes). The suite releases a daemon left blocked by an
-earlier run via `/stop` before it starts.
+The daemon's `SADDLE_WORKSPACE_ROOT` and the app's
+`SADDLE_PROJECTS_ROOT` point at the SAME directory (the goal clone the
+daemon cuts must anchor the worker worktrees — the shared-volume
+deployment assumption). For a container run point `WEB`/`SADDLE` at the
+published ports, pass `SADDLE_HOST=clanker-saddle:7100`, the container's
+SQLite file as `DB_PATH`, and start the in-container mock via
+`MOCK_CMD='podman exec -d clanker-saddle python3
+/usr/libexec/saddle/mock_llm.py --port 9999'` (the test appends the
+same scripted responses to whatever command `MOCK_CMD` prefixes). The
+suite releases a daemon left blocked by an earlier run via `/stop`
+before it starts.
 
 
 `meson test` additionally runs `test/web_stack_test.py` — a
 `meson test` additionally runs `test/web_stack_test.py` — a
 self-contained suite (no daemon needed) that spawns the app in a scratch
 self-contained suite (no daemon needed) that spawns the app in a scratch
@@ -288,11 +319,14 @@ idempotency, the one-probe staleness policy with its dead-daemon
 requeue, and the retired `/output`//`/log`//`/start` daemon routes 404ing
 requeue, and the retired `/output`//`/log`//`/start` daemon routes 404ing
 while the web side never calls them), and the phase-3 capability
 while the web side never calls them), and the phase-3 capability
 awareness + UI (the assignment `capabilities` context,
 awareness + UI (the assignment `capabilities` context,
-`GET /api/saddle/capabilities`, subtask `--capability` validation with
-the valid-tag list in the error, the task page's top status block and
-newest-first blocking sections, the dashboard's pinned link-through
-cards above the activity timeline, and the slow-ask lost-wake
-regression).
+`GET /api/saddle/capabilities`, task-callback `--capability` validation
+with the valid-tag list in the error, the task page's top status block
+and newest-first blocking sections, the dashboard's pile and timeline,
+and the slow-ask lost-wake regression), and the plan-12 goal era
+(lifecycle, plan/completion reviews with dual-rowed goal blocks and the
+agreed verdict wordings, task callbacks, coalesced terminal-event
+resumes, pause queueing, cancel cascade, auto-enqueue-after, goal
+notifications and the goal context of worker assignments).
 
 
 ## Realtime channels and the browser's SSE budget
 ## Realtime channels and the browser's SSE budget
 
 
@@ -306,6 +340,29 @@ terminal tabs close their stream until focused, and task pages refresh
 their state on focus in addition to each pulse. Serving the app over
 their state on focus in addition to each pulse. Serving the app over
 HTTP/2 removes the cap entirely.
 HTTP/2 removes the cap entirely.
 
 
+## Deployment assumptions (goal era)
+
+- **Shared workspace volume across saddles.** A goal's clone workspace
+  (`<root>/goal-<id>`, one clone per involved repo on the goal branch)
+  anchors every task worktree (`<root>/task-<task-id>`, ephemeral
+  branch `task/<goal-id>-<task-id>`). Whichever saddle runs a goal task
+  must see the same `SADDLE_WORKSPACE_ROOT` volume as the saddle that
+  runs the goal agent — today the fleet is single-saddle per goal by
+  construction (one goal agent, one worker at a time on the one
+  registered saddle); multi-saddle fleets must mount the same volume
+  everywhere before fanning goal work out.
+- **Database tear-down-and-recreate.** The plan-12 migrations flatten
+  the schema again (a fresh `M0001_Initial`: goals, goal links/comments/
+  blocks/events, the flattened tasks). There is NO upgrade path from a
+  plan-11 database — delete the SQLite file (or point `SPRY_DB_PATH` at
+  a fresh path) and let the app recreate it.
+- **Restart both sides.** The daemon (`saddle`) and this app must both
+  restart onto the goal-era builds together — the wire contract
+  (assignment `kind`/`mode`/`creator`/`goal`/`ephemeral_branch`, the
+  task callbacks, goal-keyed blocks, `/status` `goal_branches`) is only
+  honoured by matching pairs, and the daemon's registration/token
+  persists across its restarts while the app re-seeds on a fresh DB.
+
 ## Deployment (USM)
 ## Deployment (USM)
 
 
 `MANIFEST.usm` + `usm-scripts/` package the app as `bin:clanker-saddle`
 `MANIFEST.usm` + `usm-scripts/` package the app as `bin:clanker-saddle`