Skip to content

refactor: improve packages - #70

Merged
pedronauck merged 18 commits into
mainfrom
pn/refac
Apr 7, 2026
Merged

pedronauck merged 18 commits into
mainfrom
pn/refac

Conversation

@pedronauck

@pedronauck pedronauck commented Apr 7, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Adds an upgrade command, asynchronous update checks, and on-screen update notifications.
    • Introduces new CLI workflows: fetch-reviews, fix-reviews, start, exec, migrate, sync, archive.
  • Improvements

    • Better tool-name detection and more robust content-block serialization/handling.
    • Stricter runtime/workspace validation and safer run/job artifact naming.
    • Refined migration, sync, and archive behaviors; styled update message display.
  • Tests

    • Expanded unit and integration tests for parsing, conversion, migration targets, and update flow.

@pedronauck pedronauck self-assigned this Apr 7, 2026
@coderabbitai

coderabbitai Bot commented Apr 7, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Reorganizes CLI into a command-state layer with per-command constructors, adds async update-check + upgrade command, splits and reshapes model/agent/migration/reviews/contentconv packages, refactors run→exec APIs, and introduces many test updates across the codebase.

Changes

Cohort / File(s) Summary
CLI: entry & update
cmd/compozy/main.go, cmd/compozy/main_test.go
Introduce run() wrapper, async update check with cancellable context and bounded wait, update notification rendering, and tests for update-check behaviors.
CLI: commands & wiring
internal/cli/...
internal/cli/commands.go, internal/cli/commands_simple.go, internal/cli/dispatch_adapters.go, internal/cli/upgrade.go, internal/cli/root.go, internal/cli/root_test.go
Move subcommand implementations into focused files, add commandState wiring and adapter factories, reduce root.go surface, add upgrade command and associated tests.
CLI: state, run & input handling
internal/cli/state.go, internal/cli/run.go, internal/cli/form.go, internal/cli/workspace_config.go, internal/cli/workspace_config_test.go
Add centralized commandState, unified flag-to-config/apply helpers, prompt-source resolution, persisted exec support, preflight checks, and many related tests.
Core kernel: commands, adapters, handlers
internal/core/kernel/...
internal/core/kernel/commands/*, internal/core/kernel/core_adapters.go, internal/core/kernel/core_adapters_test.go, internal/core/kernel/handlers.go
Replace RuntimeFields with explicit model.RuntimeConfig, centralize dispatch adapter helper, add delegating handlers, and move command/result types to model.*.
Model: split & new models
internal/core/model/* (new), internal/core/model/model.go (deleted)
Decompose monolithic model into runtime_config, artifacts, workspace_paths, workflow_ops, preparation, task/review models; add sanitizers and ApplyDefaults; remove old combined model file.
Content-block & conversion
internal/contentblock/engine.go, internal/contentblock/engine_test.go, internal/core/contentconv/contentconv.go, internal/core/contentconv/contentconv_test.go
Add canonical envelope helpers, payload validation, generic envelope (un)marshal, and bidirectional internal↔public content conversions with tests.
Agent runtime registry & launch validation
internal/core/agent/registry_specs.go, internal/core/agent/registry_launch.go, internal/core/agent/registry_validate.go, internal/core/agent/registry.go (deleted)
Split registry into specs/launch/validation, add AvailabilityError and probe/verify logic, provide catalog helpers, and remove legacy monolithic registry.
Agent ACP conversion & tool-name heuristics
internal/core/agent/acp_convert.go, internal/core/agent/tool_call_name.go, internal/core/agent/tool_call_input.go, internal/core/agent/session.go (partial removals)
Introduce ACP→model conversion layer, add tool-call name normalization heuristics, and tighten tool input marshaling/error handling.
Reviews & tasks parsing
internal/core/reviews/*, internal/core/prompt/*
Extract review parsing (modern + legacy) and task parsing into dedicated modules, update callers, change error wrapping, and add tests.
Migration
internal/core/migration/*, internal/core/migrate.go (delegate)
Move migration implementation (walk, v1→v2 conversion, pending writes) into migration package and make core.migrate delegate to it.
Run → exec refactor
internal/core/run/exec/*, internal/core/run/command_io.go (deleted)
Rename run→exec package, add aliases/helpers, export/use new struct field casing, and remove previous session/command_io lifecycle code.
Plan & prepare
internal/core/plan/*
Replace ad-hoc task reading with tasks.ReadTaskEntries, use slog for logs, centralize regexes, and update prepare/journal call sites.
Other core changes & tests
assorted internal/core/* and tests
Multiple files updated to use model.* types, refactored helpers, moved packages (preputil→plan), and widespread test adjustments to match API changes.
Deps & misc
go.mod, .gitignore
Add several indirect dependencies and minor .gitignore tweak.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pn/refac

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (1)
internal/core/plan/journal.go (1)

19-21: ⚠️ Potential issue | 🟠 Major

Replace context.Background() fallback with context.TODO().

Line 20 uses context.Background() in production code. Per coding guidelines, avoid context.Background() outside main and focused tests. Use context.TODO() instead to signal incomplete context setup.

Suggested fix
 	closeCtx := ctx
 	if closeCtx == nil {
-		closeCtx = context.Background()
+		closeCtx = context.TODO()
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/plan/journal.go` around lines 19 - 21, Replace the use of
context.Background() fallback with context.TODO() in the journal close path:
where the variable closeCtx is being defaulted (the block containing "if
closeCtx == nil { closeCtx = context.Background() }"), change it to use
context.TODO() so the code signals an incomplete context setup; update the
assignment referencing closeCtx in the journal.go function/method that performs
the close logic accordingly.
🧹 Nitpick comments (11)
internal/core/agent/tool_call_name.go (4)

134-143: Remove duplicate title trimming logic.

The ToolKindOther case and the code after the switch both check for a trimmed title. The ToolKindOther case can be removed since the post-switch logic handles it.

♻️ Proposed fix
 	case acp.ToolKindFetch:
 		if looksLikeWebSearchInput(input) {
 			return toolNameWebSearch
 		}
 		return toolNameWebFetch
-	case acp.ToolKindOther:
-		if trimmed := strings.TrimSpace(title); trimmed != "" {
-			return trimmed
-		}
 	}
 	if trimmed := strings.TrimSpace(title); trimmed != "" {
 		return trimmed
 	}
 	return ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 134 - 143, Remove the
duplicate trimming logic by deleting the acp.ToolKindOther case block inside the
switch in the GetToolCallName (or equivalent) function so that the post-switch
fallback that trims and returns title handles that kind; ensure you leave the
final post-switch check that uses strings.TrimSpace(title) and returns trimmed
or "" so other cases still short-circuit as intended and no duplicate trimmed
variable declarations remain.

91-93: Consider adding parentheses for clarity.

The condition relies on Go's operator precedence, but explicit parentheses would improve readability.

✨ Suggested improvement
-		if token == "update_todo" || input != nil && input["todos"] != nil {
+		if token == "update_todo" || (input != nil && input["todos"] != nil) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 91 - 93, The conditional
combining token and input checks is correct but unclear; update the if in
tool_call_name.go to use explicit parentheses around the alternatives—e.g.,
parenthesize (token == "update_todo") and (input != nil && input["todos"] !=
nil) when returning toolNameTodoWrite—so the intent is immediately obvious when
reading the token, input, and toolNameTodoWrite logic.

227-236: Shallow clone may share nested references.

This performs a shallow copy, so nested maps or slices within src will be shared between the original and clone. Verify this is the intended behavior for your use case, or consider a deep clone if mutation isolation is required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 227 - 236, The cloneMap
function currently does a shallow copy and will share nested map/slice
references; either document this behavior or change cloneMap to perform a deep
copy by recursively handling nested types: when iterating in cloneMap, if value
is map[string]any call cloneMap(value) recursively, if value is []any make a new
slice and deep-clone each element (recursing for nested maps/slices), and for
other types copy as-is; update function name/signature or add helper
deepCloneValue/deepCloneMap helpers as needed to keep code clear.

28-60: Consider using constants consistently for tool names.

The map uses a mix of constant references (toolNameFind, toolNameGrep) and string literals ("Edit", "Write", "Glob"). For maintainability, consider defining constants for all tool names used here and using them consistently throughout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 28 - 60, The map
commonToolTitleAliases mixes string literals and existing constants (e.g.,
toolNameFind); define constants for the repeated tool title strings (e.g., Edit,
Write, Glob, Grep, Read, Task, etc.), replace the string literals in
commonToolTitleAliases with those new constants, and update any other places
that use the same literal values to use the constants for consistency; keep
existing constant names like toolNameFind unchanged and ensure the new constants
are declared near other toolName* constants for discoverability.
internal/core/agent/acp_convert.go (1)

436-449: Consider returning the error instead of silently returning nil.

The marshaling error is silently swallowed, which could mask issues. Per coding guidelines, errors should be handled explicitly. Consider returning the error or logging it.

♻️ Proposed fix
-func marshalRawJSON(value any) json.RawMessage {
+func marshalRawJSON(value any) (json.RawMessage, error) {
 	if value == nil {
-		return nil
+		return nil, nil
 	}
 	if raw, ok := value.(json.RawMessage); ok {
-		return append(json.RawMessage(nil), raw...)
+		return append(json.RawMessage(nil), raw...), nil
 	}
 
 	payload, err := json.Marshal(value)
 	if err != nil {
-		return nil
+		return nil, fmt.Errorf("marshal raw JSON: %w", err)
 	}
-	return payload
+	return payload, nil
 }

As per coding guidelines: "Never ignore errors with _ in Go — every error must be handled or have a written justification."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/acp_convert.go` around lines 436 - 449, marshalRawJSON
currently swallows json.Marshal errors and returns nil; change its signature to
return (json.RawMessage, error) and propagate the marshal error instead of
returning nil, updating callers to handle the error (or alternatively log the
error where appropriate); specifically modify function marshalRawJSON to return
both payload and error and update any call sites that use marshalRawJSON to
check and handle the returned error.
internal/core/model/constants.go (1)

15-22: Keep provider defaults out of package constants.

Default*Model and DefaultActivityTimeout are runtime configuration, so every provider rename, deprecation, or policy change now becomes a code change. Prefer loading these defaults from TOML or injecting them via options instead of freezing them in internal/core/model.

As per coding guidelines, "Never hardcode configuration in Go code — use TOML config or functional options."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/model/constants.go` around lines 15 - 22, The constants
DefaultCodexModel, DefaultClaudeModel, DefaultCursorModel, DefaultOpenCodeModel,
DefaultPiModel, DefaultGeminiModel, DefaultCopilotModel and
DefaultActivityTimeout are hardcoded runtime configuration and should be moved
out of package constants; instead remove these values from
internal/core/model/constants.go and load them from a TOML config or provide
them via functional options/constructor parameters (e.g., pass defaults into the
model/provider constructors or a Config struct) so provider renames/deprecations
and timeouts can be changed without code edits; update any usages of these
symbols to read from the injected Config or options and add sensible fallback
logic where the config is missing.
internal/cli/kernel_dispatch_test.go (1)

248-249: Remove the unnecessary loop variable capture pattern since the project targets Go 1.26.1.

The tt := tt pattern was required before Go 1.22 to capture loop variables for closures, but is redundant in Go 1.22+ due to improved loop variable scoping. Delete line 249 to simplify the code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/kernel_dispatch_test.go` around lines 248 - 249, Remove the
redundant per-iteration capture assignment by deleting the inner "tt := tt" in
the test loop that iterates over "tests" (the loop variable "tt" is already
correctly scoped in Go 1.26.1), so simply iterate with "for _, tt := range tests
{ ... }" and update any closures to reference "tt" directly without the extra
reassignment.
cmd/compozy/main.go (1)

61-66: Channel read blocks until closed - verify this is intentional.

waitForUpdateResult blocks on <-result, which will wait until either a value is sent or the channel is closed. Since startUpdateCheck always closes the channel in its defer, this is safe. However, if CheckForUpdate takes a long time, the CLI will block after command execution completes.

Consider adding a timeout or making this non-blocking if rapid CLI exit is important:

♻️ Optional: Add timeout to avoid blocking on slow update checks
 func waitForUpdateResult(result <-chan *update.ReleaseInfo) *update.ReleaseInfo {
 	if result == nil {
 		return nil
 	}
-	return <-result
+	select {
+	case release := <-result:
+		return release
+	case <-time.After(500 * time.Millisecond):
+		return nil
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/compozy/main.go` around lines 61 - 66, waitForUpdateResult currently
blocks on <-result and can hang the CLI if CheckForUpdate is slow; change
waitForUpdateResult to use a select that waits on the result channel and a
timeout (e.g., time.After with a short duration) and return nil on timeout so
the CLI can exit promptly; reference the result channel returned by
startUpdateCheck/CheckForUpdate and ensure you import time and handle the nil
result case as before.
internal/core/kernel/deps_test.go (1)

580-581: Consider removing redundant loop variable capture.

In Go 1.22+, the loop variable semantics changed, making tt := tt unnecessary. However, this is a minor cleanup that can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/kernel/deps_test.go` around lines 580 - 581, Remove the
redundant loop-variable shadowing line (`tt := tt`) inside the for loop over
`tests`; simply iterate `for _, tt := range tests {` and use `tt` directly (or
pass `tt` into the subtest closure via `t.Run(tt.name, func(t *testing.T) { tc
:= tt; ... })` if you prefer explicit capture), so eliminate the unnecessary `tt
:= tt` statement in the test loop in deps_test.go.
internal/cli/workspace_config_test.go (1)

234-308: Use table-driven t.Run("Should ...") subtests for the new coercion helper coverage.

These two tests exercise several independent type/parsing branches in one body, so the first failure hides which contract actually regressed. A small table of Should ... cases would make the failures more precise and keep future coercion cases cheap to add.

As per coding guidelines, "Use table-driven tests with subtests (t.Run) as the default test pattern" and "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/workspace_config_test.go` around lines 234 - 308, The two
monolithic tests TestApplyConfigHandlesSupportedTypes and
TestApplyInputHandlesSupportedTypes should be refactored into table-driven
subtests using t.Run("Should ...") so each coercion/parsing branch is exercised
and reported independently; for each test create a slice of cases (each with a
name like "Should apply string config", target flag/key, input value, helper
function reference such as applyConfig/applyInput, and expected result) and
iterate calling t.Run(case.name, func(t *testing.T){ t.Parallel(); ... assert
expectations }), keeping use of helpers passThroughInput, parseIntInput,
parseStringSliceInput and the clone function for slices, and preserve current
assertions but split them into per-case checks so failures identify the exact
contract that regressed.
internal/core/kernel/core_adapters_test.go (1)

98-221: Consolidate these adapter cases into one table-driven Should ... test.

The four new tests all repeat the same dispatcher setup, global stub swap, and result-shape assertions. Folding them into a single table with t.Run("Should ...") cases will remove boilerplate and make future adapter additions much cheaper.

As per coding guidelines, "Use table-driven tests with subtests (t.Run) as the default pattern for Go tests" and "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/kernel/core_adapters_test.go` around lines 98 - 221, The four
near-duplicate tests (TestDispatchFetchReviewsAdapterBuildsReviewsFetchCommand,
TestDispatchMigrateAdapterBuildsWorkspaceMigrateCommand,
TestDispatchSyncAdapterBuildsWorkflowSyncCommand,
TestDispatchArchiveAdapterBuildsWorkflowArchiveCommand) should be merged into a
single table-driven test that iterates cases and uses t.Run("Should ...")
subtests; create a cases slice where each entry specifies the adapter function
to call (dispatchFetchReviewsAdapter, dispatchMigrateAdapter,
dispatchSyncAdapter, dispatchArchiveAdapter), the input config, the expected
result assertions, and the capture handler type/result to register, then inside
the loop set up NewDispatcher(), Register(dispatcher, handler), stub
coreAdapterDispatcherFn to return the dispatcher, call the adapter function, and
assert result shape and handler.got fields; keep using t.Cleanup to restore
coreAdapterDispatcherFn and preserve the same assertions per-case but eliminated
boilerplate by centralizing common setup and teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cli/run.go`:
- Around line 95-103: Replace the ignored return values from the fmt.Fprintf
call that writes the summary (the call using cmd.OutOrStdout() with
result.Total/result.Provider/result.PR/result.ReviewsDir/result.Round) with
proper error handling: capture the returned n, err := fmt.Fprintf(...), check if
err != nil and return or wrap that error (or log and return) so the function
does not silently succeed when stdout is closed or the write fails.

In `@internal/cli/state.go`:
- Around line 379-423: The logic currently omits non-empty stdin from
sourceCount, allowing two prompt sources; fix by calling
readPromptFromCommandInput(cmd.InOrStdin()) before computing/validating
sourceCount, store stdinPrompt and hasStdinPrompt, increment sourceCount if
hasStdinPrompt, then perform the existing exclusivity check and use the stored
stdinPrompt/hasStdinPrompt in the switch; ensure you don't re-read stdin later
(use the saved stdinPrompt and set s.readPromptStdin = true /
s.resolvedPromptText = stdinPrompt when selecting the stdin case).

In `@internal/contentblock/engine.go`:
- Around line 51-60: MarshalEnvelopeJSON currently returns any non-empty data
verbatim without validating it's well-formed JSON or that an embedded "type"
matches blockType; update MarshalEnvelopeJSON(blockType, data) to first ensure
json.Valid(data) (return an error if not), then unmarshal data into a small
container (e.g. map[string]json.RawMessage or struct with a Type field) and, if
a "type" field is present, verify it equals blockType (return an error if it
disagrees); if validation passes, return the original data unchanged and include
clear contextual error messages mentioning blockType.

In `@internal/core/agent/registry_launch.go`:
- Around line 73-79: EnsureAvailable currently calls resolveLaunchCommand with
spec.DefaultModel, ignoring any user-supplied cfg.Model; change it to use the
effective model selection logic (use cfg.Model when set, otherwise fall back to
spec.DefaultModel) and pass that effective model into resolveLaunchCommand so
availability probing uses the same model choice as runtime. Update the call in
EnsureAvailable to compute effectiveModel := cfg.Model; if effectiveModel == ""
then effectiveModel = spec.DefaultModel, then call resolveLaunchCommand(spec,
effectiveModel, cfg.ReasoningEffort, cfg.AddDirs, cfg.AccessMode, true).
- Around line 306-324: joinAvailabilityErrors currently flattens multiple errors
into a single string losing typed AvailabilityError details; update the function
(joinAvailabilityErrors) to preserve error types by returning a chained/joined
error instead of strings.Join — e.g., when len(errs) > 1 return
errors.Join(errs...) (or wrap with fmt.Errorf("%w", err) per project style) so
callers can use errors.As to recover AvailabilityError fields; keep the existing
zero-case that returns an AvailabilityError constructed from Spec and the
single-error case returning errs[0].
- Around line 180-186: formatShellArg currently uses strconv.Quote, which
produces Go string literals but allows shell expansions; replace that with
shell-safe quoting: implement a shell quoting routine (used by formatShellArg)
that wraps the arg in single quotes and escapes any internal single quotes by
replacing ' with '\'' (i.e., close single quote, insert escaped single quote,
reopen single quote) so the resulting string is safe to paste into a POSIX shell
and will not undergo $ or ` expansion; update formatShellArg to call this
shell-safe function whenever strings.ContainsAny detects characters requiring
quoting.

In `@internal/core/agent/registry_specs.go`:
- Around line 55-60: Update the RuntimeRegistry interface so EnsureAvailable
accepts context.Context as its first parameter (change signature to
EnsureAvailable(ctx context.Context, cfg *model.RuntimeConfig) error), then
update all concrete implementations of RuntimeRegistry to match that signature
and propagate the incoming ctx through their external process calls; finally,
replace any uses that call EnsureAvailable (notably verifyLauncher) to pass the
caller's ctx instead of using context.Background(), ensuring context is threaded
through to allow cancellation/timeouts.

In `@internal/core/agent/registry_validate.go`:
- Around line 145-156: The runtimePromptSourceCount function currently counts
PromptText, PromptFile, and ReadPromptStdin but omits ResolvedPromptText,
causing valid requests normalized into ResolvedPromptText to fail validation;
update runtimePromptSourceCount(cfg *model.RuntimeConfig) to also check
strings.TrimSpace(cfg.ResolvedPromptText) != "" and increment sources when
present (treat it as a fallback prompt source), so the function aligns with
resolveExecPromptText which prefers ResolvedPromptText.
- Around line 12-18: ValidateRuntimeConfig currently dereferences cfg (cfg.Mode)
and will panic when called with nil; add an explicit nil check at the start of
ValidateRuntimeConfig to return a descriptive error (e.g., "runtime config is
nil") instead of panicking, then proceed to call validateRuntimeMode and
lookupAgentSpec as before; reference the ValidateRuntimeConfig function and
ensure any callers still get the same semantic errors from
validateRuntimeMode/lookupAgentSpec when cfg is non-nil.

In `@internal/core/agent/tool_call_name.go`:
- Around line 161-171: The condition inside the ref_id branch redundantly tests
refID != "" again (refID was already checked), causing the OpenURL branch to
always fire when pattern is empty; in the block starting with refID :=
extractString(input, "ref_id", "refId") check, change the final if to only test
presence of "url" (i.e., if extractString(input, "url") != "" ) before returning
"OpenURL", leaving the Click and toolNameFind checks intact so OpenURL is only
returned when a url key exists.

In `@internal/core/migration/migrate.go`:
- Around line 166-194: The three functions appendTaskMigration,
appendReviewMigration, and recordRoundMeta currently swallow all errors from
inspectTaskArtifact/inspectReviewArtifact/inspectRoundMeta by calling
s.recordInvalid(path) and returning nil; instead, preserve and return the
original errors wrapped with context (e.g., fmt.Errorf("inspectTaskArtifact %s:
%w", path, err)) so I/O/permission/parse failures remain visible to callers.
Concretely: in appendTaskMigration and appendReviewMigration, on error do not
just call s.recordInvalid(path) and return nil — return a wrapped error; if you
have a specific sentinel error for “invalid artifact” (e.g.,
ErrInvalidArtifact), you may still call s.recordInvalid(path) for that case but
still return the wrapped sentinel error. Apply the same change to
recordRoundMeta (wrap and return inspectRoundMeta errors instead of silently
recording invalid).
- Around line 81-88: The write loop over pending artifacts currently always
writes every file; change it to honor the migration context by checking
ctx.Err() or using a select on ctx.Done() before each os.WriteFile call and
aborting early (returning ctx.Err() or a wrapped context.Canceled error) so no
further files are mutated after cancellation; update the loop that iterates over
pending (the slice sorted with sort.Slice) to perform this context check before
attempting each write and avoid proceeding to os.WriteFile when the context is
cancelled.

In `@internal/core/migration/workflow_target.go`:
- Around line 30-31: Reject workflow names that can escape the workflow root by
validating cfg.name before joining it to rootDir: ensure cfg.name is non-empty
and does not contain path separators ('/' or '\\'), dot-segments ('.' or '..'),
or an absolute path prefix; if any of these are present return an error (fail
fast). Update the check around strings.TrimSpace(cfg.name) != "" (and the
similar check at the other occurrence around lines 61-63) to perform this
validation and use the same validation helper wherever cfg.name is used to form
a path to prevent directory traversal/escape.

In `@internal/core/model/artifacts.go`:
- Around line 74-79: The JobArtifacts helper trusts the caller's safeName and
can be tricked into creating paths outside the intended jobs directory; update
RunArtifacts.JobArtifacts to sanitize/normalize safeName (same approach used in
NewRunArtifacts) before joining: reject empty or names containing path
separators or "..", or else replace with filepath.Base / strings.ReplaceAll to
strip directory components and disallow traversal, and then use that sanitized
name to build PromptPath, OutLogPath and ErrLogPath so artifact writes remain
confined to the flat jobs namespace.

In `@internal/core/model/content_test.go`:
- Around line 217-248: Wrap the existing test body of
TestContentBlockMarshalUsesCamelCaseJSONTags into a t.Run subtest with a
"Should..." name (e.g., t.Run("Should use camelCase JSON tags", func(t
*testing.T) { ... })), moving t.Parallel() into the subtest if parallelism is
desired, and keep all existing setup and assertions (NewContentBlock,
json.Marshal, checks against encoded for required/forbidden fields) unchanged
inside that subtest so the test follows the repository's t.Run naming pattern.

In `@internal/core/model/preparation.go`:
- Around line 35-53: SetJournal currently overwrites an existing JournalHandle
without releasing it and CloseJournal clears JournalHandle before Close(ctx)
completes; update SetJournal and CloseJournal on SolvePreparation to protect
ownership: in SetJournal, if p.JournalHandle != nil then refuse to replace
(no-op) or return without dropping the old owner (i.e., do not call
journal.NewOwner when an owner already exists) to avoid orphaning the previous
handle; in CloseJournal, capture the current handle, call handle.Close(ctx)
first and only set p.JournalHandle = nil after Close returns successfully
(propagate/return the error if Close fails) so the previous owner remains
reachable on failure; reference methods: SetJournal, CloseJournal,
JournalHandle, journal.NewOwner, and handle.Close.

In `@internal/core/prompt/prompt_test.go`:
- Around line 288-294: Update TestSafeFileName to a table-driven subtest using
t.Run and verify both the sanitized prefix and the hash suffix behavior of
SafeFileName: for each case assert the returned string starts with the expected
sanitized prefix and then either (a) verify the suffix matches the expected
6-character alphanumeric fingerprint shape (e.g., regex [A-Za-z0-9]{6} after a
hyphen) or (b) include a pair of inputs that sanitize to the same base and
assert SafeFileName yields different full names for them (ensuring a
collision-resistant suffix); reference the SafeFileName function and the
TestSafeFileName test name when updating the assertions.

In `@internal/core/reviews/parser_test.go`:
- Around line 9-96: The tests in TestReviewParsingHelpers,
TestExtractIssueNumber, and TestWrapParseErrorProvidesMigrationGuidance must be
refactored into table-driven subtests using t.Run("Should ...") so each scenario
is isolated; create a slice of test cases (with name, input, expected) for the
ParseReviewContext/IsReviewResolved/legacy parse/extract flows and for
ExtractIssueNumber and WrapParseError, iterate over cases and call t.Run("Should
<behavior>", func(t *testing.T){ t.Parallel(); ... assertions ... }), moving
each assertion into its matching subtest and invoking the existing helper
functions (ParseReviewContext, IsReviewResolved, ParseLegacyReviewContext,
ExtractLegacyReviewBody, LooksLikeLegacyReviewFile, ExtractIssueNumber,
WrapParseError) inside the subtests; ensure error expectations use errors.Is
where appropriate and keep test behaviour identical while adopting the required
"Should..." subtest naming and table-driven pattern.

In `@internal/core/reviews/parser.go`:
- Around line 144-156: The extractXMLTag function currently searches the whole
file and must be limited to the <review_context> block: first locate the
"<review_context>" open and "</review_context>" close (return "" if either is
missing), slice content to just that inner block, then perform the existing tag
extraction logic on that slice; update references to extractXMLTag to continue
accepting the full file string (the function itself will do the scoping), and
keep the function name extractXMLTag and tag parameter unchanged.

---

Outside diff comments:
In `@internal/core/plan/journal.go`:
- Around line 19-21: Replace the use of context.Background() fallback with
context.TODO() in the journal close path: where the variable closeCtx is being
defaulted (the block containing "if closeCtx == nil { closeCtx =
context.Background() }"), change it to use context.TODO() so the code signals an
incomplete context setup; update the assignment referencing closeCtx in the
journal.go function/method that performs the close logic accordingly.

---

Nitpick comments:
In `@cmd/compozy/main.go`:
- Around line 61-66: waitForUpdateResult currently blocks on <-result and can
hang the CLI if CheckForUpdate is slow; change waitForUpdateResult to use a
select that waits on the result channel and a timeout (e.g., time.After with a
short duration) and return nil on timeout so the CLI can exit promptly;
reference the result channel returned by startUpdateCheck/CheckForUpdate and
ensure you import time and handle the nil result case as before.

In `@internal/cli/kernel_dispatch_test.go`:
- Around line 248-249: Remove the redundant per-iteration capture assignment by
deleting the inner "tt := tt" in the test loop that iterates over "tests" (the
loop variable "tt" is already correctly scoped in Go 1.26.1), so simply iterate
with "for _, tt := range tests { ... }" and update any closures to reference
"tt" directly without the extra reassignment.

In `@internal/cli/workspace_config_test.go`:
- Around line 234-308: The two monolithic tests
TestApplyConfigHandlesSupportedTypes and TestApplyInputHandlesSupportedTypes
should be refactored into table-driven subtests using t.Run("Should ...") so
each coercion/parsing branch is exercised and reported independently; for each
test create a slice of cases (each with a name like "Should apply string
config", target flag/key, input value, helper function reference such as
applyConfig/applyInput, and expected result) and iterate calling
t.Run(case.name, func(t *testing.T){ t.Parallel(); ... assert expectations }),
keeping use of helpers passThroughInput, parseIntInput, parseStringSliceInput
and the clone function for slices, and preserve current assertions but split
them into per-case checks so failures identify the exact contract that
regressed.

In `@internal/core/agent/acp_convert.go`:
- Around line 436-449: marshalRawJSON currently swallows json.Marshal errors and
returns nil; change its signature to return (json.RawMessage, error) and
propagate the marshal error instead of returning nil, updating callers to handle
the error (or alternatively log the error where appropriate); specifically
modify function marshalRawJSON to return both payload and error and update any
call sites that use marshalRawJSON to check and handle the returned error.

In `@internal/core/agent/tool_call_name.go`:
- Around line 134-143: Remove the duplicate trimming logic by deleting the
acp.ToolKindOther case block inside the switch in the GetToolCallName (or
equivalent) function so that the post-switch fallback that trims and returns
title handles that kind; ensure you leave the final post-switch check that uses
strings.TrimSpace(title) and returns trimmed or "" so other cases still
short-circuit as intended and no duplicate trimmed variable declarations remain.
- Around line 91-93: The conditional combining token and input checks is correct
but unclear; update the if in tool_call_name.go to use explicit parentheses
around the alternatives—e.g., parenthesize (token == "update_todo") and (input
!= nil && input["todos"] != nil) when returning toolNameTodoWrite—so the intent
is immediately obvious when reading the token, input, and toolNameTodoWrite
logic.
- Around line 227-236: The cloneMap function currently does a shallow copy and
will share nested map/slice references; either document this behavior or change
cloneMap to perform a deep copy by recursively handling nested types: when
iterating in cloneMap, if value is map[string]any call cloneMap(value)
recursively, if value is []any make a new slice and deep-clone each element
(recursing for nested maps/slices), and for other types copy as-is; update
function name/signature or add helper deepCloneValue/deepCloneMap helpers as
needed to keep code clear.
- Around line 28-60: The map commonToolTitleAliases mixes string literals and
existing constants (e.g., toolNameFind); define constants for the repeated tool
title strings (e.g., Edit, Write, Glob, Grep, Read, Task, etc.), replace the
string literals in commonToolTitleAliases with those new constants, and update
any other places that use the same literal values to use the constants for
consistency; keep existing constant names like toolNameFind unchanged and ensure
the new constants are declared near other toolName* constants for
discoverability.

In `@internal/core/kernel/core_adapters_test.go`:
- Around line 98-221: The four near-duplicate tests
(TestDispatchFetchReviewsAdapterBuildsReviewsFetchCommand,
TestDispatchMigrateAdapterBuildsWorkspaceMigrateCommand,
TestDispatchSyncAdapterBuildsWorkflowSyncCommand,
TestDispatchArchiveAdapterBuildsWorkflowArchiveCommand) should be merged into a
single table-driven test that iterates cases and uses t.Run("Should ...")
subtests; create a cases slice where each entry specifies the adapter function
to call (dispatchFetchReviewsAdapter, dispatchMigrateAdapter,
dispatchSyncAdapter, dispatchArchiveAdapter), the input config, the expected
result assertions, and the capture handler type/result to register, then inside
the loop set up NewDispatcher(), Register(dispatcher, handler), stub
coreAdapterDispatcherFn to return the dispatcher, call the adapter function, and
assert result shape and handler.got fields; keep using t.Cleanup to restore
coreAdapterDispatcherFn and preserve the same assertions per-case but eliminated
boilerplate by centralizing common setup and teardown.

In `@internal/core/kernel/deps_test.go`:
- Around line 580-581: Remove the redundant loop-variable shadowing line (`tt :=
tt`) inside the for loop over `tests`; simply iterate `for _, tt := range tests
{` and use `tt` directly (or pass `tt` into the subtest closure via
`t.Run(tt.name, func(t *testing.T) { tc := tt; ... })` if you prefer explicit
capture), so eliminate the unnecessary `tt := tt` statement in the test loop in
deps_test.go.

In `@internal/core/model/constants.go`:
- Around line 15-22: The constants DefaultCodexModel, DefaultClaudeModel,
DefaultCursorModel, DefaultOpenCodeModel, DefaultPiModel, DefaultGeminiModel,
DefaultCopilotModel and DefaultActivityTimeout are hardcoded runtime
configuration and should be moved out of package constants; instead remove these
values from internal/core/model/constants.go and load them from a TOML config or
provide them via functional options/constructor parameters (e.g., pass defaults
into the model/provider constructors or a Config struct) so provider
renames/deprecations and timeouts can be changed without code edits; update any
usages of these symbols to read from the injected Config or options and add
sensible fallback logic where the config is missing.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19813922-699f-440e-8333-6acace34e7fd

📥 Commits

Reviewing files that changed from the base of the PR and between 63664c3 and fca9d27.

⛔ Files ignored due to path filters (69)
  • .agents/skills/refactoring-analysis/SKILL.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/assets/refactoring-report-template.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/checklists/analysis-checklist.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/references/code-smells-catalog.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/references/refactoring-techniques.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/references/solid-ddd-context.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/refactoring-analysis/scripts/validate-metadata.py is excluded by !.agents/**
  • .claude/commands/refac.md is excluded by !**/*.md, !.claude/**
  • .codex/ledger/2026-04-06-MEMORY-dry-generics.md is excluded by !**/*.md
  • .codex/ledger/2026-04-06-MEMORY-package-splits.md is excluded by !**/*.md
  • .codex/ledger/2026-04-07-MEMORY-acp-cockpit-hang.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/_meta.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/_techspec.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-001.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-002.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-003.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-004.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-005.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/adrs/adr-006.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_02.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_03.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_04.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_05.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_06.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_07.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/memory/task_08.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_01.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_02.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_03.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_04.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_05.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_06.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_07.md is excluded by !**/*.md
  • .compozy/tasks/_archived/20260406-231057-engine-kernel/task_08.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/_meta.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/_techspec.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/adrs/adr-001.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/adrs/adr-002.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/adrs/adr-003.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/auto-update/task_01.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-agent-run.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-cli-entry.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-core-foundation.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-plan-domain.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-provider-public.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-summary.md is excluded by !**/*.md
  • .compozy/tasks/refac/20260406-verification.md is excluded by !**/*.md
  • .compozy/tasks/refac/_meta.md is excluded by !**/*.md
  • .compozy/tasks/refac/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/refac/_techspec.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/task_02.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/task_03.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/task_04.md is excluded by !**/*.md
  • .compozy/tasks/refac/memory/task_05.md is excluded by !**/*.md
  • .compozy/tasks/refac/task_01.md is excluded by !**/*.md
  • .compozy/tasks/refac/task_02.md is excluded by !**/*.md
  • .compozy/tasks/refac/task_03.md is excluded by !**/*.md
  • .compozy/tasks/refac/task_04.md is excluded by !**/*.md
  • .compozy/tasks/refac/task_05.md is excluded by !**/*.md
  • go.sum is excluded by !**/*.sum
  • skills-lock.json is excluded by !**/*.json
📒 Files selected for processing (167)
  • cmd/compozy/main.go
  • go.mod
  • internal/cli/commands.go
  • internal/cli/commands_simple.go
  • internal/cli/dispatch_adapters.go
  • internal/cli/form.go
  • internal/cli/kernel_dispatch_test.go
  • internal/cli/root.go
  • internal/cli/root_test.go
  • internal/cli/run.go
  • internal/cli/state.go
  • internal/cli/upgrade.go
  • internal/cli/workspace_config.go
  • internal/cli/workspace_config_test.go
  • internal/contentblock/engine.go
  • internal/core/agent/acp_convert.go
  • internal/core/agent/registry.go
  • internal/core/agent/registry_launch.go
  • internal/core/agent/registry_specs.go
  • internal/core/agent/registry_validate.go
  • internal/core/agent/session.go
  • internal/core/agent/tool_call_input.go
  • internal/core/agent/tool_call_name.go
  • internal/core/api.go
  • internal/core/archive.go
  • internal/core/contentconv/contentconv.go
  • internal/core/contentconv/contentconv_test.go
  • internal/core/fetch.go
  • internal/core/kernel/commands/commands_test.go
  • internal/core/kernel/commands/reviews_fetch.go
  • internal/core/kernel/commands/run_start.go
  • internal/core/kernel/commands/runtime_config.go
  • internal/core/kernel/commands/runtime_fields.go
  • internal/core/kernel/commands/workflow_archive.go
  • internal/core/kernel/commands/workflow_prepare.go
  • internal/core/kernel/commands/workflow_sync.go
  • internal/core/kernel/commands/workspace_migrate.go
  • internal/core/kernel/core_adapters.go
  • internal/core/kernel/core_adapters_test.go
  • internal/core/kernel/deps_test.go
  • internal/core/kernel/handlers.go
  • internal/core/migrate.go
  • internal/core/migration/migrate.go
  • internal/core/migration/migrate_test.go
  • internal/core/migration/workflow_target.go
  • internal/core/model/artifacts.go
  • internal/core/model/constants.go
  • internal/core/model/content.go
  • internal/core/model/content_test.go
  • internal/core/model/model.go
  • internal/core/model/preparation.go
  • internal/core/model/runtime_config.go
  • internal/core/model/task_review.go
  • internal/core/model/workflow_ops.go
  • internal/core/model/workspace_paths.go
  • internal/core/plan/input.go
  • internal/core/plan/journal.go
  • internal/core/plan/prepare.go
  • internal/core/plan/prepare_test.go
  • internal/core/prompt/common.go
  • internal/core/prompt/prd.go
  • internal/core/prompt/prompt_test.go
  • internal/core/prompt/templates.go
  • internal/core/providerdefaults/defaults.go
  • internal/core/reviews/parser.go
  • internal/core/reviews/parser_test.go
  • internal/core/reviews/store.go
  • internal/core/reviews/store_test.go
  • internal/core/run/command_io.go
  • internal/core/run/exec/aliases.go
  • internal/core/run/exec/exec.go
  • internal/core/run/exec/exec_integration_test.go
  • internal/core/run/exec/exec_test.go
  • internal/core/run/exec/test_helpers_test.go
  • internal/core/run/exec_facade.go
  • internal/core/run/execution.go
  • internal/core/run/executor/aliases.go
  • internal/core/run/executor/execution.go
  • internal/core/run/executor/execution_acp_integration_test.go
  • internal/core/run/executor/execution_acp_test.go
  • internal/core/run/executor/execution_test.go
  • internal/core/run/executor/execution_ui_test.go
  • internal/core/run/executor/format_helpers.go
  • internal/core/run/executor/lifecycle.go
  • internal/core/run/executor/result.go
  • internal/core/run/executor/result_test.go
  • internal/core/run/executor/review_hooks.go
  • internal/core/run/executor/runner.go
  • internal/core/run/executor/shutdown.go
  • internal/core/run/executor/test_helpers_test.go
  • internal/core/run/internal/acpshared/aliases.go
  • internal/core/run/internal/acpshared/command_io.go
  • internal/core/run/internal/acpshared/command_io_test.go
  • internal/core/run/internal/acpshared/compat_test.go
  • internal/core/run/internal/acpshared/session_exec.go
  • internal/core/run/internal/acpshared/session_handler.go
  • internal/core/run/internal/acpshared/session_handler_test.go
  • internal/core/run/internal/runshared/buffers.go
  • internal/core/run/internal/runshared/config.go
  • internal/core/run/internal/runshared/config_test.go
  • internal/core/run/internal/runshared/logging.go
  • internal/core/run/internal/runshared/shutdown.go
  • internal/core/run/internal/runshared/types.go
  • internal/core/run/internal/runtimeevents/events.go
  • internal/core/run/logging.go
  • internal/core/run/preflight.go
  • internal/core/run/preflight/preflight.go
  • internal/core/run/preflight/preflight_test.go
  • internal/core/run/preflight/preflight_test_helpers_test.go
  • internal/core/run/run.go
  • internal/core/run/run_test.go
  • internal/core/run/runtime_logger.go
  • internal/core/run/transcript/compat_test.go
  • internal/core/run/transcript/model.go
  • internal/core/run/transcript/model_test.go
  • internal/core/run/transcript/render.go
  • internal/core/run/transcript/tool_use_summary.go
  • internal/core/run/types.go
  • internal/core/run/ui/adapter_test.go
  • internal/core/run/ui/aliases.go
  • internal/core/run/ui/layout.go
  • internal/core/run/ui/model.go
  • internal/core/run/ui/model_test.go
  • internal/core/run/ui/sidebar.go
  • internal/core/run/ui/styles.go
  • internal/core/run/ui/summary.go
  • internal/core/run/ui/timeline.go
  • internal/core/run/ui/types.go
  • internal/core/run/ui/update.go
  • internal/core/run/ui/update_test.go
  • internal/core/run/ui/validation_form.go
  • internal/core/run/ui/validation_form_test.go
  • internal/core/run/ui/view.go
  • internal/core/run/ui/view_test.go
  • internal/core/run/ui_view.go
  • internal/core/sync.go
  • internal/core/tasks/parser.go
  • internal/core/tasks/parser_test.go
  • internal/core/tasks/store.go
  • internal/core/tasks/validate.go
  • internal/core/tasks/walker.go
  • internal/core/tasks/walker_test.go
  • internal/core/workflow_target.go
  • internal/core/workflow_target_test.go
  • internal/core/workspace/config.go
  • internal/core/workspace/config_types.go
  • internal/core/workspace/config_validate.go
  • internal/setup/agents.go
  • internal/setup/agents_test.go
  • internal/setup/install.go
  • internal/setup/select.go
  • internal/update/check.go
  • internal/update/check_test.go
  • internal/update/install.go
  • internal/update/install_test.go
  • internal/update/state.go
  • internal/update/state_test.go
  • pkg/compozy/events/event_test.go
  • pkg/compozy/events/kinds/content_block.go
  • pkg/compozy/events/kinds/job.go
  • pkg/compozy/events/kinds/payload_compat_test.go
  • pkg/compozy/events/kinds/session.go
  • pkg/compozy/events/kinds/session_test.go
  • pkg/compozy/events/kinds/shutdown.go
  • pkg/compozy/runs/replay.go
  • pkg/compozy/runs/run.go
  • pkg/compozy/runs/status.go
💤 Files with no reviewable changes (6)
  • internal/core/agent/session.go
  • internal/core/run/command_io.go
  • internal/core/agent/tool_call_input.go
  • internal/core/kernel/commands/runtime_fields.go
  • internal/core/model/model.go
  • internal/core/agent/registry.go

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/state.go
Comment thread internal/contentblock/engine.go
Comment thread internal/core/agent/registry_launch.go
Comment thread internal/core/agent/registry_launch.go Outdated
Comment thread internal/core/model/content_test.go
Comment thread internal/core/model/preparation.go
Comment thread internal/core/prompt/prompt_test.go
Comment thread internal/core/reviews/parser_test.go
Comment thread internal/core/reviews/parser.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (7)
internal/cli/root_test.go (1)

1619-1628: Consider returning the underlying error instead of io.ErrClosedPipe as fallback.

When w.err is nil, the writer returns io.ErrClosedPipe. This fallback is fine for current tests but could mask issues if someone accidentally uses the struct without setting the error.

Minor improvement
 type failOnWriteWriter struct {
 	err error
 }

 func (w *failOnWriteWriter) Write(_ []byte) (int, error) {
-	if w.err != nil {
-		return 0, w.err
-	}
-	return 0, io.ErrClosedPipe
+	if w.err == nil {
+		return 0, io.ErrClosedPipe
+	}
+	return 0, w.err
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/root_test.go` around lines 1619 - 1628, The
failOnWriteWriter.Write fallback returns io.ErrClosedPipe when w.err is nil,
which can mask real underlying errors; modify the Write method on the
failOnWriteWriter type to return a more informative default by returning the
underlying w.err when present and otherwise return a distinct sentinel (e.g.,
nil or a new explicit error) that clearly signals "no underlying error set" so
callers/tests won't be misled—update the Write method on failOnWriteWriter
(referenced by type name failOnWriteWriter and its Write method) to return w.err
when non-nil and a clearer fallback error when nil.
internal/cli/run.go (1)

13-67: Consider extracting shared setup logic between run and exec.

Both methods share identical patterns: signal context setup, workspace defaults, config building, persisted exec config, validation, and runPrepared call. The only difference is exec calls resolveExecPromptSource instead of maybeCollectInteractiveParams.

This duplication increases maintenance burden if the common flow needs modification.

Potential extraction (optional)
func (s *commandState) prepareAndRun(cmd *cobra.Command, setupFn func() error) error {
	ctx, stop := signalCommandContext(cmd)
	defer stop()

	if err := s.applyWorkspaceDefaults(ctx, cmd); err != nil {
		return s.handleExecError(cmd, fmt.Errorf("apply workspace defaults for %s: %w", cmd.Name(), err))
	}
	if err := setupFn(); err != nil {
		return s.handleExecError(cmd, err)
	}

	cfg, err := s.buildConfig()
	if err != nil {
		return s.handleExecError(cmd, err)
	}
	// ... rest of common flow
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/run.go` around lines 13 - 67, Extract the shared setup flow from
run and exec into a helper (e.g., prepareAndRun) that performs
signalCommandContext, applyWorkspaceDefaults, buildConfig,
applyPersistedExecConfig, cfg.Validate and runPrepared, and accept a small
setupFn parameter so callers can inject the differing step (call
maybeCollectInteractiveParams for run and resolveExecPromptSource for exec);
ensure the helper uses the same error-wrapping/handling via handleExecError and
preserves the special applyWorkspaceDefaults error message formatting used in
exec, then replace the bodies of run and exec to call this new prepareAndRun
helper.
internal/cli/state.go (2)

107-134: withFallbacks modifies receiver but returns value - consider consistency.

The method modifies defaults fields in-place (e.g., defaults.isInteractive = builtin.isInteractive) and then returns defaults. This works because commandStateDefaults is passed by value, but the pattern could be confusing. Consider either:

  • Making it purely functional (create new struct), or
  • Making it a pointer receiver that modifies in place and returns nothing

Current code is correct but the hybrid approach is unusual.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/state.go` around lines 107 - 134, The method withFallbacks
currently mutates its value receiver (commandStateDefaults) and returns it,
which is confusing; either convert it to an in-place pointer receiver or make it
purely functional. To fix: choose one approach — (A) change the receiver to
*commandStateDefaults (withFallbacks) and remove the return value so the method
mutates the struct in place, updating all call sites to rely on mutation; or (B)
keep a value receiver but avoid mutating the parameter by creating a new local
variable (e.g., result := defaults), set fallbacks on result using
defaultCommandStateDefaults(), and return result; then keep existing call sites
that expect a returned value. Ensure you update all references to withFallbacks
accordingly.

225-249: Redundant nil checks for callbacks that are always set by withFallbacks.

Lines 230-233 and 241-244 check if isInteractive and collectForm are nil, but newCommandStateWithDefaults always calls withFallbacks() which guarantees these are set. The nil checks are defensive but potentially misleading about the contract.

Consider removing redundant nil checks or documenting the defensiveness

If withFallbacks() is always called during construction, these checks are unreachable. Either:

  1. Remove them to clarify the invariant, or
  2. Add a comment explaining they're defensive for direct struct construction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/state.go` around lines 225 - 249, The nil checks for
isInteractive and collectForm in maybeCollectInteractiveParams are redundant
because newCommandStateWithDefaults calls withFallbacks() which guarantees those
callbacks are set; remove the conditional assignments and directly call
s.isInteractive() and s.collectForm(cmd, s) (or, if you prefer to keep the
defensive pattern, add a short comment above the checks referencing
newCommandStateWithDefaults and withFallbacks to explain they are only defensive
for non-standard construction). Update maybeCollectInteractiveParams to use the
guaranteed callbacks (isInteractive and collectForm) and simplify the error/flow
accordingly, referencing the methods/fields: maybeCollectInteractiveParams,
isInteractive, collectForm, newCommandStateWithDefaults, and withFallbacks.
internal/core/model/preparation.go (2)

53-54: Wrap close errors with operation context.

Returning the raw error here loses call-site context; wrapping makes failures easier to diagnose.

Suggested change
 import (
 	"context"
+	"fmt"

 	"github.com/compozy/compozy/internal/core/run/journal"
 )
@@
 	handle := p.JournalHandle
 	if err := handle.Close(ctx); err != nil {
-		return err
+		return fmt.Errorf("close preparation journal: %w", err)
 	}

As per coding guidelines, "Prefer explicit error returns with wrapped context using fmt.Errorf(\"context: %w\", err)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/model/preparation.go` around lines 53 - 54, The current return
of the raw error from handle.Close(ctx) should be wrapped with operation
context; change the error return in the function that calls handle.Close(ctx) to
return fmt.Errorf("closing handle: %w", err) (or another clear contextual
string) and add a fmt import if missing so the caller receives a wrapped error
with call-site context.

9-12: Add compile-time interface conformance assertion.

Since SetJournal relies on journal.NewOwner, add an explicit compile-time check to prevent future interface drift.

Suggested addition
 type JournalHandle interface {
 	Journal() *journal.Journal
 	Close(context.Context) error
 }
+
+var _ JournalHandle = (*journal.Owner)(nil)

As per coding guidelines, "Use compile-time interface verification in Go: var _ Interface = (*Type)(nil)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/model/preparation.go` around lines 9 - 12, Add a compile-time
interface conformance check to ensure the concrete type produced by
journal.NewOwner implements the JournalHandle interface used by SetJournal: add
a package-level var declaration asserting JournalHandle is satisfied by the
concrete owner type returned from journal.NewOwner (use the concrete type name
from the journal package, e.g. the Owner type) so future interface drift causes
a compile error.
internal/core/agent/client.go (1)

537-545: Consider propagating the caller's context instead of context.Background().

Using context.Background() in production code prevents callers from cancelling or timing out launch command resolution. The caller ensureStarted receives a ctx parameter that could be propagated here.

If the decoupling from caller cancellation is intentional (e.g., to ensure resolution completes once started), consider documenting this design decision with a comment explaining why the caller's context is not used.

As per coding guidelines: "Pass context.Context as the first argument to all functions crossing runtime boundaries; avoid context.Background() outside main and focused tests."

💡 Option to propagate context
 func (c *clientImpl) resolveStartCommand(req SessionRequest) (string, []string, error) {
+// Note: This method would need ctx as a parameter if propagation is desired
 	requestedModel := resolveModel(c.spec, firstNonEmpty(req.Model, c.cfg.Model))
 	startModel := c.spec.DefaultModel
 	if c.spec.UsesBootstrapModel {
 		startModel = requestedModel
 	}
 	command, err := resolveLaunchCommand(
-		context.Background(),
+		ctx, // propagated from caller
 		c.spec,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/client.go` around lines 537 - 545, The call to
resolveLaunchCommand uses context.Background() which prevents caller
cancellation; update ensureStarted to pass its incoming ctx through to
resolveLaunchCommand (replace context.Background() with the ensureStarted ctx)
so resolution honors timeouts/cancellation, referencing resolveLaunchCommand and
ensureStarted and the existing parameters (c.spec, startModel,
c.cfg.ReasoningEffort, c.cfg.AddDirs, c.cfg.AccessMode); if the behavior of
decoupling is intentional, instead add a brief comment above the call explaining
why a detached context is required and the decision rationale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/contentblock/engine.go`:
- Around line 86-87: The code calls validate(envelope.Type, data) without
checking if validate is nil and returns the validator error without context; add
a nil-check for the validate function pointer and return a clear wrapped error
if it's missing (e.g., return Envelope[T]{}, fmt.Errorf("no validator for type
%s", envelope.Type)), and when validate returns an error wrap it with context
including envelope.Type using fmt.Errorf("validate %s: %w", envelope.Type, err);
ensure fmt is imported and update the call site around validate(...) and the
Envelope[T] return accordingly.
- Around line 100-111: The code calls the blockType callback without checking
for nil (unlike normalize); add a nil guard before invoking blockType in the
decode function: verify blockType != nil and return a descriptive error (e.g.,
"decode <expected> block: missing blockType callback") if it's nil, then proceed
to call blockType(block) and compare to expected; reference the blockType and
normalize parameters and the existing error-return pattern used after
json.Unmarshal to keep behavior consistent.

In `@internal/core/agent/registry_launch.go`:
- Around line 63-87: EnsureAvailable may dereference a nil cfg (cfg.DryRun)
before the ctx nil-check; add an explicit nil check for cfg at the start of
EnsureAvailable (e.g., if cfg == nil { return fmt.Errorf("nil RuntimeConfig") })
and return a clear error, preserving the existing ctx nil-default logic and
subsequent calls to lookupAgentSpec and resolveLaunchCommand; reference
EnsureAvailable and cfg.DryRun when making the change.

In `@internal/core/migration/workflow_target_test.go`:
- Around line 39-40: Replace string-based error checks by introducing a sentinel
error (e.g., ErrInvalidWorkflowName) in the package, have normalizeWorkflowName
wrap/return that sentinel (use fmt.Errorf("%w: %s", ErrInvalidWorkflowName,
details) or similar) so callers can detect the failure, and update the test in
workflow_target_test.go to assert with errors.Is(err, ErrInvalidWorkflowName)
instead of strings.Contains(err.Error(), ...); apply the same
replace-for-string-match change in the other tests noted in migrate_test.go that
check the same validation.

In `@internal/core/model/preparation_test.go`:
- Around line 13-62: Split the two top-level tests
TestSolvePreparationSetJournalPreservesExistingOwnership and
TestSolvePreparationCloseJournalPreservesHandleOnFailure into t.Run subtests
(preferably table-driven): create a table of cases describing the scenario and
expected outcome and for each case call t.Run("Should <description>", func(t
*testing.T){ ... }); move the existing setup/assert logic into the subtest body
and preserve use of SolvePreparation.SetJournal, SolvePreparation.Journal(),
CloseJournal, and the stubJournalHandle (err) to drive the failure case; ensure
each subtest calls t.Parallel() where appropriate and that the original
assertions remain unchanged.
- Around line 55-58: The test currently only checks err != nil for
prep.CloseJournal; instead define or reuse a sentinel error (e.g.,
ErrCloseJournalFailed) that the CloseJournal implementation returns on the
failure path, then update the test to assert the exact error using
errors.Is(err, ErrCloseJournalFailed) (or testing helper
require.ErrorIs/Assert.ErrorIs) against prep.CloseJournal; ensure the sentinel
is exported/visible to the test or placed in a shared test-only package and that
CloseJournal returns that sentinel in the failure scenario.

---

Nitpick comments:
In `@internal/cli/root_test.go`:
- Around line 1619-1628: The failOnWriteWriter.Write fallback returns
io.ErrClosedPipe when w.err is nil, which can mask real underlying errors;
modify the Write method on the failOnWriteWriter type to return a more
informative default by returning the underlying w.err when present and otherwise
return a distinct sentinel (e.g., nil or a new explicit error) that clearly
signals "no underlying error set" so callers/tests won't be misled—update the
Write method on failOnWriteWriter (referenced by type name failOnWriteWriter and
its Write method) to return w.err when non-nil and a clearer fallback error when
nil.

In `@internal/cli/run.go`:
- Around line 13-67: Extract the shared setup flow from run and exec into a
helper (e.g., prepareAndRun) that performs signalCommandContext,
applyWorkspaceDefaults, buildConfig, applyPersistedExecConfig, cfg.Validate and
runPrepared, and accept a small setupFn parameter so callers can inject the
differing step (call maybeCollectInteractiveParams for run and
resolveExecPromptSource for exec); ensure the helper uses the same
error-wrapping/handling via handleExecError and preserves the special
applyWorkspaceDefaults error message formatting used in exec, then replace the
bodies of run and exec to call this new prepareAndRun helper.

In `@internal/cli/state.go`:
- Around line 107-134: The method withFallbacks currently mutates its value
receiver (commandStateDefaults) and returns it, which is confusing; either
convert it to an in-place pointer receiver or make it purely functional. To fix:
choose one approach — (A) change the receiver to *commandStateDefaults
(withFallbacks) and remove the return value so the method mutates the struct in
place, updating all call sites to rely on mutation; or (B) keep a value receiver
but avoid mutating the parameter by creating a new local variable (e.g., result
:= defaults), set fallbacks on result using defaultCommandStateDefaults(), and
return result; then keep existing call sites that expect a returned value.
Ensure you update all references to withFallbacks accordingly.
- Around line 225-249: The nil checks for isInteractive and collectForm in
maybeCollectInteractiveParams are redundant because newCommandStateWithDefaults
calls withFallbacks() which guarantees those callbacks are set; remove the
conditional assignments and directly call s.isInteractive() and
s.collectForm(cmd, s) (or, if you prefer to keep the defensive pattern, add a
short comment above the checks referencing newCommandStateWithDefaults and
withFallbacks to explain they are only defensive for non-standard construction).
Update maybeCollectInteractiveParams to use the guaranteed callbacks
(isInteractive and collectForm) and simplify the error/flow accordingly,
referencing the methods/fields: maybeCollectInteractiveParams, isInteractive,
collectForm, newCommandStateWithDefaults, and withFallbacks.

In `@internal/core/agent/client.go`:
- Around line 537-545: The call to resolveLaunchCommand uses
context.Background() which prevents caller cancellation; update ensureStarted to
pass its incoming ctx through to resolveLaunchCommand (replace
context.Background() with the ensureStarted ctx) so resolution honors
timeouts/cancellation, referencing resolveLaunchCommand and ensureStarted and
the existing parameters (c.spec, startModel, c.cfg.ReasoningEffort,
c.cfg.AddDirs, c.cfg.AccessMode); if the behavior of decoupling is intentional,
instead add a brief comment above the call explaining why a detached context is
required and the decision rationale.

In `@internal/core/model/preparation.go`:
- Around line 53-54: The current return of the raw error from handle.Close(ctx)
should be wrapped with operation context; change the error return in the
function that calls handle.Close(ctx) to return fmt.Errorf("closing handle: %w",
err) (or another clear contextual string) and add a fmt import if missing so the
caller receives a wrapped error with call-site context.
- Around line 9-12: Add a compile-time interface conformance check to ensure the
concrete type produced by journal.NewOwner implements the JournalHandle
interface used by SetJournal: add a package-level var declaration asserting
JournalHandle is satisfied by the concrete owner type returned from
journal.NewOwner (use the concrete type name from the journal package, e.g. the
Owner type) so future interface drift causes a compile error.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56010523-90d4-4256-a16a-88fcaf3dcc2a

📥 Commits

Reviewing files that changed from the base of the PR and between fca9d27 and 72d3043.

⛔ Files ignored due to path filters (20)
  • .compozy/tasks/refac/reviews-001/_meta.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_012.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_013.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_014.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_015.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_016.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_017.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_018.md is excluded by !**/*.md
  • .compozy/tasks/refac/reviews-001/issue_019.md is excluded by !**/*.md
📒 Files selected for processing (25)
  • internal/cli/root_test.go
  • internal/cli/run.go
  • internal/cli/state.go
  • internal/contentblock/engine.go
  • internal/contentblock/engine_test.go
  • internal/core/agent/client.go
  • internal/core/agent/registry_launch.go
  • internal/core/agent/registry_specs.go
  • internal/core/agent/registry_test.go
  • internal/core/agent/registry_validate.go
  • internal/core/agent/session_helpers_test.go
  • internal/core/migration/migrate.go
  • internal/core/migration/migrate_test.go
  • internal/core/migration/workflow_target.go
  • internal/core/migration/workflow_target_test.go
  • internal/core/model/artifacts.go
  • internal/core/model/content_test.go
  • internal/core/model/model_test.go
  • internal/core/model/preparation.go
  • internal/core/model/preparation_test.go
  • internal/core/plan/prepare.go
  • internal/core/prompt/prompt_test.go
  • internal/core/reviews/parser.go
  • internal/core/reviews/parser_test.go
  • internal/core/run/exec/exec.go
✅ Files skipped from review due to trivial changes (3)
  • internal/core/model/content_test.go
  • internal/core/migration/workflow_target.go
  • internal/core/migration/migrate.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/core/migration/migrate_test.go
  • internal/core/agent/registry_validate.go

Comment thread internal/contentblock/engine.go Outdated
Comment thread internal/contentblock/engine.go
Comment thread internal/core/agent/registry_launch.go
Comment thread internal/core/migration/workflow_target_test.go Outdated
Comment thread internal/core/model/preparation_test.go
Comment thread internal/core/model/preparation_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
.gitignore (1)

16-16: Use a directory-specific ignore pattern for clarity.

At Line 16, ai-docs also ignores a file named ai-docs. If the intent is directory-only, prefer ai-docs/ to avoid accidental overmatching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 16, Change the .gitignore entry "ai-docs" to a
directory-specific pattern "ai-docs/" so the rule only ignores the ai-docs
directory (not a file named ai-docs); update the entry referenced as "ai-docs"
in the .gitignore accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/agent/registry_test.go`:
- Around line 587-695: These tests violate the project's t.Run("Should...")
subtest pattern—refactor each standalone test
(TestValidateRuntimeConfigRejectsNilConfig, TestEnsureAvailableRejectsNilConfig,
TestValidateRuntimeConfigAcceptsResolvedPromptTextAsExecPromptSource,
TestBuildShellCommandStringShellEscapesInterpolatedArguments,
TestEnsureAvailableHonorsCallerContext,
TestJoinAvailabilityErrorsPreservesAvailabilityErrorTypes) to use the required
subtest pattern: either convert them into a single table-driven test with
multiple t.Run("Should ...", func(t *testing.T){ ... }) entries or wrap the
existing body inside a t.Run("Should <behavior>", func(t *testing.T){ ... })
call; keep the existing assertions and setup (e.g., calls to
ValidateRuntimeConfig, EnsureAvailable, BuildShellCommandString,
joinAvailabilityErrors, registerTestSpec) unchanged but move them into the
corresponding subtest functions and preserve t.Parallel where appropriate inside
each subtest.
- Around line 590-603: Add a package-level sentinel error (e.g., var
ErrRuntimeConfigNil = errors.New("runtime config is nil")) and replace the
string-based error checks and string-returned errors with that sentinel: change
the error returns in the functions that currently return the "runtime config is
nil" message (refer to the error returns in registry_validate.go and
registry_launch.go) to return ErrRuntimeConfigNil, and update the tests that
call ValidateRuntimeConfig and EnsureAvailable to use errors.Is(err,
ErrRuntimeConfigNil) (or errors.As where appropriate) instead of
strings.Contains(err.Error(), ...); ensure imports include "errors" where added.

---

Nitpick comments:
In @.gitignore:
- Line 16: Change the .gitignore entry "ai-docs" to a directory-specific pattern
"ai-docs/" so the rule only ignores the ai-docs directory (not a file named
ai-docs); update the entry referenced as "ai-docs" in the .gitignore
accordingly.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62449c06-d6a4-41ea-b6b1-52272ad8e4eb

📥 Commits

Reviewing files that changed from the base of the PR and between 72d3043 and c5ea9c7.

⛔ Files ignored due to path filters (35)
  • ai-docs/reviews-pr-60/_summary.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/001-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/002-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/003-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/004-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/005-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/006-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/007-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/008-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/009-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/010-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/issues/011-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/outside/002-outside_01_internal_core_run_execution.go.md is excluded by !**/*.md
  • ai-docs/reviews-pr-60/outside/004-outside_01_internal_core_workspace_config.go.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/_manual-triage.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/_summary.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/001-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/002-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/003-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/004-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/005-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/006-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/007-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/issues/008-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/outside/002-outside_01_internal_core_agent_session.go.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/outside/002-outside_02_internal_core_run_logging.go.md is excluded by !**/*.md
  • ai-docs/reviews-pr-68/outside/003-outside_01_internal_core_run_execution_ui_test.go.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/_summary.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/001-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/002-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/003-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/004-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/005-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/006-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/outside/002-outside_01_internal_core_plan_journal.go.md is excluded by !**/*.md
📒 Files selected for processing (9)
  • .gitignore
  • internal/contentblock/engine.go
  • internal/contentblock/engine_test.go
  • internal/core/agent/registry_launch.go
  • internal/core/agent/registry_test.go
  • internal/core/migration/workflow_target.go
  • internal/core/migration/workflow_target_test.go
  • internal/core/model/preparation_test.go
  • internal/core/plan/journal.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/contentblock/engine.go
  • internal/core/agent/registry_launch.go

Comment thread internal/core/agent/registry_test.go
Comment thread internal/core/agent/registry_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
internal/core/agent/tool_call_name.go (1)

169-179: ⚠️ Potential issue | 🟡 Minor

Redundant condition on line 176 always evaluates to true.

Within this block, refID != "" is already guaranteed true from the check on line 169. The condition extractString(input, "url") != "" || refID != "" on line 176 will always be true if the pattern check on line 173 fails, causing OpenURL to be returned unconditionally.

If the intent is to return OpenURL only when a url key is present, simplify the logic:

🐛 Proposed fix
 	if refID := extractString(input, "ref_id", "refId"); refID != "" {
 		if _, ok := extractInt(input, "id"); ok {
 			return toolNameClick
 		}
 		if extractString(input, "pattern") != "" {
 			return toolNameFind
 		}
-		if extractString(input, "url") != "" || refID != "" {
-			return toolNameOpenURL
-		}
+		return toolNameOpenURL
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 169 - 179, The branch in
the tool name resolver incorrectly uses "extractString(input, \"url\") != \"\"
|| refID != \"\"" which is redundant because refID is already known non-empty
(set by the outer if), causing toolNameOpenURL to be returned unconditionally;
update the final conditional in the block that references refID (the one after
extractInt and extractString("pattern")) to check only for a non-empty URL
(i.e., use extractString(input, "url") != "") so OpenURL is returned only when a
url key is present; keep the existing extractInt, extractString("pattern")
checks and the outer refID check intact and ensure you reference extractString,
extractInt, refID, toolNameClick, toolNameFind, and toolNameOpenURL when making
the change.
🧹 Nitpick comments (5)
internal/core/agent/acp_convert.go (1)

466-471: Consider handling edge cases in diff rendering.

The renderDiffText function generates unified-diff-style output but doesn't escape special characters in path or content. If paths contain newlines or the content doesn't end with a newline, the output may be malformed. This is likely acceptable for display purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/acp_convert.go` around lines 466 - 471, The
renderDiffText function can produce malformed diffs when path contains newlines
or when newText/oldText don't end with a newline; update renderDiffText to
sanitize the path (e.g., replace or escape newline characters and other control
characters in the path parameter) and normalize content by ensuring newText and
*oldText end with a trailing newline (append "\n" if missing) before formatting
the diff; reference the renderDiffText(path string, newText string, oldText
*string) function and apply these transformations to path, newText, and oldText
prior to the fmt.Sprintf calls.
internal/core/agent/tool_call_name.go (1)

219-229: looksLikeWebSearchInput may trigger false positives.

The presence of a url key alone triggers web-search detection, but url is also used for WebFetch and OpenURL tools. This could cause incorrect categorization in normalizeToolNameByKind (line 116-118) for fetch-like inputs.

The current ordering in inferToolNameFromInputShape (line 166 calls this before the url-only check on line 180) mitigates this for that path. However, normalizeToolNameByKind at line 116 may still misclassify a pure fetch URL as web search when kind is ToolKindSearch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/tool_call_name.go` around lines 219 - 229,
looksLikeWebSearchInput currently treats any input with a "url" key as a web
search which causes false positives for fetch-like tools; update the logic so a
lone "url" does not trigger web-search detection — either remove "url" from the
top-level key list in looksLikeWebSearchInput or change the check to require
"url" plus a query-like key (e.g., "query", "queries", "search_query",
"action_query") before returning true; also ensure normalizeToolNameByKind (used
when kind == ToolKindSearch) continues to prefer WebFetch/OpenURL for url-only
inputs if you choose the list change, so inferToolNameFromInputShape,
normalizeToolNameByKind, and looksLikeWebSearchInput remain consistent.
cmd/compozy/main_test.go (1)

10-56: Convert this into a table-driven test.

The three subtests all exercise the same helper and mostly vary by channel setup and expected result, so a small table would remove duplicated harness code and make the next case easier to add.

As per coding guidelines, Use table-driven tests with subtests (t.Run) as the default pattern for Go tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/compozy/main_test.go` around lines 10 - 56, Refactor
TestWaitForUpdateResult into a table-driven test that iterates over cases
describing the channel setup and expected result; keep using subtests via t.Run
for each case, and exercise waitForUpdateResult with inputs such as (1) a
buffered result channel pre-filled with &update.ReleaseInfo{Version:"v1.2.3"}
expecting that pointer, (2) a closed unbuffered channel expecting nil, and (3) a
never-sent channel with a goroutine reading waitForUpdateResult and a timeout
using updateResultWaitTimeout expecting nil; preserve parallelization
(t.Parallel()) appropriately and reference TestWaitForUpdateResult,
waitForUpdateResult, update.ReleaseInfo and updateResultWaitTimeout when
locating the code to change.
cmd/compozy/main.go (1)

41-61: Accept a caller-owned context and explicit completion signal here.

startUpdateCheck creates its own context.Background() and only returns cancel, so the caller cannot tie this goroutine to the root CLI context or wait for it to finish. Since update.CheckForUpdate does real state-file and network I/O, that leaves shutdown implicit.

As per coding guidelines, Pass context.Context as the first argument to all functions crossing runtime boundaries; avoid context.Background() outside main and focused tests, Every goroutine must have explicit ownership and shutdown via context.Context cancellation, and No fire-and-forget goroutines; track all goroutines with sync.WaitGroup or equivalent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/compozy/main.go` around lines 41 - 61, startUpdateCheck currently creates
its own background context and spawns a fire-and-forget goroutine; change its
signature to accept a caller-owned context (e.g., startUpdateCheck(ctx
context.Context, currentVersion string) ...) and create a derived ctx via
context.WithCancel(ctx), pass that ctx into update.CheckForUpdate, and return
the cancel function. Also add an explicit completion signal (e.g., return a done
channel or WaitGroup notification) that the goroutine closes/signals when it
finishes so callers can wait for shutdown; keep usage of update.StateFilePath
and update.CheckForUpdate unchanged except for using the provided ctx and ensure
the goroutine closes the result channel and the done signal on all exit paths.
internal/cli/workspace_config_test.go (1)

426-432: Rename these subtests to the Should... form.

This is the only new case table in the file that does not follow the repo’s subtest naming convention.

As per coding guidelines, MUST use t.Run("Should...") pattern for ALL test cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/workspace_config_test.go` around lines 426 - 432, The subtest
names in the cases table (variable cases containing entries with name and base
referencing migrateState.simpleCommandBase, syncState.simpleCommandBase,
archiveState.simpleCommandBase) must be renamed to the "Should..." form; update
each name string (e.g., change "migrate", "sync", "archive") to descriptive
"Should ..." phrases (e.g., "Should migrate workspace", "Should sync workspace",
"Should archive workspace") so that t.Run uses the required t.Run("Should...")
pattern for all entries using the cases slice and simpleCommandBase references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/compozy/main.go`:
- Around line 31-33: The code currently discards the error from fmt.Fprintln
when writing the update notification; change it to capture and handle the error
from fmt.Fprintln(cmd.ErrOrStderr(), renderUpdateNotification(version.Version,
release)) (called after waitForUpdateResult and renderUpdateNotification) —
assign the return error to a variable, check if err != nil, and then handle it
(for example, write a descriptive error message to stderr via cmd.ErrOrStderr()
or propagate/return the error from main), optionally ignoring benign
broken-pipe/EPIPE errors if appropriate.

In `@internal/cli/state.go`:
- Around line 255-261: The code currently accepts zero or negative durations
from s.timeout because time.ParseDuration allows them; update the parsing logic
in the function that builds the core.Config (where s.timeout is parsed into
timeoutDuration) to validate parsed > 0 and return an error when parsed <= 0,
e.g., after calling time.ParseDuration(s.timeout) check the parsed value and
return fmt.Errorf("invalid timeout: must be > 0") (or similar) rather than using
a non-positive timeoutDuration; keep the variable names timeoutDuration,
s.timeout and the time.ParseDuration call so the change is localized.
- Around line 226-252: The interactivity gate in maybeCollectInteractiveParams
blocks the exec command when a positional prompt exists because
cmd.Flags().NFlag() only counts flags; update prepareAndRun/exec flow so
resolveExecPromptSource is called (or positional prompt presence is checked)
before calling maybeCollectInteractiveParams, or alter
maybeCollectInteractiveParams to skip the interactive-terminal check when
resolveExecPromptSource (or a positional prompt) indicates a provided prompt;
specifically locate prepareAndRun and the exec path that calls
resolveExecPromptSource and reorder or add a conditional that if
resolveExecPromptSource returns a valid prompt (or if positional args exist)
then do not enforce the isInteractive() check that uses cmd.Flags().NFlag().
- Around line 371-423: The method resolveExecPromptSource leaves s.promptFile
from a prior run which can make the exclusivity check or behavior incorrect;
ensure s.promptFile is cleared unless the current chosen source is the
prompt-file. Specifically, after computing local promptFile :=
strings.TrimSpace(s.promptFile) (and before the sourceCount logic) or
immediately when taking the positionalPrompt or stdin branch, set s.promptFile =
"" so stale state is removed; when the prompt-file branch wins keep the existing
assignment s.promptFile = promptFile as currently done. This guarantees
s.promptFile only reflects the active source and prevents false positives in the
exclusivity check.

---

Duplicate comments:
In `@internal/core/agent/tool_call_name.go`:
- Around line 169-179: The branch in the tool name resolver incorrectly uses
"extractString(input, \"url\") != \"\" || refID != \"\"" which is redundant
because refID is already known non-empty (set by the outer if), causing
toolNameOpenURL to be returned unconditionally; update the final conditional in
the block that references refID (the one after extractInt and
extractString("pattern")) to check only for a non-empty URL (i.e., use
extractString(input, "url") != "") so OpenURL is returned only when a url key is
present; keep the existing extractInt, extractString("pattern") checks and the
outer refID check intact and ensure you reference extractString, extractInt,
refID, toolNameClick, toolNameFind, and toolNameOpenURL when making the change.

---

Nitpick comments:
In `@cmd/compozy/main_test.go`:
- Around line 10-56: Refactor TestWaitForUpdateResult into a table-driven test
that iterates over cases describing the channel setup and expected result; keep
using subtests via t.Run for each case, and exercise waitForUpdateResult with
inputs such as (1) a buffered result channel pre-filled with
&update.ReleaseInfo{Version:"v1.2.3"} expecting that pointer, (2) a closed
unbuffered channel expecting nil, and (3) a never-sent channel with a goroutine
reading waitForUpdateResult and a timeout using updateResultWaitTimeout
expecting nil; preserve parallelization (t.Parallel()) appropriately and
reference TestWaitForUpdateResult, waitForUpdateResult, update.ReleaseInfo and
updateResultWaitTimeout when locating the code to change.

In `@cmd/compozy/main.go`:
- Around line 41-61: startUpdateCheck currently creates its own background
context and spawns a fire-and-forget goroutine; change its signature to accept a
caller-owned context (e.g., startUpdateCheck(ctx context.Context, currentVersion
string) ...) and create a derived ctx via context.WithCancel(ctx), pass that ctx
into update.CheckForUpdate, and return the cancel function. Also add an explicit
completion signal (e.g., return a done channel or WaitGroup notification) that
the goroutine closes/signals when it finishes so callers can wait for shutdown;
keep usage of update.StateFilePath and update.CheckForUpdate unchanged except
for using the provided ctx and ensure the goroutine closes the result channel
and the done signal on all exit paths.

In `@internal/cli/workspace_config_test.go`:
- Around line 426-432: The subtest names in the cases table (variable cases
containing entries with name and base referencing
migrateState.simpleCommandBase, syncState.simpleCommandBase,
archiveState.simpleCommandBase) must be renamed to the "Should..." form; update
each name string (e.g., change "migrate", "sync", "archive") to descriptive
"Should ..." phrases (e.g., "Should migrate workspace", "Should sync workspace",
"Should archive workspace") so that t.Run uses the required t.Run("Should...")
pattern for all entries using the cases slice and simpleCommandBase references.

In `@internal/core/agent/acp_convert.go`:
- Around line 466-471: The renderDiffText function can produce malformed diffs
when path contains newlines or when newText/oldText don't end with a newline;
update renderDiffText to sanitize the path (e.g., replace or escape newline
characters and other control characters in the path parameter) and normalize
content by ensuring newText and *oldText end with a trailing newline (append
"\n" if missing) before formatting the diff; reference the renderDiffText(path
string, newText string, oldText *string) function and apply these
transformations to path, newText, and oldText prior to the fmt.Sprintf calls.

In `@internal/core/agent/tool_call_name.go`:
- Around line 219-229: looksLikeWebSearchInput currently treats any input with a
"url" key as a web search which causes false positives for fetch-like tools;
update the logic so a lone "url" does not trigger web-search detection — either
remove "url" from the top-level key list in looksLikeWebSearchInput or change
the check to require "url" plus a query-like key (e.g., "query", "queries",
"search_query", "action_query") before returning true; also ensure
normalizeToolNameByKind (used when kind == ToolKindSearch) continues to prefer
WebFetch/OpenURL for url-only inputs if you choose the list change, so
inferToolNameFromInputShape, normalizeToolNameByKind, and
looksLikeWebSearchInput remain consistent.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43459a78-dad3-463c-bbd8-729d097922b5

📥 Commits

Reviewing files that changed from the base of the PR and between c5ea9c7 and 322ea69.

⛔ Files ignored due to path filters (1)
  • .codex/ledger/2026-04-07-MEMORY-review-nitpicks.md is excluded by !**/*.md
📒 Files selected for processing (15)
  • cmd/compozy/main.go
  • cmd/compozy/main_test.go
  • internal/cli/kernel_dispatch_test.go
  • internal/cli/root_test.go
  • internal/cli/run.go
  • internal/cli/state.go
  • internal/cli/workspace_config_test.go
  • internal/core/agent/acp_convert.go
  • internal/core/agent/client.go
  • internal/core/agent/session_helpers_test.go
  • internal/core/agent/tool_call_input.go
  • internal/core/agent/tool_call_name.go
  • internal/core/kernel/core_adapters_test.go
  • internal/core/kernel/deps_test.go
  • internal/core/model/preparation.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/core/agent/client.go
  • internal/core/agent/session_helpers_test.go
  • internal/cli/root_test.go
  • internal/cli/run.go

Comment thread cmd/compozy/main.go
Comment thread internal/cli/state.go
Comment thread internal/cli/state.go
Comment thread internal/cli/state.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/core/agent/registry_launch.go (1)

74-85: ⚠️ Potential issue | 🟠 Major

Still use the effective model for availability probes.

Line 81 still hardcodes spec.DefaultModel, so EnsureAvailable never validates a caller-supplied cfg.Model. If bootstrap args depend on the model, the probe can pass and the real launch can still fail later.

💡 Suggested fix
 	spec, err := lookupAgentSpec(cfg.IDE)
 	if err != nil {
 		return err
 	}
+	effectiveModel := resolveModel(spec, cfg.Model)
 	if _, err := resolveLaunchCommand(
 		ctx,
 		spec,
-		spec.DefaultModel,
+		effectiveModel,
 		cfg.ReasoningEffort,
 		cfg.AddDirs,
 		cfg.AccessMode,
 		true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/registry_launch.go` around lines 74 - 85, Ensure the
availability probe uses the caller-specified effective model instead of always
passing spec.DefaultModel: inside the EnsureAvailable / registry launch flow
replace the hardcoded spec.DefaultModel argument to resolveLaunchCommand with
the effective model from the config (e.g., cfg.Model or a computed
effectiveModel derived from cfg and spec) so that resolveLaunchCommand (called
in EnsureAvailable) validates the same model the real launch will use.
🧹 Nitpick comments (3)
internal/core/agent/registry_launch.go (1)

141-149: Wrap the LookPath failure instead of replacing it.

Line 148 drops the original exec.LookPath error and hardcodes a “not found” message, so callers lose errors.Is/As against the real cause and can also get a misleading message for non-PATH failures.

💡 Suggested fix
 	if _, err := exec.LookPath(command[0]); err != nil {
 		return &AvailabilityError{
 			IDE:         spec.ID,
 			DisplayName: spec.DisplayName,
 			Command:     command,
 			DocsURL:     spec.DocsURL,
 			InstallHint: spec.InstallHint,
-			Cause:       fmt.Errorf("command %q was not found on PATH", command[0]),
+			Cause:       fmt.Errorf("resolving command %q on PATH: %w", command[0], err),
 		}
 	}

As per coding guidelines, "Prefer explicit error returns with wrapped context using fmt.Errorf("context: %w", err). Use errors.Is() and errors.As() for error matching; do not compare error strings."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/registry_launch.go` around lines 141 - 149, The current
LookPath error is being replaced with a hardcoded message which loses the
original error context; update the code in registry_launch.go (the exec.LookPath
check) to wrap the original err when constructing the AvailabilityError Cause
(use fmt.Errorf("command %q: %w", command[0], err) or equivalent) so callers can
use errors.Is/errors.As and see the actual underlying failure while preserving
the descriptive "command X" context in the AvailabilityError returned by the
function that constructs AvailabilityError.
internal/core/agent/acp_convert.go (1)

15-20: Wrap helper failures with branch context.

These early returns pass helper errors through unchanged, so a failure loses whether it came from message conversion or tool-lifecycle conversion.

Suggested change
 func convertACPUpdate(driverID string, update acp.SessionUpdate) (model.SessionUpdate, error) {
 	if converted, ok, err := convertACPMessageUpdate(update); ok || err != nil {
-		return converted, err
+		if err != nil {
+			return model.SessionUpdate{}, fmt.Errorf("convert ACP message update: %w", err)
+		}
+		return converted, nil
 	}
 	if converted, ok, err := convertACPToolLifecycleUpdate(driverID, update); ok || err != nil {
-		return converted, err
+		if err != nil {
+			return model.SessionUpdate{}, fmt.Errorf("convert ACP tool lifecycle update: %w", err)
+		}
+		return converted, nil
 	}

As per coding guidelines, "Prefer explicit error returns with wrapped context using fmt.Errorf("context: %w", err) in Go code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/acp_convert.go` around lines 15 - 20, The helper errors
from convertACPMessageUpdate and convertACPToolLifecycleUpdate must be returned
with contextual wrapping instead of passed through; change the early-return
logic to first check ok and return the converted value when ok is true, and if
err != nil return nil, fmt.Errorf("message conversion failed: %w", err) for
convertACPMessageUpdate (and similarly fmt.Errorf("tool-lifecycle conversion
failed: %w", err) for convertACPToolLifecycleUpdate), ensuring you import fmt if
needed and preserve the original converted return when ok is true.
internal/cli/state.go (1)

311-314: Wrap persisted-run load failures with local context.

The bare return err at line 313 drops which --run-id was being resumed, making CLI errors harder to diagnose. The file already uses fmt.Errorf consistently elsewhere for error context.

🩹 Suggested change
 	record, err := coreRun.LoadPersistedExecRun(s.workspaceRoot, s.runID)
 	if err != nil {
-		return err
+		return fmt.Errorf("load persisted exec run %q: %w", s.runID, err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/state.go` around lines 311 - 314, Wrap the error returned from
coreRun.LoadPersistedExecRun with contextual information about which run was
being resumed: replace the bare return err after calling
coreRun.LoadPersistedExecRun(s.workspaceRoot, s.runID) with a fmt.Errorf that
includes s.runID (and optionally s.workspaceRoot) and uses %w to wrap the
original err so callers retain the original error while gaining context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cli/root_test.go`:
- Around line 836-848: Replace the string-based error check with an errors.Is
assertion against a sentinel error: introduce a package-level sentinel variable
(e.g., var brokenStdout = errors.New("broken stdout")), use that sentinel when
constructing the failOnWriteWriter in the test (failOnWriteWriter{err:
brokenStdout}), call state.fetchReviews(cmd, nil) as before, then assert
errors.Is(err, brokenStdout) instead of checking err.Error() for "write fetch
summary"; keep the existing check for the wrapper message if you still want to
validate the outer text but ensure the underlying writer error is asserted via
errors.Is.

In `@internal/core/agent/registry_launch.go`:
- Around line 186-192: The shell-escaping logic in formatShellArg incorrectly
substitutes embedded single quotes with '\"'\"' which is not valid POSIX syntax;
update the replacement so embedded "'" are escaped using the correct pattern
'"'\'"'" (i.e. replace occurrences of "'" with `'"'"'`) so the function returns
a valid single-quoted string like "'" + strings.ReplaceAll(arg, "'", `'"'"'`) +
"'" and ensures inputs like O'Connor produce a well-formed quoted argument.

---

Duplicate comments:
In `@internal/core/agent/registry_launch.go`:
- Around line 74-85: Ensure the availability probe uses the caller-specified
effective model instead of always passing spec.DefaultModel: inside the
EnsureAvailable / registry launch flow replace the hardcoded spec.DefaultModel
argument to resolveLaunchCommand with the effective model from the config (e.g.,
cfg.Model or a computed effectiveModel derived from cfg and spec) so that
resolveLaunchCommand (called in EnsureAvailable) validates the same model the
real launch will use.

---

Nitpick comments:
In `@internal/cli/state.go`:
- Around line 311-314: Wrap the error returned from coreRun.LoadPersistedExecRun
with contextual information about which run was being resumed: replace the bare
return err after calling coreRun.LoadPersistedExecRun(s.workspaceRoot, s.runID)
with a fmt.Errorf that includes s.runID (and optionally s.workspaceRoot) and
uses %w to wrap the original err so callers retain the original error while
gaining context.

In `@internal/core/agent/acp_convert.go`:
- Around line 15-20: The helper errors from convertACPMessageUpdate and
convertACPToolLifecycleUpdate must be returned with contextual wrapping instead
of passed through; change the early-return logic to first check ok and return
the converted value when ok is true, and if err != nil return nil,
fmt.Errorf("message conversion failed: %w", err) for convertACPMessageUpdate
(and similarly fmt.Errorf("tool-lifecycle conversion failed: %w", err) for
convertACPToolLifecycleUpdate), ensuring you import fmt if needed and preserve
the original converted return when ok is true.

In `@internal/core/agent/registry_launch.go`:
- Around line 141-149: The current LookPath error is being replaced with a
hardcoded message which loses the original error context; update the code in
registry_launch.go (the exec.LookPath check) to wrap the original err when
constructing the AvailabilityError Cause (use fmt.Errorf("command %q: %w",
command[0], err) or equivalent) so callers can use errors.Is/errors.As and see
the actual underlying failure while preserving the descriptive "command X"
context in the AvailabilityError returned by the function that constructs
AvailabilityError.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f654462e-86da-4fd0-88fe-61e9cd484e44

📥 Commits

Reviewing files that changed from the base of the PR and between 322ea69 and 735a9ce.

⛔ Files ignored due to path filters (15)
  • .agents/skills/fix-coderabbit-review/SKILL.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/fix-coderabbit-review/scripts/pr-review.ts is excluded by !.agents/**
  • .agents/skills/fix-coderabbit-review/scripts/pr_review.py is excluded by !.agents/**
  • .agents/skills/fix-coderabbit-review/scripts/resolve_pr_issues.py is excluded by !.agents/**
  • .agents/skills/fix-coderabbit-review/scripts/resolve_pr_issues.sh is excluded by !.agents/**
  • .codex/ledger/2026-04-07-MEMORY-pr70-coderabbit.md is excluded by !**/*.md
  • .codex/ledger/2026-04-07-MEMORY-review-fixes.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/_summary.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/001-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/002-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/003-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/004-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/005-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/issues/006-issue.md is excluded by !**/*.md
  • ai-docs/reviews-pr-70/outside/002-outside_01_internal_core_plan_journal.go.md is excluded by !**/*.md
📒 Files selected for processing (12)
  • cmd/compozy/main.go
  • cmd/compozy/main_test.go
  • internal/cli/root_test.go
  • internal/cli/state.go
  • internal/cli/workspace_config_test.go
  • internal/core/agent/acp_convert.go
  • internal/core/agent/registry_launch.go
  • internal/core/agent/registry_test.go
  • internal/core/agent/registry_validate.go
  • internal/core/agent/session_helpers_test.go
  • internal/core/agent/tool_call_name.go
  • internal/core/agent/tool_call_name_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/compozy/main.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/compozy/main_test.go
  • internal/cli/workspace_config_test.go

Comment thread internal/cli/root_test.go
Comment on lines +836 to +848
cmd := newTestCommand(state)
cmd.SetOut(&failOnWriteWriter{err: errors.New("broken stdout")})
if err := cmd.Flags().Set("dry-run", "true"); err != nil {
t.Fatalf("set dry-run flag: %v", err)
}

err := state.fetchReviews(cmd, nil)
if err == nil {
t.Fatal("expected fetchReviews to return a write error")
}
if !strings.Contains(err.Error(), "write fetch summary") {
t.Fatalf("unexpected error: %v", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
from pathlib import Path
text = Path("internal/cli/root_test.go").read_text()
start = text.index("func TestFetchReviewsReturnsWriterErrors")
end = text.index("func TestFormInputsApplyForFetchWorkflow")
block = text[start:end]
print(block)
print("\nasserts wrapped cause:", "errors.Is(" in block or "errors.As(" in block)
PY

Repository: compozy/compozy

Length of output: 900


Assert the wrapped writer error using errors.Is().

The test validates only the wrapper message text but misses the underlying error. This means the test would still pass if fetchReviews stops wrapping the original write failure, defeating the purpose of the test.

🧪 Suggested change
-	cmd.SetOut(&failOnWriteWriter{err: errors.New("broken stdout")})
+	writeErr := errors.New("broken stdout")
+	cmd.SetOut(&failOnWriteWriter{err: writeErr})
 	if err := cmd.Flags().Set("dry-run", "true"); err != nil {
 		t.Fatalf("set dry-run flag: %v", err)
 	}
 
 	err := state.fetchReviews(cmd, nil)
 	if err == nil {
 		t.Fatal("expected fetchReviews to return a write error")
 	}
+	if !errors.Is(err, writeErr) {
+		t.Fatalf("expected wrapped writer error, got %v", err)
+	}
 	if !strings.Contains(err.Error(), "write fetch summary") {
 		t.Fatalf("unexpected error: %v", err)
 	}

Per guidelines, use errors.Is() for error matching instead of string comparison, and test code must include specific error assertions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cmd := newTestCommand(state)
cmd.SetOut(&failOnWriteWriter{err: errors.New("broken stdout")})
if err := cmd.Flags().Set("dry-run", "true"); err != nil {
t.Fatalf("set dry-run flag: %v", err)
}
err := state.fetchReviews(cmd, nil)
if err == nil {
t.Fatal("expected fetchReviews to return a write error")
}
if !strings.Contains(err.Error(), "write fetch summary") {
t.Fatalf("unexpected error: %v", err)
}
cmd := newTestCommand(state)
writeErr := errors.New("broken stdout")
cmd.SetOut(&failOnWriteWriter{err: writeErr})
if err := cmd.Flags().Set("dry-run", "true"); err != nil {
t.Fatalf("set dry-run flag: %v", err)
}
err := state.fetchReviews(cmd, nil)
if err == nil {
t.Fatal("expected fetchReviews to return a write error")
}
if !errors.Is(err, writeErr) {
t.Fatalf("expected wrapped writer error, got %v", err)
}
if !strings.Contains(err.Error(), "write fetch summary") {
t.Fatalf("unexpected error: %v", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/root_test.go` around lines 836 - 848, Replace the string-based
error check with an errors.Is assertion against a sentinel error: introduce a
package-level sentinel variable (e.g., var brokenStdout = errors.New("broken
stdout")), use that sentinel when constructing the failOnWriteWriter in the test
(failOnWriteWriter{err: brokenStdout}), call state.fetchReviews(cmd, nil) as
before, then assert errors.Is(err, brokenStdout) instead of checking err.Error()
for "write fetch summary"; keep the existing check for the wrapper message if
you still want to validate the outer text but ensure the underlying writer error
is asserted via errors.Is.

Comment on lines +186 to +192
func formatShellArg(arg string) string {
if arg == "" {
return `''`
}
if strings.ContainsAny(arg, " \t\n\"'\\$`|&;<>*?[]{}()") {
return "'" + strings.ReplaceAll(arg, "'", `'\"'\"'`) + "'"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 <<'PY'
import subprocess

arg = "O'Connor"
quoted = "'" + arg.replace("'", r"'\"'\"'") + "'"
print("generated:", quoted)

proc = subprocess.run(
    ["sh", "-n", "-c", f"printf '%s\\n' {quoted}"],
    capture_output=True,
    text=True,
)
print("status:", proc.returncode)
print("stderr:", proc.stderr.strip())
PY

Repository: compozy/compozy

Length of output: 152


🏁 Script executed:

fd -t f registry_launch.go

Repository: compozy/compozy

Length of output: 97


🏁 Script executed:

cat -n internal/core/agent/registry_launch.go | sed -n '180,200p'

Repository: compozy/compozy

Length of output: 774


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 <<'PY'
import subprocess

# Test the current (broken) implementation
arg = "O'Connor"
quoted_current = "'" + arg.replace("'", r"'\"'\"'") + "'"
print("Current implementation:")
print(f"  generated: {quoted_current}")
proc = subprocess.run(
    ["sh", "-n", "-c", f"printf '%s\\n' {quoted_current}"],
    capture_output=True,
    text=True,
)
print(f"  status: {proc.returncode}")
if proc.stderr:
    print(f"  stderr: {proc.stderr.strip()}")

# Test the suggested fix
quoted_fixed = "'" + arg.replace("'", '\'"\'"\'') + "'"
print("\nSuggested fix (using '\"'\"'):")
print(f"  generated: {quoted_fixed}")
proc = subprocess.run(
    ["sh", "-n", "-c", f"printf '%s\\n' {quoted_fixed}"],
    capture_output=True,
    text=True,
)
print(f"  status: {proc.returncode}")
if proc.stderr:
    print(f"  stderr: {proc.stderr.strip()}")

# Also verify the actual output for the fixed version
proc = subprocess.run(
    ["sh", "-c", f"printf '%s\\n' {quoted_fixed}"],
    capture_output=True,
    text=True,
)
print(f"  output: {proc.stdout.strip()}")
PY

Repository: compozy/compozy

Length of output: 271


Fix single-quote escaping in shell previews.

Line 191 emits '\"'\"' for embedded single quotes, which is not valid POSIX shell syntax. Inputs like O'Connor produce an unterminated quoted string error, making the preview command unusable.

The correct escaping pattern is '"'"', which terminates the single-quoted string, adds an escaped single quote in double quotes, then resumes the single-quoted string.

Suggested fix
 	if strings.ContainsAny(arg, " \t\n\"'\\$`|&;<>*?[]{}()") {
-		return "'" + strings.ReplaceAll(arg, "'", `'\"'\"'`) + "'"
+		return "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'"
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func formatShellArg(arg string) string {
if arg == "" {
return `''`
}
if strings.ContainsAny(arg, " \t\n\"'\\$`|&;<>*?[]{}()") {
return "'" + strings.ReplaceAll(arg, "'", `'\"'\"'`) + "'"
}
func formatShellArg(arg string) string {
if arg == "" {
return `''`
}
if strings.ContainsAny(arg, " \t\n\"'\\$`|&;<>*?[]{}()") {
return "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'"
}
return arg
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/agent/registry_launch.go` around lines 186 - 192, The
shell-escaping logic in formatShellArg incorrectly substitutes embedded single
quotes with '\"'\"' which is not valid POSIX syntax; update the replacement so
embedded "'" are escaped using the correct pattern '"'\'"'" (i.e. replace
occurrences of "'" with `'"'"'`) so the function returns a valid single-quoted
string like "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'" and ensures inputs
like O'Connor produce a well-formed quoted argument.

@pedronauck
pedronauck merged commit 904341d into main Apr 7, 2026
6 checks passed
@pedronauck
pedronauck deleted the pn/refac branch April 7, 2026 18:19
This was referenced Apr 17, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Apr 18, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Apr 19, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Apr 21, 2026
This was referenced Apr 27, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jun 19, 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.

1 participant