+ The next revision unlocks after execution-verified delivery and before Apply. Undo an
+ applied change set first if you want to continue this lineage.
+
+ ) : (
+
+ This is immutable history. Continue from the latest revision.
+
+ )}
+
+ {error &&
{error}
}
+
+ );
+}
diff --git a/apps/web/lib/api-client.ts b/apps/web/lib/api-client.ts
index 4d3d685..1bc9b1e 100644
--- a/apps/web/lib/api-client.ts
+++ b/apps/web/lib/api-client.ts
@@ -154,6 +154,20 @@ export const tasksApi = {
apiFetch('/api/v1/tasks', { method: 'POST', body: JSON.stringify(body) }),
cancel: (id: string) => apiFetch(`/api/v1/tasks/${id}/cancel`, { method: 'POST' }),
retry: (id: string) => apiFetch(`/api/v1/tasks/${id}/retry`, { method: 'POST' }),
+ revisions: (id: string) => apiFetch(`/api/v1/tasks/${id}/revisions`),
+ createRevision: (
+ id: string,
+ body: {
+ feedback: string;
+ kind: 'implementation_fix' | 'product_decision';
+ autostart?: boolean;
+ },
+ ) =>
+ apiFetch(`/api/v1/tasks/${id}/revisions`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ timeoutMs: 30_000,
+ }),
upload: uploadFile,
start: (id: string) => apiFetch(`/api/v1/tasks/${id}/start`, { method: 'POST' }),
respond: (id: string, answer: string) =>
diff --git a/docs/STRATEGY.md b/docs/STRATEGY.md
index 9c11aa5..31275b7 100644
--- a/docs/STRATEGY.md
+++ b/docs/STRATEGY.md
@@ -255,6 +255,23 @@ includes median/tail steps, tokens, time, questions, and stop reasons.
### Gate 5 — Developer-feedback loop
+**Implementation status (2026-07-22): first end-to-end revision slice complete; visual
+version delta and real-provider evidence remain open.** Every new local-project task now
+starts a Product Session. After an execution-verified delivery, the UI can record either
+an implementation correction or a changed product decision and create the next immutable
+revision. Revision creation verifies the prior Receipt and locked contract, seeds a fresh
+isolated checkout with the exact prior patch, content-addresses the new specification,
+and carries the prior contract gate into implementation corrections. Only the latest
+revision can Apply; source and database locks plus a unique revision index admit one
+concurrent successor. The v1 task, contract, Receipt, workspace, and rollback evidence
+remain addressable through the version navigator.
+
+The deterministic integration path now proves v1 feedback → spec v2 → cumulative verified
+change set v2 → Apply → Undo while v1 stays integrity-valid. Gate 5 is not yet marked
+complete because the UI shows each cumulative change set but does not yet render a direct
+v1-to-v2 visual/output delta, and the whole two-delivery journey still needs a recorded
+real-provider run.
+
- Group successive deliveries into a Product Session with versioned specifications,
feedback deltas, change sets, and Receipts.
- Show evidence and visual/output differences between versions.
diff --git a/docs/adr/0011-product-session-revisions.md b/docs/adr/0011-product-session-revisions.md
new file mode 100644
index 0000000..56bd8c4
--- /dev/null
+++ b/docs/adr/0011-product-session-revisions.md
@@ -0,0 +1,52 @@
+# ADR 0011: Product Session revision lineage
+
+- Status: accepted
+- Date: 2026-07-22
+
+## Context
+
+A verified task is a delivery, but developer feedback often arrives only after that
+delivery is inspected. Treating the feedback as an unrelated retry loses the relationship
+between specifications, contracts, evidence, and patches. Reusing `parent_id` would also
+conflate product history with the existing sub-agent execution tree.
+
+The change-set model adds another constraint: Apply writes a cumulative patch to the source
+repository. If an older revision is applied while a newer one is being prepared, the newer
+patch no longer has the clean base that makes Apply and Undo deterministic.
+
+## Decision
+
+Local-project tasks receive a separate Product Session identity and monotonically increasing
+revision number. Each revision stores its previous task, feedback type and text,
+content-addressed product specification, and superseding task.
+
+A successor is created only when the current latest revision:
+
+- completed with execution-backed acceptance and a valid Receipt;
+- has a locked acceptance contract;
+- still has a pending change set, or was applied and then undone; and
+- points to the same clean source commit.
+
+The runtime clones that immutable base and applies the prior Receipt-bound patch into the new
+workspace. Work on v2 therefore starts from the exact verified v1 delivery, while v2's final
+change set remains cumulative against the original base. Only the latest revision may Apply.
+
+Implementation corrections retain a gate for the previous acceptance contract and add the
+feedback as a regression requirement. Product decisions preserve the prior contract as
+history but allow the new contract to supersede it, because a legitimate decision may
+contradict an earlier requirement. Both meanings remain explicit in the specification and
+Receipt.
+
+Revision creation holds the existing cross-process source lock and a database row lock. A
+unique `(product_session_id, product_revision)` index is the final race guard.
+
+## Consequences
+
+- Product history and sub-agent topology remain independent.
+- v1 specifications, contracts, Receipts, workspaces, and change sets remain auditable after
+ v2 exists.
+- Apply and Undo keep one cumulative, base-relative patch model.
+- A user must Undo an applied revision before continuing its lineage. Loop will not silently
+ rewrite a dirty source tree.
+- Direct visual/output diffs between adjacent revisions require a separate derived-delta
+ view; the first slice exposes cumulative change sets and version navigation only.
diff --git a/packages/api-contract/src/index.ts b/packages/api-contract/src/index.ts
index b5cd11f..091cf2c 100644
--- a/packages/api-contract/src/index.ts
+++ b/packages/api-contract/src/index.ts
@@ -135,6 +135,30 @@ export interface ChangeSet extends ProjectChangeSet {
blocked_reason: string | null;
}
+export interface ProductRevision {
+ session_id: string;
+ revision: number;
+ previous_task_id: string | null;
+ superseded_by_task_id: string | null;
+ feedback_kind: 'implementation_fix' | 'product_decision' | null;
+ feedback_delta: string | null;
+ specification: {
+ schema: 'loop.product-specification/v1';
+ original_goal: string;
+ required_acceptance_criteria: string[];
+ feedback_history: Array<{
+ revision: number;
+ kind: 'implementation_fix' | 'product_decision';
+ feedback: string;
+ previous_task_id: string;
+ }>;
+ previous_contract_hash: string | null;
+ previous_receipt_hash: string | null;
+ };
+ specification_hash: string;
+ is_latest: boolean;
+}
+
export interface Task {
id: string;
goal: string;
@@ -179,6 +203,7 @@ export interface Task {
skill: string | null;
parent_id: string | null;
depth: number;
+ product_revision: ProductRevision | null;
idempotency_key: string | null;
attempt: number;
limits: Limits;
diff --git a/scripts/demo.sh b/scripts/demo.sh
index 441e34c..1830e4f 100755
--- a/scripts/demo.sh
+++ b/scripts/demo.sh
@@ -7,6 +7,7 @@ web_port="${LOOP_DEMO_WEB_PORT:-3000}"
demo_dir="$root/.demo"
api_log="$demo_dir/api.log"
web_log="$demo_dir/web.log"
+demo_db=""
next_env="$root/apps/web/next-env.d.ts"
next_env_backup="$demo_dir/next-env.d.ts"
api_pid=""
@@ -24,6 +25,7 @@ cleanup() {
[[ -n "$api_pid" ]] && kill "$api_pid" 2>/dev/null || true
[[ -n "$web_pid" ]] && wait "$web_pid" 2>/dev/null || true
[[ -n "$api_pid" ]] && wait "$api_pid" 2>/dev/null || true
+ [[ -n "$demo_db" && -f "$demo_db" ]] && rm -f "$demo_db"
if [[ -f "$next_env_backup" ]]; then
cp "$next_env_backup" "$next_env"
rm -f "$next_env_backup"
@@ -89,6 +91,7 @@ port_available "$api_port" || fail "API port $api_port is already in use (set LO
port_available "$web_port" || fail "Web port $web_port is already in use (set LOOP_DEMO_WEB_PORT)."
mkdir -p "$demo_dir/workspaces" "$demo_dir/memory"
+demo_db="$(mktemp "$demo_dir/loop.XXXXXX.db")"
cp "$next_env" "$next_env_backup"
: >"$api_log"
: >"$web_log"
@@ -96,6 +99,11 @@ demo_token="$(apps/api/.venv/bin/python -c 'import secrets; print(secrets.token_
printf '%s' "$demo_token" >"$demo_dir/token"
chmod 600 "$demo_dir/token"
+(
+ cd apps/api
+ DATABASE_URL="sqlite+aiosqlite:///$demo_db" .venv/bin/alembic upgrade head
+) >>"$api_log" 2>&1
+
(
cd apps/api
API_TOKEN="$demo_token" \
@@ -105,7 +113,7 @@ chmod 600 "$demo_dir/token"
EXECUTION_MODE=inline \
CACHE_BACKEND=memory \
AGENT_SANDBOX=inline \
- DATABASE_URL="sqlite+aiosqlite:///$demo_dir/loop.db" \
+ DATABASE_URL="sqlite+aiosqlite:///$demo_db" \
AGENT_WORKSPACES_ROOT="$demo_dir/workspaces" \
AGENT_MEMORY_ROOT="$demo_dir/memory" \
.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port "$api_port"
diff --git a/scripts/k8s-deployment-acceptance.sh b/scripts/k8s-deployment-acceptance.sh
index 8eba5e0..9acf24a 100755
--- a/scripts/k8s-deployment-acceptance.sh
+++ b/scripts/k8s-deployment-acceptance.sh
@@ -312,7 +312,7 @@ migration="$(
kubectl exec deployment/postgres --namespace "$namespace" -- \
psql -U app -d app -tAc 'SELECT version_num FROM alembic_version'
)"
-if [[ "$migration" != "0011_loop_state_machine" ]]; then
+if [[ "$migration" != "0012_product_sessions" ]]; then
echo "Unexpected Alembic revision: $migration" >&2
exit 1
fi