Commit graph

4 commits

Author SHA1 Message Date
48a81c0802 fix(agent-team): centralize safe decision mapping + remove free-text abandon hair-trigger
Security-review follow-up (LOGIC-01/02/05, all confirmed correctness).

- New transport-neutral `decisions.normalize_decision(raw, *, allow_abandon)` is
  the single source of truth: approve-allowlist→approve; abandon-allowlist→abandon
  ONLY when allow_abandon; everything else (prose, empty, abandon-verbs when
  disallowed) → request_changes with the full reply as notes; idempotent on an
  already-formed decision dict. slack_adapter.map_plan_decision is now a thin
  wrapper (default allow_abandon=True, no caller churn).
- LOGIC-01/02: graph._parse_decision now delegates to normalize_decision (was:
  any unrecognized verb → abandon → FAILED). The graph is now the universal safe
  backstop, so EVERY writer that bypassed the listener mapping — operator CLI
  answer_on_behalf (raw), Coordinator.submit_answer (raw), the recovery sweep —
  loops back on prose instead of silently FAILing the task. Explicit abandon
  still abandons (preserves the confirmed-button path).
- LOGIC-05: the Slack FREE-TEXT reply path maps with allow_abandon=False, so a
  bare "cancel"/"stop"/"abandon" typed in-thread → request_changes (never
  terminal abandon); abandon stays reachable only via the confirm-guarded button.

Tests: graph unrecognized→loops-back (not FAILED), operator raw-prose→request_
changes, free-text destructive verbs→request_changes vs button→abandon,
normalizer idempotency. 1484 passed.
2026-06-24 11:48:04 -04:00
f4de957915 feat(agent-team): Slack decision surface for the plan-review gate (Phase B3)
Turn a human's Slack interaction at the plan gate into a structured decision the
graph can route, with the kind-aware mapping that closes a silent-FAIL hazard.

- KIND-AWARE NORMALIZATION (load-bearing): map_plan_decision() in slack_adapter
  maps a reply to {"decision","notes"} — approve ∈ {approve,approved,yes,ok,lgtm,
  ship}; abandon ∈ {abandon,reject,cancel,stop,kill}; EVERYTHING ELSE →
  request_changes with the full reply as notes (never accidental abandon). Wired
  in the listener's _resolve_payload for plan_decision rows ONLY (clarify passes
  through). Without this, arbitrary change-notes hit the graph's
  unrecognized-verb→FAILED path and silently fail the task. Anti-FAIL tests
  assert prose → request_changes (!= abandon) at both the mapper and the
  end-to-end listener seam; a regression test guards clarify pass-through.
  New find_open_question_kind_by_channel_ref (anti-replay, status='open') powers
  the thread-reply fallback's kind lookup.
- BUTTONS + MODAL: build_plan_decision_blocks() renders Approve (primary) /
  Request changes / Abandon (danger+confirm); question_id double-anchored in
  message metadata AND each button value ("<verb>:<question_id>"). Approve/abandon
  submit via the existing @app.action(.*); request_changes has a dedicated
  handler that AUTHORIZES before views_open (proven by test) and opens a notes
  modal (private_metadata carries the id) → view_submission → request_changes +
  notes. Free-text reply stays the always-available equal path. AUTHZ-01 ordering
  preserved.
- No manifest change (views.open needs no extra scope).

1454 passed (1412 + 42).
2026-06-23 21:03:54 -04:00
3847e43ba3 feat(agent-team): one Slack thread per task — root "Task received" message + threaded questions/milestones
WS Slack-UX Feature 1. A /new-task task now maps to ONE Slack thread instead of
several top-level messages.

- /new-task posts an immediate root "📥 Task received: …" ack and captures its
  ts (root_ts); this is the instant acknowledgement.
- root_ts is plumbed into start: new PipelineState/TaskRecord channel
  slack_thread_ts, seeded by graph.start_task and threaded through
  Coordinator.start_task. The NewTaskCallback is now (task_text, via, root_ts).
- All clarifier questions for the task post as THREADED REPLIES under root_ts
  (chat.postMessage thread_ts=root_ts), and each question's ledger channel_ref
  is set to root_ts (NOT the reply's own ts). Because answer-mapping resolves a
  reply via find_open_question_by_channel_ref(thread_ts), a reply in the root
  thread (thread_ts==root_ts) maps to the task's currently-open question with NO
  change to the mapping logic or the first-answer-wins CAS. The open-only
  partial-unique index still holds (one open question per task at a time).
- Lifecycle milestones (parked / plan-ready / needs-input) and follow-up
  questions thread under root_ts too; the notify sink gained an optional
  thread_ts kwarg (degrades to top-level on a sink that doesn't accept it).
  notify failures still never break tick.
- SlackTransport.post_question + the live poster accept/forward thread_ts.
- No root_ts (non-/new-task origin) ⇒ top-level posts exactly as before.

AUTHZ-01 (owner-allowlist-first, fail-closed) and the atomic open→answered
compare-and-set are unchanged.

Adds plumbing for the inbound-ack reactor seam used by Feature 2 (dormant until
a reactor is injected). Tests cover thread_ts forwarding, channel_ref=root_ts,
graph seeding, and coordinator threading.
2026-06-23 15:49:40 -04:00
15a416d31a Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.

Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled

KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven

Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 15:16:12 -04:00