Skip to content

feat: lifecycle-driven Looper with native Plane collaboration - #526

Open
lefarcen wants to merge 178 commits into
mainfrom
feat/looper-auto-flowchart-runtime
Open

feat: lifecycle-driven Looper with native Plane collaboration#526
lefarcen wants to merge 178 commits into
mainfrom
feat/looper-auto-flowchart-runtime

Conversation

@lefarcen

@lefarcen lefarcen commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

目标

把 Looper 的规划、决策、实现、评审和通知收敛成可持续运行的生命周期,并让多位同事可以把自己的本机 Looper 连接到 Plane、直接接收任务。

主要变化

  • Plane / GitHub 成为任务、状态和人工回答的唯一事实源;飞书改为单向通知。
  • Plane 可绑定 owner 的单台本机 Looper,安全派发任务并展示排队、运行、失败、等待决策、评审和完成状态。
  • 严格协议采用签名、会话 fencing、nonce 防重放和终止证据,避免离线节点、旧进程或错误 owner 领取任务。
  • 规划前先做需求调研与 GRILL,把问题按产品、设计、研发、QA 分流。
  • 产品、设计和研发负责人在 Plane 内自然语言多轮沟通;Looper 判断回复是否充分,不足时继续追问,收敛后自动恢复。
  • 正式产品 Spec 保留独立文档门禁;聊天中的“你自己定”不能绕过 PROD-000。
  • 收齐决策后再写中文技术 Spec,经 GRILL + 独立 REVIEW,最后由 Looper owner 审批。
  • worker 可持续 shepherd 实现 PR;Plane 状态随实现、In Review 和完成阶段同步。
  • 加入生命周期引擎、阻塞条件、磁盘背压、可恢复基础设施重试和隔离 E2E 覆盖。
  • Looper 不自动合并 PR;最终合并仍由人完成。

配套 Plane PR

兼容与取舍

  • 移除 Feishu inbound HITL 路由;访问旧路由明确返回 404。
  • 决策通知必须包含 Plane / GitHub 的精确回答链接。
  • 当前一个 Plane 账号只绑定一台活跃 Looper 节点,多节点留到后续版本。
  • 保留本地/runtime handoff mailbox,但没有任何飞书入站入口。

验证

  • go test ./internal/planestrict ./internal/planner/... ./internal/runtime -count=1
  • go vet ./...
  • go build ./...
  • Plane 后端契约 17 项、前端状态 11 项及类型检查通过
  • 隔离 Plane 浏览器流程和 signed request → follow-up → resolution 协议链路通过
  • 全量 go test ./... 曾出现一次既有 timing-sensitive agent 测试失败,立即单独重跑通过

lefarcen and others added 30 commits July 6, 2026 23:08
Two fixes surfaced by the live e2e:
- P6: onboarding started looperd with a bare `nohup &` that dies on reboot/
  logout and never restarts. Switch to `looper daemon install` + `looper
  daemon start --daemon-restart-policy on-failure` (launchd RunAtLoad + restart-
  on-crash), so a closed laptop resumes in-flight work on wake. The sourced
  LOOPER_* secrets are captured into the launchd service and persist across
  reboots.
- P4: the bundle left the planner on (default), so one issue produced both a
  spec PR and an impl PR. Disable the planner (worker-only: label looper:plan +
  assign self implements directly, one PR). Also set requireAssigneeCurrentUser
  so assigning yourself actually routes the issue to your looper.

Docs (SETUP-PROMPT, GUIDE) updated to match. Config validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rding

Card correctness fixes surfaced by the live e2e (§3 of the HITL redesign plan):
- P1: ensureFeishuThreadRoot was a lock-free check-then-act, so two concurrent
  callers for one loop (progress ticker + an ask) could each POST a duplicate
  anchor before either persisted. Add a per-loop lock so the first caller claims
  (post + persist) before the second re-checks → exactly one anchor.
- P2: feishuPhaseFromTail's default branch echoed the raw shell command onto the
  human-scannable anchor (e.g. 'tmpdir=$(mktemp -d …'). An unrecognised command
  now becomes a generic '正在处理…'; the raw feed stays inside the thread.
- P3: a worker 'completed' only means the PR is OPENED, not merged. The header
  now shows '✅ 已交付 · 待合并' when there's a PR (turquoise), '🎉 已合并' only
  when actually merged, and '✅ Looper 已完成' only for a no-PR run.

Unit tests added/updated. (Cross-role label-overlap validation deferred — it
conflicts with the current Plane bootstrap which intentionally shares looper:plan.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op level)

The card wording still showed '已完成' after a worker opened a PR because
loopWorkerString reads $.worker.prUrl but prUrl is stored at the metadata top
level, so prURL was empty and hasPR=false. A completed worker loop's target
flips to pr:repo:N, so derive hasPR from that (plus prURL). Live-verified: the
card now flips to '✅ 已交付 · 待合并' on completion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single issue firing both the planner and the worker (one need → two PRs) is a
config mistake, not intended behaviour. validateNoIssueRoleTriggerLabelOverlap
flags it, while exempting the Plane single-label lifecycle where both roles share
one label on purpose and route by assignee UUID (RequireAssigneeCurrentUser=false
on both). Completes PR-1's §5 checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n §B)

The planner/worker/reviewer/fixer loops spawned for ONE issue each posted their
own anchor card. Re-key the card from per-loop to the originating issue
(issue:repo:N): add a task_key column (migration 0019) + RootByTask lookup, and
resolve/lock/upsert the anchor by task key so sibling loops collapse onto one
card. Loops with no source issue (PR-triggered, project-level, issue-less bug)
fall back to per-loop keying, unchanged. A loop joining an existing task card
re-points reply routing at the active loop.

Foundational for the PR-status mirror (§A) and looper:auto planner→worker chaining
(§C/§D). Unit-tested both metadata shapes and RootByTask sharing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plan §A)

Once the worker has delivered (completed) and a PR exists, the card header now
reflects the PR's REAL state instead of a generic '待合并': 👀 待 review → 🔄 CI
检查中 → ✋ 待修改 / ❌ CI 失败 → ✅ 待合并, derived from the PR's latest snapshot
(ReviewState + ChecksSummary). Two parts:

- Render-time mirror in feishuThreadHeaderCard: reads the latest pull_request
  snapshot for the task's PR and maps it to the §A title table. Falls back to the
  loop-status wording when there's no snapshot, so nothing regresses.
- Event-driven refresh: after a snapshot is captured (processSnapshotQueueItem),
  resolve the PR → its worker loop (LoopsRepository.GetByTargetID) → refresh that
  task's card, so the header advances through review without a poll.

'🎉 已合并' stays a merge-detection terminal (not produced from a review snapshot);
the card honestly rests at '✅ 待合并' until merge is observed. Unit-tested the
state mapping, check rollup, PR-number parsing, and GetByTargetID.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (plan §F/P5)

Before, a thread reply to a completed task was silently queued to a loop that
never runs again. Now, when a message routes to a finished loop (completed / done
/ merged), the poll posts ONE honest reply — 'task is done; continue on the
issue/PR or open a new task' — and skips the dead-end enqueue. At most once per
loop (in-memory dedupe), and only for finished-well states; failed/abandoned loops
still queue messages since they may be retried. Unit-tested the poll routing and
the finished-state predicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…umes (plan §E)

Full recovery (orphan cleanup, lock release, requeue) previously ran only ONCE at
process startup — but closing a laptop suspends the daemon without restarting it,
so on wake none of that reran and wedged work stayed wedged. The live stale-run
reconcile that did exist only fired when every slot was full (availableSlots==0)
and didn't touch locks.

The claim pump now detects a suspend/resume (a gap between 1s passes far larger
than the tick) and also runs periodically, triggering a lightweight wake reconcile:
release expired locks + reconcile stale running runs (which repairs the queue).
Unlike startup recovery it does NOT force-interrupt live runs — after a suspend the
agent subprocesses are usually still alive — so it only touches provably-dead state.
Expired-lock release is factored out and shared with startup. Unit-tested the
trigger decision (wall-clock jump vs periodic vs neither).

§E.1 (launchd managed daemon) shipped earlier; §E.3 (no duplicate card on resume)
is covered by the task_key anchor keying (§B) surviving restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lan §C/§D)

Adds the looper:auto label and makes the dispatch decision treat it as a per-issue
opt-in: an issue carrying looper:auto takes the autonomous path (plan → implement,
no /plan or /implement command) even when the coordinator is otherwise human-gated,
and skips the autonomous cool-off since the human already committed by labelling.
It still respects triage state, the hold label, and dependency gates. The label is
registered in StandardLooperLabels so repos get it.

This is the label + routing opt-in the plan calls for; the coordinator is off by
default, so it's inert until enabled (zero production risk). The full plan→implement
re-transition after a spec is approved rides the coordinator's existing autonomous
dispatch — wiring that end-to-end (with the §8.6 human-approve spec gate) is the
remaining flowchart work. Unit-tested the opt-in, the cool-off skip, the
human-gated fallback without the label, and triage/hold gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (plan §8.3)

Adds ProjectRefConfig.productOwner (Feishu open_id + optional Plane id) — the
person looper @-mentions in the task thread when a feature arrives without a
product spec. Resolver ProjectProductOwner + validation that the open_id, when set,
is trimmed and shaped like an open_id (ou_…) so an @-mention never resolves to
nobody. This is the config foundation for §8.3; the missing-spec detection +
@-mention + wait/resume flow rides on the Plane spec pipeline (§8.2), which is the
remaining flowchart work. Unit-tested the resolver and validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plane CLI (§8.2)

New internal/infra/planedoc: reads/writes Plane Pages and associates them with
work items through native Plane work-item Links, by shelling out to the plane CLI
(mirrors how github shells out to gh). This is the base for the tech-spec pipeline
(specs live in Plane, not repo files) and the page↔work-item convention (a native
link tagged looper:product-spec / looper:tech-spec):

- CreatePage / PageContent (markdown → HTML, round-trips)
- ListWorkItemLinks / FindSpecLink (reverse lookup: which page is the spec)
- UpsertSpecLink (idempotent create-or-repoint by title tag)
- Tools.planePath config + auto-detection (mirrors ghPath/gitPath)

Unit-tested arg construction + JSON decoding with an injected run func; a live,
env-gated round-trip (PLANE_LIVE_E2E=1) verified create → link → reverse-query →
read → cleanup against the real deployment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the worker/planner-facing spec ops on top of the §8.2 primitives:
- ReadSpec: resolve a work item's product/tech spec (link → page → body); returns
  the raw URL when the link points at a non-Plane doc a human dropped.
- WriteTechSpec: create the tech-spec page AND associate it (idempotent) in one call.
- PageIDFromURL: parse a page id back out of a page URL.

Unit-tested; the live round-trip (PLANE_LIVE_E2E=1) now exercises WriteTechSpec +
ReadSpec end-to-end against the real deployment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…8.3)

AssociateDroppedSpec acts on an agent's judgment that a thread message is a spec:
a URL is linked directly (Plane page / Feishu doc / any URL); raw pasted spec text
is captured into a Plane page first, then linked. Both tag the link
looper:product-spec / looper:tech-spec so the reverse lookup finds it. This is the
action layer for 'looper associates a spec on the human's behalf when they just
drop it in the thread' — the LLM judgment feeds this. Unit-tested URL vs inline-text
paths + rejection of empty/unknown input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the previously-inert looper:needs-human label into a real gate. Opt-in via
roles.reviewer.discovery.specReview.requireHumanApproval (default false → today's
self-approve behaviour, byte-identical). When on, a CLEAN automated spec review no
longer self-approves to spec-ready: the reviewer adds looper:needs-human and holds
until a person submits an APPROVE review on the spec PR, then clears the label and
proceeds. The approval signal is unambiguous — GitHub forbids self-approval so
looper downgrades its own APPROVE to a COMMENT, hence any APPROVED review is a
human's. Pure decision (decideSpecReviewGate) + hasHumanApproval unit-tested
(full truth table); freshDetail already carries reviews via the reviewer's
ViewPullRequest path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ork item (§8.0/§8.3 nodes D,E)

The front of the looper:auto feature flowchart, Plane side:
- HasProductSpec / DecideIntake: does the work item have a product-spec link → proceed vs request.
- CommentOnWorkItem / RequestProductSpec: ask the product owner (HTML-escaped) to
  supply a spec, telling them looper will auto-associate whatever link/text they reply.

Composes the §8.2 primitives; comment op spike-verified against real Plane. This is
the intake-gate slice; wiring it into a runtime driver + the planner/worker Plane-spec
changes are the remaining orchestration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hart node D)

Wires node D into the dispatch decision: a kind/feature issue heading to the planner
(dispatch/plan) is held until it has a product spec — the runner feeds the Plane
product-spec presence into Issue.HasProductSpec, and dispatch returns RequestProductSpec
(ask product + hold) instead of dispatching. Off by default (RequireProductSpecForPlan);
bugs heading to a tech spec are exempt (they don't need a product spec); unknown spec
status no-ops. Pure decision, unit-tested (has/no spec, gate off, bug exempt, unknown).

The Plane read + the @product/wait/resume runtime is the next wiring; this is node D's
decision core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oject (§8 runtime base)

Shared foundation for wiring the Plane spec pipeline into the planner/worker/reviewer
runtimes: given a project whose task-source provider is 'plane', builds a planedoc
gateway (from the provider's workspace/baseUrl/tokenEnv + Tools.planePath) and returns
the Plane project UUID; returns false for github/forgejo projects so they keep the
repo-file spec path unchanged. Unit-tested (plane vs github vs default vs unknown vs
nil). First plumbing of the invasive flowchart-runtime refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ure (nodes D/E)

Wires the flowchart's node D/E into the planner runtime. On a Plane-provider project,
before writing a spec the planner checks whether the feature work item has a product
spec (planedoc link tagged looper:product-spec, resolving the work-item UUID from the
issue URL). If missing, it comments on the work item asking product for one and holds
(FailureManualIntervention) — no worktree, no tech spec — instead of planning without
a product spec. A no-op for github/forgejo projects, non-features (bugs/perf don't need
a product spec), or when the work item can't be resolved (fails open). Threads a
planedoc resolver (planeDocForProject) + WorkItemIDFromURL as the shared foundation.
Unit-tested: hold-without-spec, proceed-with-spec, skip bug/github/nil.

Auto-resume when the spec is supplied is the next piece (node E2, polling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…G, additive)

On a Plane project the planner now also writes the agent's tech spec to a Plane page
and links it to the work item as looper:tech-spec (planedoc.WriteTechSpec). Additive
+ best-effort + idempotent (skips when a tech-spec page is already linked): the GitHub
spec PR stays the review surface for now, so nothing breaks; Plane-page review (node H)
and dropping the repo spec PR are the coupled next steps. No-op for github/forgejo or
when there's no spec file. Unit-tested: write+link, idempotent, non-plane, file read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… I, additive)

On a Plane project the worker now reads the work item's linked product + tech spec
pages (planedoc.ReadSpec, resolving the work-item UUID from the issue URL) and appends
them to the agent prompt alongside the existing repo-file spec block. Additive +
best-effort: a github/forgejo project, an unresolvable work item, no linked spec, or a
Plane hiccup all leave the repo-file path untouched. Threads the same planedoc resolver
as the planner. Unit-tested: reads both specs, empty on no-links/non-plane/unresolvable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lane

Env-gated (PLANE_LIVE_E2E=1) planner integration test: a feature work item with no
product spec → productSpecGate comments on the real Plane work item asking product and
returns a hold. Verified against the real deployment (gate held + @Product comment
posted). Marks the planner↔Plane node D/E integration end-to-end verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rs (node E2)

Completes the node D/E hold into an auto-resume (flowchart node E2). A periodic poll
(reconcileAwaitingProductSpec, on the wake/periodic reconcile) scans paused planner
loops on Plane projects, re-checks whether the work item's product spec has appeared,
and re-queues the loop so the planner re-runs past the gate. Poll-based (looper has no
Plane webhook). Precise: only resumes planner loops that are paused AND haven't opened
a spec PR yet (still at the gate, not paused for a later reason) AND whose spec is now
present — no marking/mid-step loop mutation, so no race with the pause write.
Unit-tested the resume decision truth table + the metadata parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t (node Z)

Completes the card lifecycle (flowchart node Z): §A rested the card at ✅ 待合并; now
prCardStyleFromSnapshot reads the PR's lifecycle state (MERGED / CLOSED) from the
captured snapshot detail and flips the header to 🎉 已合并 (green, the only terminal)
or 🚫 已关闭. Falls through to the review-cycle style while the PR is open. Case-
insensitive state parse, tolerates MergedAt/ClosedAt timestamps. Unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Steers the worker agent to work a kind/bug issue by reproducing it first, locating the
root cause (not the symptom), then fixing and proving the reproduction passes — instead
of the spec-driven feature flow. Threads the issue's triage labels through the worker
payload (workerInput.Labels) so the prompt can branch on kind/bug. Unit-tested: bug gets
the reproduce/root-cause steering, feature does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(spec linked)

Extends the live planner gate test to cover both branches against real Plane: a feature
with no product spec holds (@Product + manual intervention), then once a product-spec
page is linked the same gate proceeds. Both verified end-to-end (7.6s live run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s (node G ordering)

Moves publishTechSpecToPlane to immediately after the spec PR is confirmed, before the
label/reviewer steps, so the Plane tech-spec page lands even if a later publish step
retries. Best-effort + idempotent, unchanged behaviour otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…UID)

Requesting a PR reviewer that isn't a GitHub collaborator (a Plane work item's
assignee is a UUID) returned 422 and, since the step was retryable, retried forever —
each retry re-created the worktree and re-pushed the already-pushed branch, hitting
non-fast-forward / 'protected branch' and wedging the whole planner loop. Two fixes:
skip UUID-shaped reviewers (looksLikeUUID) so we never request them, and make the
reviewer-request best-effort (log + continue) so ANY reviewer failure can't wedge the
spec PR. Unit-tested the UUID shape check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op (non-ff)

A plain push (planner/worker, no expected-remote-SHA) that was rejected non-fast-
forward because the branch already exists on the remote — a prior attempt or the
agent already pushed it — used to hard-fail, and a retrying planner loop then wedged
re-pushing the same branch forever. Now, on a rejection where the remote branch does
exist, Push treats it as already-published (no fail, no force-push that could clobber
a good spec) and lets the caller adopt the existing branch/PR. Verified git + full
suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urface

Notion-style page comments landed in powerformer/plane PR #11 on the public
/api/v1. Reach them through the plane CLI's request escape hatch until it grows a
typed page-comment subcommand. This is the review/approve surface for flowchart
node H: the reviewer posts its assist-review as a page comment, a human replies to
approve. Live-verified against plane.powerformer.net (create → list → find).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The anchor card header collapsed every pre-PR phase into a generic 处理中. Make it
role-aware so the title names the flowchart lane: 🧭 分诊中 / 📝 编写技术方案中 /
👀 评审中 / 🔨 实现中, plus ⏸ 等待产品方案 (node E) distinct from a generic HITL
ask, and 👀 方案评审中 once a planner has written its tech spec to Plane (node H).
The worker's impl PR still mirrors the §A review cycle through to 🎉 已合并 (node Z);
planner specs live on Plane pages (no spec PR), so their review state is the label.
The planner sets an awaitingProductSpec metadata marker when its node-E gate holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lefarcen added 9 commits July 16, 2026 19:18
# Conflicts:
#	docs/GUIDE-hitl-setup.md
#	go.mod
#	internal/api/testdata/contracts/daemon-http.responses.compat.json
#	internal/config/config_test.go
#	internal/forge/plane.go
#	internal/forge/plane_test.go
#	internal/infra/notify/gateway.go
#	internal/infra/planedoc/gateway.go
#	internal/planner/product_spec_gate_test.go
#	internal/planner/runner.go
#	internal/planner/runner_test.go
#	internal/release/manifest_test.go
#	internal/runtime/awaiting_product_spec.go
#	internal/runtime/hitl_feishu_poll.go
#	internal/runtime/hitl_github_poll.go
#	internal/runtime/hitl_github_poll_test.go
#	internal/runtime/runtime.go
#	internal/runtime/scheduler.go
@PerishCode

Copy link
Copy Markdown
Contributor

@lefarcen I'm holding off on generating review comments for #526 because this pull request has merge conflicts right now.

Please resolve the conflicts with main and push the updated branch. Once that's done, request or wait for the review to run again and I'll take another look.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

# Conflicts:
#	internal/agent/executor.go
#	internal/agent/executor_test.go
#	internal/api/handler.go
#	internal/api/handler_hitl_test.go
#	internal/api/testdata/contracts/daemon-http.responses.compat.json
#	internal/cliapp/app.go
#	internal/cliapp/app_test.go
#	internal/config/types.go
#	internal/config/validate.go
#	internal/infra/github/gateway_test.go
#	internal/infra/notify/gateway.go
#	internal/infra/notify/gateway_feishu_app_test.go
#	internal/planner/runner.go
#	internal/planner/runner_test.go
#	internal/release/manifest_test.go
#	internal/reviewer/runner.go
#	internal/runtime/hitl_github_poll.go
#	internal/runtime/runtime.go
#	internal/runtime/scheduler.go
#	internal/storage/repositories_test.go
#	internal/webhookforward/forwarder.go
#	internal/worker/runner.go
#	internal/worker/runner_test.go
@lefarcen lefarcen changed the title feat: looper:auto flowchart runtime — worker-shepherd + node H spec pipeline + QA gate feat: lifecycle-driven Looper with role-routed planning Jul 20, 2026
@lefarcen lefarcen changed the title feat: lifecycle-driven Looper with role-routed planning feat: lifecycle-driven Looper with native Plane collaboration Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants