feat: lifecycle-driven Looper with native Plane collaboration - #526
Open
lefarcen wants to merge 178 commits into
Open
feat: lifecycle-driven Looper with native Plane collaboration#526lefarcen wants to merge 178 commits into
lefarcen wants to merge 178 commits into
Conversation
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>
# 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
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
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
目标
把 Looper 的规划、决策、实现、评审和通知收敛成可持续运行的生命周期,并让多位同事可以把自己的本机 Looper 连接到 Plane、直接接收任务。
主要变化
配套 Plane PR
兼容与取舍
验证
go test ./internal/planestrict ./internal/planner/... ./internal/runtime -count=1go vet ./...go build ./...go test ./...曾出现一次既有 timing-sensitive agent 测试失败,立即单独重跑通过