Skip to content

feat: Buttons Flow runtime with research-deck example - #285

Open
bobakemamian wants to merge 3 commits into
mainfrom
feat/buttons-flow-runtime
Open

feat: Buttons Flow runtime with research-deck example#285
bobakemamian wants to merge 3 commits into
mainfrom
feat/buttons-flow-runtime

Conversation

@bobakemamian

@bobakemamian bobakemamian commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Makes drawer_kind: "flow" pressable by compiling the flow definition in memory at press time into the existing drawer executor pipeline (provider-list → claim → perform → validate → apply → ensure-trigger), wired for both drawer press and webhook dispatch.
  • Adds schema v3 flow fields (gates, roles, prompts, capabilities), local/GitHub provider pipeline buttons, and thin buttons flow CLI sugar (init, task, status, logs, approve/reject, rm).
  • Ships builtin demo package @buttonsflow/research-deck (research → open-slide deck) with claim/staleness recovery and integration coverage.

Test plan

  • go test ./internal/drawer/ ./internal/flowkit/ ./internal/store/ ./cmd/
  • go test ./test/integration/ -run 'Flow|ResearchDeck|ButtonsFlow'
  • buttons add @buttonsflow/research-deck && buttons flow init research-deck
  • buttons flow task add research-deck "Q3 competitive landscape for leadership offsite"
  • Press until review gate, buttons flow approve, confirm buttons flow task list research-deck --filter status=done
  • Confirm webhook path compiles flow drawers (no silent no-op) and flow drawers still never persist steps on disk

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added flow board management, including setup, tasks, status, logs, approvals, scheduling, webhooks, and removal.
    • Added local and GitHub-backed execution with task claiming, recovery, transitions, and approval gates.
    • Added the built-in research-to-deck workflow with staged processing and human review.
    • Added provider, role, capability, evidence, feedback, and review configuration.
  • Bug Fixes

    • Flow drawers can now be prepared and executed through presses and webhooks.
    • Improved package installation when no registry is configured.

Compile drawer_kind:flow in memory at press time into the existing executor pipeline, ship local/GitHub provider buttons and flow CLI sugar, and replace the SWE demo with @buttonsflow/research-deck for open-slide workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds flow board management for local and GitHub providers. It adds flow drawer compilation, task lifecycle scripts, approval gates, scheduling, built-in research-deck support, and integration coverage for execution and recovery.

Changes

Flow execution and management

Layer / File(s) Summary
Flow contracts and compilation
docs/schemas/drawer.schema.json, internal/drawer/*
Flow definitions now support providers, roles, capabilities, evidence, prompts, and approval gates. Flow drawers compile into provider-specific action pipelines.
Provider buttons and task lifecycle
internal/config/paths.go, internal/flowkit/*
Flowkit installs local or GitHub buttons and implements task listing, claims, execution, transitions, CRUD, comments, approvals, and rejections.
Built-in research-deck package
internal/store/builtin.go, cmd/flow_research_deck.go, cmd/install.go
The built-in source provides the research-deck package and the installer creates its staged flow board.
Flow command and execution wiring
cmd/flow.go, cmd/drawer.go, cmd/serve.go, cmd/root.go
The CLI adds flow initialization, operations, status, logs, scheduling, and removal. Drawer and webhook execution prepare flow drawers before execution.
End-to-end flow validation
test/integration/*
Integration tests cover execution, approval progression, stale-claim recovery, GitHub workflow generation, and flow command setup.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FlowCLI
  participant Flowkit
  participant Provider
  User->>FlowCLI: initialize or press flow board
  FlowCLI->>Flowkit: ensure provider buttons and task store
  FlowCLI->>Provider: list and claim actionable task
  Provider-->>FlowCLI: task claim result
  FlowCLI->>Provider: perform, validate, and apply stage result
  Provider-->>User: status, approval, or completion result
Loading

Possibly related PRs

Suggested reviewers: prolificeric

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the Buttons Flow runtime and its research-deck example.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/buttons-flow-runtime

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (13)
internal/store/builtin.go (3)

148-156: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fall back to the primary source for unknown @buttonsflow/ packages.

Line 149 routes every @buttonsflow/ name to BuiltinSource. BuiltinSource.Fetch only serves @buttonsflow/research-deck. A registry-hosted package such as @buttonsflow/other therefore fails, even when Primary can serve it.

♻️ Proposed fallback
 func (s PreferBuiltin) Fetch(name, version string) (*Bundle, error) {
 	if strings.HasPrefix(name, "`@buttonsflow/`") {
-		return (&BuiltinSource{}).Fetch(name, version)
+		b, err := (&BuiltinSource{}).Fetch(name, version)
+		if err == nil || s.Primary == nil {
+			return b, err
+		}
 	}
 	if s.Primary == nil {
 		return nil, fmt.Errorf("package %q not found", name)
 	}
 	return s.Primary.Fetch(name, version)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/builtin.go` around lines 148 - 156, Update PreferBuiltin.Fetch
so `@buttonsflow/` names first use BuiltinSource, then fall back to
s.Primary.Fetch when the builtin lookup reports the package is unavailable.
Preserve the existing not-found error when Primary is nil, and continue routing
non-@buttonsflow/ names directly through Primary.

136-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Surface or document the discarded primary index error.

Line 141 receives an error from s.Primary.Index(), and line 143 returns nil. A registry outage then looks like an empty catalog. golangci-lint (nilerr) flags this. If the fallback is intentional, add a comment that states the intent, and log the error. If it is not intentional, propagate the error.

♻️ Proposed change to keep the fallback and record the cause
 	primary, err := s.Primary.Index()
 	if err != nil {
+		// Degrade to builtin-only when the registry is unreachable, but do
+		// not hide the cause from the operator.
+		fmt.Fprintf(os.Stderr, "warning: registry index unavailable: %v\n", err)
 		return builtin, nil
 	}

fmt is already imported; add os to the import block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/builtin.go` around lines 136 - 146, Update PreferBuiltin.Index
to handle the error returned by s.Primary.Index instead of silently returning
the builtin catalog: preserve the fallback only if intentional, document it at
the error branch, and log the discarded error using the suggested os-based
mechanism; otherwise propagate the error. Ensure the nilerr warning is resolved.

Source: Linters/SAST tools


60-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

The research-deck board is defined twice, and the two definitions have already drifted. researchDeckDrawer() builds the board as a drawer.Drawer literal. installResearchDeckBoard() rebuilds the same stage graph, worker agents, timeouts, gate, manager, and initial stage through AddFlowStage and SetFlowField. The stage prompts already differ: the builtin definition states the advance condition for each stage, and the CLI definition omits it. A user who installs the package therefore gets a different board than a user who runs buttons flow init research-deck.

Export one canonical definition and derive both paths from it.

  • internal/store/builtin.go#L60-L128: move this literal into a shared exported constructor, for example drawer.ResearchDeckDefinition(), and call it here.
  • cmd/flow_research_deck.go#L22-L63: replace the stage loop and the sets map with a single persist of the shared definition, so the CLI path and the package path produce identical boards.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/builtin.go` around lines 60 - 128, The research-deck flow is
defined independently in two locations and has drifted. In
internal/store/builtin.go:60-128, move the literal from researchDeckDrawer into
a shared exported constructor such as drawer.ResearchDeckDefinition, and have
researchDeckDrawer return that definition. In cmd/flow_research_deck.go:22-63,
remove the duplicated stage loop and sets map, then persist the shared
definition directly so both paths produce identical boards.
cmd/flow_research_deck.go (1)

37-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply the flow fields in a deterministic order.

sets is a map, so line 64 iterates in randomized order. Two consequences follow. First, a partial failure leaves a different on-disk state on each run, which makes install failures hard to reproduce. Second, SetFlowField persists the drawer on each call, so this loop performs 26 sequential writes for one install.

Use an ordered slice of path/value pairs. A single batched update would also remove the repeated writes, if the service exposes one.

♻️ Proposed change to an ordered slice
-	sets := map[string]any{
-		"initial_stage":                              "brief",
-		"manager.agent":                              "activation.manager",
+	sets := []struct {
+		path  string
+		value any
+	}{
+		{"initial_stage", "brief"},
+		{"manager.agent", "activation.manager"},
 		...
 	}
-	for path, value := range sets {
-		if _, err := svc.SetFlowField("research-deck", path, value); err != nil {
-			return fmt.Errorf("set flow.%s: %w", path, err)
+	for _, s := range sets {
+		if _, err := svc.SetFlowField("research-deck", s.path, s.value); err != nil {
+			return fmt.Errorf("set flow.%s: %w", s.path, err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/flow_research_deck.go` around lines 37 - 68, Replace the unordered sets
map and range loop in the flow setup with an ordered slice of path/value pairs,
preserving the current field order and values so SetFlowField applies changes
deterministically. If the service exposes a batch flow-field update API, use it
to persist the complete set in one operation; otherwise retain sequential
SetFlowField calls over the ordered slice and existing error wrapping.
internal/flowkit/install.go (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

claimWaitSeconds is unused.

golangci-lint reports the constant as dead. The scripts hardcode the same default through BUTTONS_FLOW_CLAIM_WAIT with fallback "1". Either remove the constant or inject it into the generated script bodies so one value defines the wait.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/install.go` at line 18, Resolve the unused claimWaitSeconds
constant by either removing it or using it when generating the script bodies’
BUTTONS_FLOW_CLAIM_WAIT fallback. Ensure the wait default is defined in one
place and remains 1.

Source: Linters/SAST tools

internal/drawer/entity.go (1)

160-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the field HeartbeatSeconds for consistency.

FlowManager uses HeartbeatSeconds for the same heartbeat_seconds JSON key. FlowRole.Heartbeat diverges from that convention. Rename it now, while no external code depends on the Go field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/drawer/entity.go` at line 160, Rename the FlowRole field from
Heartbeat to HeartbeatSeconds while preserving the `heartbeat_seconds` JSON tag,
and update any references to the field accordingly. Keep the existing type and
omitempty behavior unchanged.
internal/drawer/schema_embedded.json (1)

154-157: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

roles is generated as an untyped object in both schema artifacts. FlowDefinition.Roles is map[string]FlowRole in internal/drawer/entity.go, but the generator emits a bare "type": "object", so no role field is validated. Both files are produced from that one struct annotation.

  • internal/drawer/schema_embedded.json#L154-L157: after adding a FlowRole $def and wiring additionalProperties to it in the Go annotation, regenerate this embedded copy with go generate ./....
  • docs/schemas/drawer.schema.json#L154-L157: regenerate the canonical schema from the same run so the two artifacts stay identical.

As per coding guidelines: "The canonical JSON Schema lives at docs/schemas/drawer.schema.json and is generated from the Go struct via go generate ./...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/drawer/schema_embedded.json` around lines 154 - 157, Update the Go
schema annotation for FlowDefinition.Roles in internal/drawer/entity.go to
define FlowRole and set roles.additionalProperties to the FlowRole schema, so
each role field is validated. Regenerate both
internal/drawer/schema_embedded.json:154-157 and
docs/schemas/drawer.schema.json:154-157 with go generate ./...; both sites
require generated updates and must remain identical.

Source: Coding guidelines

cmd/serve.go (1)

427-432: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Compile before the 202 response so senders learn about failures.

PrepareForExecute runs inside the goroutine. The handler always answers 202 Accepted on Line 446, so a flow drawer with an invalid definition produces a success response and one stderr line. Compilation is deterministic and does not perform I/O, so it can run before the goroutine starts. A prep failure can then return 500 with the error code.

♻️ Proposed direction
+	execDrawer, prepErr := drawer.PrepareForExecute(d)
+	if prepErr != nil {
+		http.Error(w, `{"ok":false,"error":"drawer_compile_failed"}`, http.StatusInternalServerError)
+		return
+	}
+
 	h.wg.Add(1)
 	go func() {
 		defer h.wg.Done()
 		ctx, cancel := context.WithTimeout(h.pressCtx, time.Hour)
 		defer cancel()
 
 		exec := drawer.NewExecutor()
-		execDrawer, prepErr := drawer.PrepareForExecute(d)
-		if prepErr != nil {
-			fmt.Fprintf(os.Stderr, "[serve] drawer %s compile error: %v\n", d.Name, prepErr)
-			return
-		}
 		result, execErr := exec.Execute(ctx, execDrawer, map[string]any{"webhook": webhookInput})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/serve.go` around lines 427 - 432, Move the PrepareForExecute call for the
webhook flow out of the goroutine and execute it before the handler sends the
202 response. In the surrounding serve handler, return HTTP 500 with the
appropriate error code when preparation fails; only launch the goroutine and
call exec.Execute after successful preparation, while preserving the existing
execution-error handling.
internal/drawer/compile_flow_test.go (1)

70-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer errors.As for the error type check.

err.(*ServiceError) fails if CompileFlow later wraps the error with %w. errors.As keeps the test correct across that change.

♻️ Proposed change
-	se, ok := err.(*ServiceError)
-	if !ok || se.Code != "VALIDATION_ERROR" {
+	var se *ServiceError
+	if !errors.As(err, &se) || se.Code != "VALIDATION_ERROR" {
 		t.Fatalf("err = %#v", err)
 	}

Add "errors" to the import block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/drawer/compile_flow_test.go` around lines 70 - 73, Update the error
assertion in the CompileFlow test to use errors.As with a *ServiceError target,
preserving the existing validation-code check and failure message; add the
errors import required for this assertion.
internal/flowkit/claim_test.go (1)

28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Build the tasks path from the shared helper.

The test hard-codes home/flows/<board>/tasks. config.FlowBoardDir already owns that layout (see cmd/flow.go Line 314). If the layout changes, this test breaks silently rather than following the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/claim_test.go` at line 28, Update the task path construction
in the relevant test to use the shared config.FlowBoardDir helper instead of
manually joining home, “flows”, board, and “tasks”. Preserve the existing
tid-based JSON filename while delegating the directory layout to
config.FlowBoardDir.
test/integration/flow_drawer_test.go (1)

185-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the press assertion.

ExecuteResult always serializes a status field. The nested condition therefore passes for a failed run as well as a successful run, so the check adds nothing beyond the exit-code check at Line 182. The test name states that the flow compiles and runs, so assert the successful status directly.

🧪 Proposed change
-	if !strings.Contains(r.Stdout, `"ok"`) && !strings.Contains(r.Stdout, `"status": "ok"`) {
-		// ExecuteResult uses status field
-		if !strings.Contains(r.Stdout, `"status"`) {
-			t.Fatalf("unexpected press output: %s", r.Stdout)
-		}
-	}
+	if !strings.Contains(r.Stdout, `"status": "ok"`) {
+		t.Fatalf("expected a successful press result: %s", r.Stdout)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/flow_drawer_test.go` around lines 185 - 190, Update the
press-output assertion in the flow compilation/run test to require a successful
status value directly, rather than merely checking for the presence of the
"status" field. Remove the redundant nested condition and preserve the existing
failure message for outputs that do not indicate success.
internal/drawer/service.go (1)

194-204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider centralizing the provider allow-list.

SetFlowField hard-codes "local" and "github". flowkit.EnsureButtons (internal/flowkit/install.go:22-35) encodes the same set. A third provider requires edits in both places. Export a single flowkit.ValidProvider(name) helper and call it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/drawer/service.go` around lines 194 - 204, Centralize provider
validation by adding the exported flowkit.ValidProvider(name) helper with the
local and github allow-list, then update SetFlowField’s "provider" case to use
it instead of hard-coding those values. Preserve the existing type validation,
error behavior, and assignment for valid providers, and reuse the helper from
flowkit.EnsureButtons so the allow-list has one source of truth.
test/integration/flow_recovery_test.go (1)

89-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the stale claim was actually replaced.

The test verifies only that the status reached done. A provider that ignores claims entirely also passes. The test name states recovery through staleness, so also assert that flow.claimed_by no longer holds crashed-agent.

🧪 Proposed addition
 	if status != "done" {
 		t.Fatalf("expected status=done after reclaim press, got %#v task=%s", status, data)
 	}
+	if props != nil {
+		if holder, _ := props["flow.claimed_by"].(string); holder == "crashed-agent" {
+			t.Fatalf("stale claim was not released: task=%s", data)
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/flow_recovery_test.go` around lines 89 - 99, Extend the
assertions in the recovery test around the existing status validation to read
the task’s flow claim and verify that claimed_by is no longer "crashed-agent".
Keep the current status=done assertion, and fail the test with relevant task
data if the stale claim remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/flow_research_deck.go`:
- Around line 65-67: Update flowInit’s handling of installResearchDeckBoard
failures to return handleDrawerError(err) instead of the raw error, ensuring
template-install failures use the required JSON envelope with --json.

In `@cmd/flow.go`:
- Around line 106-117: Declare a separate flowReason string variable and update
flowRejectCmd to read and pass flowReason as the reject reason instead of
flowFilter. Keep flowFilter reserved for flowTaskCmd’s --filter flag and
preserve the existing omission behavior when no reason is provided.
- Around line 479-504: Replace the direct drawer.json read/modify/write hack in
the trigger-clearing flow with an exported drawer service method such as
ClearTriggers(name). Implement ClearTriggers using the service’s normal load,
validation, mutation, updated_at refresh, and save path, then call it from this
flow and remove the dead d.Triggers/d.UpdatedAt assignments and filesystem
manipulation.
- Around line 321-350: Update the flow generation logic around the schedule
metadata and GitHub workflow so the declared poll interval matches the
provider’s actual schedule. Introduce a shared provider-based interval value—300
seconds for GitHub and 60 seconds otherwise—and derive both schedule.json’s
every_seconds field and the GitHub cron expression from it, preserving the
existing workflow behavior.
- Around line 466-469: Replace the raw JSON printing loop over filtered runs
with a human-readable formatted summary using the field names defined by
drawer.Run. Keep machine-readable output exclusively in the existing jsonOutput
branch via config.WriteJSON. Update the board-name validation error to use the
coded error mechanism with the uppercase code MISSING_ARG.

In `@internal/drawer/compile_flow.go`:
- Around line 81-90: The compiled drawer in the compilation flow must use the v2
schema version to match its DrawerKindAction value. Update the SchemaVersion
field in the Drawer construction to use the SchemaVersion constant or
d.SchemaVersion, while leaving the remaining fields unchanged.
- Around line 66-74: Update the parallelism calculation in the compile flow to
enforce Limits.MaxActiveTasks as an upper bound: when processing each stage,
clamp its Concurrency to the configured max before determining the final
parallelism. Preserve the default and unlimited behavior when no positive
max_active_tasks is configured.

In `@internal/flowkit/install.go`:
- Around line 97-122: The installOne replacement flow must preserve the existing
button when svc.Create fails. Update installOne to retain the current definition
and restore it after a failed replacement, or create under a temporary name and
swap only after success; also handle and propagate the svc.Remove error instead
of discarding it.
- Line 71: Update the flow-github-task-claim registration in install.go to
include the same QueueConfig used by flow-github-claim and the corresponding
local task pair, keyed on task_id. Keep githubClaimCode and the existing
arguments unchanged so manual claims use identical serialized queue behavior.

In `@internal/flowkit/scripts_github.go`:
- Around line 170-179: Update githubTaskListCode, githubTaskReadCode,
githubTaskUpdateCode, githubTaskRmCode, githubTaskCommentCode,
githubApproveCode, and githubRejectCode to validate that repo is set before
invoking subprocess.run. Reuse the same guard and structured JSON error response
already used by githubTaskAddCode, returning it immediately when both repository
environment variables are absent.
- Around line 254-275: Update githubApproveCode, githubRejectCode, and
githubApplyCode to capture each gh subprocess result, inspect returncode, and
include captured stderr in a failure response with ok: false. Only emit the
existing ok: true response after every required GitHub operation succeeds,
preserving the scripts’ current success payloads.
- Line 139: The claim-release logic around githubClaimCode must remove the same
login that was assigned, rather than always using gh’s authenticated `@me` alias.
Pass the resolved holder through to the subprocess call or recompute it using
the exact BUTTONS_FLOW_HOLDER, GITHUB_ACTOR, then buttons-agent precedence, and
use that value for --remove-assignee.
- Around line 79-99: Update the claim checks in the issue-view flow to compare
the full assignee login set rather than only assignees[0]. In both the initial
existing-claim check and the post-edit verification, ensure the claim succeeds
only when holder is the sole assignee; treat any additional assignee as already
claimed or lost_race respectively.

In `@internal/flowkit/scripts_local.go`:
- Line 90: Replace every direct task JSON write in the identified script
locations with an atomic same-directory temporary-file workflow: write the
complete content, set the temporary file mode to 0o600, then use os.replace to
overwrite the target. Apply this consistently to all task-writing paths,
including the writes near lines 90, 193, 204, 216, 270, 347, 381, 402, and 424,
while preserving the existing JSON formatting and trailing newline.
- Line 335: Add the same task-file existence check used by localTaskReadCode to
localTaskUpdateCode, localTaskCommentCode, localApproveCode, and localRejectCode
before reading or parsing the file; return the structured {"ok": false, "error":
"not_found"} response for missing task IDs instead of allowing FileNotFoundError
tracebacks.
- Around line 200-206: Update the gated advance logic in the branch handling v
== "advance" to require flow.approved_stage to equal from_stage instead of
checking the permanent approved flag, and consume that stage-specific approval
after it is used. Update localApproveCode to store the approved stage in
flow.approved_stage, preserving pending-approval behavior for mismatched or
absent approvals.
- Line 215: Update the comment timestamp expression in the affected script to
use the existing datetime and timezone imports instead of
__import__("datetime").datetime.utcnow(), avoiding duplicate imports. Also
change the file mode from 0644 to 0700.

In `@test/integration/flow_runtime_test.go`:
- Around line 111-124: Replace loose stdout substring matching with JSON-based
assertions in test/integration/flow_runtime_test.go#L111-L124 and
test/integration/flow_drawer_test.go#L185-L190. In the flow runtime
result-counting logic, fail on unparseable payloads and count only items whose
status field equals the requested status; in the flow drawer test, decode the
response and directly assert that the status field is "ok".

---

Nitpick comments:
In `@cmd/flow_research_deck.go`:
- Around line 37-68: Replace the unordered sets map and range loop in the flow
setup with an ordered slice of path/value pairs, preserving the current field
order and values so SetFlowField applies changes deterministically. If the
service exposes a batch flow-field update API, use it to persist the complete
set in one operation; otherwise retain sequential SetFlowField calls over the
ordered slice and existing error wrapping.

In `@cmd/serve.go`:
- Around line 427-432: Move the PrepareForExecute call for the webhook flow out
of the goroutine and execute it before the handler sends the 202 response. In
the surrounding serve handler, return HTTP 500 with the appropriate error code
when preparation fails; only launch the goroutine and call exec.Execute after
successful preparation, while preserving the existing execution-error handling.

In `@internal/drawer/compile_flow_test.go`:
- Around line 70-73: Update the error assertion in the CompileFlow test to use
errors.As with a *ServiceError target, preserving the existing validation-code
check and failure message; add the errors import required for this assertion.

In `@internal/drawer/entity.go`:
- Line 160: Rename the FlowRole field from Heartbeat to HeartbeatSeconds while
preserving the `heartbeat_seconds` JSON tag, and update any references to the
field accordingly. Keep the existing type and omitempty behavior unchanged.

In `@internal/drawer/schema_embedded.json`:
- Around line 154-157: Update the Go schema annotation for FlowDefinition.Roles
in internal/drawer/entity.go to define FlowRole and set
roles.additionalProperties to the FlowRole schema, so each role field is
validated. Regenerate both internal/drawer/schema_embedded.json:154-157 and
docs/schemas/drawer.schema.json:154-157 with go generate ./...; both sites
require generated updates and must remain identical.

In `@internal/drawer/service.go`:
- Around line 194-204: Centralize provider validation by adding the exported
flowkit.ValidProvider(name) helper with the local and github allow-list, then
update SetFlowField’s "provider" case to use it instead of hard-coding those
values. Preserve the existing type validation, error behavior, and assignment
for valid providers, and reuse the helper from flowkit.EnsureButtons so the
allow-list has one source of truth.

In `@internal/flowkit/claim_test.go`:
- Line 28: Update the task path construction in the relevant test to use the
shared config.FlowBoardDir helper instead of manually joining home, “flows”,
board, and “tasks”. Preserve the existing tid-based JSON filename while
delegating the directory layout to config.FlowBoardDir.

In `@internal/flowkit/install.go`:
- Line 18: Resolve the unused claimWaitSeconds constant by either removing it or
using it when generating the script bodies’ BUTTONS_FLOW_CLAIM_WAIT fallback.
Ensure the wait default is defined in one place and remains 1.

In `@internal/store/builtin.go`:
- Around line 148-156: Update PreferBuiltin.Fetch so `@buttonsflow/` names first
use BuiltinSource, then fall back to s.Primary.Fetch when the builtin lookup
reports the package is unavailable. Preserve the existing not-found error when
Primary is nil, and continue routing non-@buttonsflow/ names directly through
Primary.
- Around line 136-146: Update PreferBuiltin.Index to handle the error returned
by s.Primary.Index instead of silently returning the builtin catalog: preserve
the fallback only if intentional, document it at the error branch, and log the
discarded error using the suggested os-based mechanism; otherwise propagate the
error. Ensure the nilerr warning is resolved.
- Around line 60-128: The research-deck flow is defined independently in two
locations and has drifted. In internal/store/builtin.go:60-128, move the literal
from researchDeckDrawer into a shared exported constructor such as
drawer.ResearchDeckDefinition, and have researchDeckDrawer return that
definition. In cmd/flow_research_deck.go:22-63, remove the duplicated stage loop
and sets map, then persist the shared definition directly so both paths produce
identical boards.

In `@test/integration/flow_drawer_test.go`:
- Around line 185-190: Update the press-output assertion in the flow
compilation/run test to require a successful status value directly, rather than
merely checking for the presence of the "status" field. Remove the redundant
nested condition and preserve the existing failure message for outputs that do
not indicate success.

In `@test/integration/flow_recovery_test.go`:
- Around line 89-99: Extend the assertions in the recovery test around the
existing status validation to read the task’s flow claim and verify that
claimed_by is no longer "crashed-agent". Keep the current status=done assertion,
and fail the test with relevant task data if the stale claim remains.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dd01139-7ff5-4e1c-ab82-cc4daa9f12ab

📥 Commits

Reviewing files that changed from the base of the PR and between c722a62 and ac38f44.

📒 Files selected for processing (22)
  • cmd/drawer.go
  • cmd/flow.go
  • cmd/flow_research_deck.go
  • cmd/install.go
  • cmd/root.go
  • cmd/serve.go
  • docs/schemas/drawer.schema.json
  • internal/config/paths.go
  • internal/drawer/compile_flow.go
  • internal/drawer/compile_flow_test.go
  • internal/drawer/entity.go
  • internal/drawer/schema_embedded.json
  • internal/drawer/service.go
  • internal/flowkit/claim_test.go
  • internal/flowkit/install.go
  • internal/flowkit/scripts_github.go
  • internal/flowkit/scripts_local.go
  • internal/store/builtin.go
  • test/integration/flow_drawer_test.go
  • test/integration/flow_recovery_test.go
  • test/integration/flow_runtime_test.go
  • test/integration/helpers_test.go

Comment thread cmd/flow_research_deck.go Outdated
Comment thread cmd/flow.go
Comment thread cmd/flow.go
Comment thread cmd/flow.go
Comment thread cmd/flow.go Outdated
Comment thread internal/flowkit/scripts_local.go Outdated
Comment thread internal/flowkit/scripts_local.go Outdated
Comment thread internal/flowkit/scripts_local.go Outdated
props.pop("flow.pending_approval", None)
if verdict.get("summary"):
comments = task.setdefault("comments", [])
comments.append({"body": verdict["summary"], "at": __import__("datetime").datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is datetime.utcnow deprecated in Python 3.12?

💡 Result:

Yes, datetime.utcnow is deprecated in Python 3.12 [1][2]. The deprecation was introduced because datetime.utcnow returns a naive datetime object (lacking timezone information), which can lead to errors when handled by methods that assume naive objects represent local time [3][4][2]. It is important to note that datetime.now(timezone.utc) is not a direct drop-in replacement, as it returns a timezone-aware object [3][5]. If your existing code requires a naive datetime object for compatibility, you should use datetime.now(timezone.utc).replace(tzinfo=None) instead [3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
file="internal/flowkit/scripts_local.go"
printf '%s\n' '--- permissions ---'
stat -c '%a %n' "$file"
printf '%s\n' '--- target context ---'
sed -n '1,35p;195,225p' "$file"
printf '%s\n' '--- datetime usage in file ---'
rg -n -C 2 'datetime|timezone|__import__' "$file"
printf '%s\n' '--- related file permissions ---'
find internal -type f -name '*.go' -o -type f -name '*spec*.json' -o -type f -name '*history*.json' | while IFS= read -r p; do
  stat -c '%a %n' "$p"
done | head -100

Repository: autonoco/buttons

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -eu
file="internal/flowkit/scripts_local.go"
printf '%s\n' '--- permissions ---'
python3 - "$file" <<'PY'
import os, sys
path = sys.argv[1]
print(f"{oct(os.stat(path).st_mode & 0o777)[2:]} {path}")
PY
printf '%s\n' '--- target context ---'
sed -n '1,35p;195,225p' "$file"
printf '%s\n' '--- datetime usage in file ---'
grep -n -C 2 -E 'datetime|timezone|__import__' "$file" || true
printf '%s\n' '--- relevant internal file permissions ---'
find internal -type f \( -name '*.go' -o -name '*spec*.json' -o -name '*history*.json' \) -print | while IFS= read -r p; do
  python3 - "$p" <<'PY'
import os, sys
path = sys.argv[1]
print(f"{oct(os.stat(path).st_mode & 0o777)[2:]} {path}")
PY
done | head -100

Repository: autonoco/buttons

Length of output: 8863


🏁 Script executed:

#!/bin/bash
set -eu
file="internal/flowkit/scripts_local.go"
python3 - "$file" <<'PY'
import os, re, sys
from datetime import datetime, timezone

path = sys.argv[1]
text = open(path, encoding="utf-8").read()
mode = os.stat(path).st_mode & 0o777
target = next(line.strip() for line in text.splitlines() if '"at": __import__("datetime")' in line)
replacement = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

print("filesystem_mode:", oct(mode))
print("target_uses_utcnow:", "datetime.utcnow()" in target)
print("target_uses_inline_import:", '__import__("datetime")' in target)
print("replacement_has_utc_suffix:", replacement.endswith("Z"))
print("replacement_contains_offset:", "+00:00" in replacement)
PY
printf '%s\n' '--- tracked mode ---'
git ls-files -s -- "$file"
printf '%s\n' '--- all tracked internal Go modes ---'
git ls-files -s -- 'internal/**/*.go' | awk '{print $1, $4}' | sort | uniq -c

Repository: autonoco/buttons

Length of output: 6345


Replace datetime.utcnow() and set the file mode to 0700.

Use the existing datetime and timezone imports. Do not add duplicate imports. internal/flowkit/scripts_local.go currently has mode 0644, but internal Go code files must use mode 0700.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/scripts_local.go` at line 215, Update the comment timestamp
expression in the affected script to use the existing datetime and timezone
imports instead of __import__("datetime").datetime.utcnow(), avoiding duplicate
imports. Also change the file mode from 0644 to 0700.

Comment thread internal/flowkit/scripts_local.go
Comment thread test/integration/flow_runtime_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
internal/drawer/service.go (1)

195-202: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Store the normalized provider value.

Lines 195-202 accept " github " because flowkit.ValidProvider trims whitespace. The method then stores " github " in d.Flow.Provider. Later provider lookup will not match github.

Trim v before validation and assignment.

Proposed fix
-	} else if !flowkit.ValidProvider(v) {
+	} else if !flowkit.ValidProvider(strings.TrimSpace(v)) {
 		setErr = fmt.Errorf("must be local or github")
 	} else {
-		d.Flow.Provider = v
+		d.Flow.Provider = strings.TrimSpace(v)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/drawer/service.go` around lines 195 - 202, Update the "provider"
case to trim the string value into the normalized provider before calling
flowkit.ValidProvider, then assign that normalized value to d.Flow.Provider so
surrounding whitespace is not stored.
internal/flowkit/scripts_github.go (3)

192-204: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return an error when the GitHub list command fails.

Lines 192-193 convert a gh issue list failure into []. The script then reports count: 0, so callers treat an unavailable repository or authorization failure as an empty board.

Exit with an error response when proc.returncode != 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/scripts_github.go` around lines 192 - 204, The issue-list
flow currently treats a failed gh issue list command as an empty result. Update
the subprocess handling before parsing issues so proc.returncode != 0 produces
an error response and exits, while preserving the existing JSON parsing and item
filtering for successful commands.

242-245: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replace the existing status label.

A status patch only adds status:<new>. It does not remove the prior status:<old> label. githubTaskListCode derives status by iterating labels, so a task with multiple status labels has ambiguous state.

Read and remove existing status: labels before adding the requested status label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/scripts_github.go` around lines 242 - 245, Update the status
handling in githubTaskListCode so status patches first read the issue’s existing
labels and remove every label with the status: prefix, then add the requested
status:new label. Preserve the current status extraction and patch response
behavior.

239-245: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Check GitHub write results before emitting success. Both operations discard failed gh mutations and return an "ok": true response.

  • internal/flowkit/scripts_github.go#L239-L245: capture each gh issue edit result and return an error when any requested update fails.
  • internal/flowkit/scripts_github.go#L257-L258: capture the gh issue close result and return an error when closure fails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/flowkit/scripts_github.go` around lines 239 - 245, Check the results
of every requested GitHub mutation before reporting success: in
internal/flowkit/scripts_github.go lines 239-245, capture each gh issue edit
invocation and return an error if any title, body, or status update fails; in
lines 257-258, likewise capture gh issue close and return an error on failure.
Only emit the existing ok response after all requested operations succeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/flow_research_deck.go`:
- Around line 20-29: Make the research-deck installation atomic by updating the
flow around CreateWithKind and Save in the research-deck installation function:
if saving the canonical ResearchDeckDrawer fails, remove the newly created
scaffold before returning the error, or use an existing service operation that
persists the canonical drawer transactionally. Preserve the existing
duplicate-installation check and successful scaffold creation behavior.

---

Outside diff comments:
In `@internal/drawer/service.go`:
- Around line 195-202: Update the "provider" case to trim the string value into
the normalized provider before calling flowkit.ValidProvider, then assign that
normalized value to d.Flow.Provider so surrounding whitespace is not stored.

In `@internal/flowkit/scripts_github.go`:
- Around line 192-204: The issue-list flow currently treats a failed gh issue
list command as an empty result. Update the subprocess handling before parsing
issues so proc.returncode != 0 produces an error response and exits, while
preserving the existing JSON parsing and item filtering for successful commands.
- Around line 242-245: Update the status handling in githubTaskListCode so
status patches first read the issue’s existing labels and remove every label
with the status: prefix, then add the requested status:new label. Preserve the
current status extraction and patch response behavior.
- Around line 239-245: Check the results of every requested GitHub mutation
before reporting success: in internal/flowkit/scripts_github.go lines 239-245,
capture each gh issue edit invocation and return an error if any title, body, or
status update fails; in lines 257-258, likewise capture gh issue close and
return an error on failure. Only emit the existing ok response after all
requested operations succeed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e55c143-2e6d-4978-b081-73e520490f2d

📥 Commits

Reviewing files that changed from the base of the PR and between ac38f44 and fe4ad0c.

📒 Files selected for processing (18)
  • cmd/flow.go
  • cmd/flow_research_deck.go
  • cmd/serve.go
  • docs/schemas/drawer.schema.json
  • internal/drawer/compile_flow.go
  • internal/drawer/compile_flow_test.go
  • internal/drawer/entity.go
  • internal/drawer/schema_embedded.json
  • internal/drawer/service.go
  • internal/flowkit/claim_test.go
  • internal/flowkit/install.go
  • internal/flowkit/scripts_github.go
  • internal/flowkit/scripts_local.go
  • internal/store/builtin.go
  • internal/tools/schemagen/main.go
  • test/integration/flow_drawer_test.go
  • test/integration/flow_recovery_test.go
  • test/integration/flow_runtime_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/drawer/compile_flow_test.go
  • test/integration/flow_runtime_test.go
  • internal/drawer/compile_flow.go
  • internal/drawer/entity.go
  • internal/flowkit/install.go
  • internal/flowkit/scripts_local.go
  • test/integration/flow_drawer_test.go

Comment thread cmd/flow_research_deck.go
Comment on lines +20 to +29
// Create the scaffold (directory + AGENTS.md + pressed/).
if _, err := svc.CreateWithKind("research-deck", "Research a topic and create an open-slide deck (https://open-slide.dev/).", nil, drawer.DrawerKindFlow); err != nil {
return err
}
// Overwrite with the canonical definition.
d := drawer.ResearchDeckDrawer(provider)
now := time.Now().UTC()
d.CreatedAt = now
d.UpdatedAt = now
return svc.Save(d)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make research-deck installation atomic.

CreateWithKind persists an empty flow scaffold before svc.Save(d) writes the canonical stages. If line 29 fails, the incomplete drawer remains on disk. A later call returns at line 17 because svc.Get("research-deck") succeeds, so it never repairs the drawer.

Remove the newly created scaffold when the canonical save fails, or add a service operation that creates the canonical drawer in one transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/flow_research_deck.go` around lines 20 - 29, Make the research-deck
installation atomic by updating the flow around CreateWithKind and Save in the
research-deck installation function: if saving the canonical ResearchDeckDrawer
fails, remove the newly created scaffold before returning the error, or use an
existing service operation that persists the canonical drawer transactionally.
Preserve the existing duplicate-installation check and successful scaffold
creation behavior.

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.

1 participant