Skip to content

feat: add extensability - #80

Merged
pedronauck merged 24 commits into
mainfrom
ext
Apr 11, 2026
Merged

pedronauck merged 24 commits into
mainfrom
ext

Conversation

@pedronauck

@pedronauck pedronauck commented Apr 11, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Complete extension platform: discovery, enablement store, install/uninstall, enable/disable, inspect, list, doctor, runtime manager, hook dispatch, host API services, and audit logging.
    • CLI: new ext subcommands and an exec flag to opt into executable extensions.
    • Declarative asset bootstrap and extension-provided provider/skill-pack support.
  • Bug Fixes

    • Improved lifecycle, timeout, and error handling across extension flows.
  • Chores

    • Updated editorconfig, .gitignore, Makefile publish targets, and added pre-commit lint hook.

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

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1058a36f-daa9-4e9e-9ce1-985030844f2a

📥 Commits

Reviewing files that changed from the base of the PR and between 75bb2e8 and ce91b6b.

⛔ Files ignored due to path filters (21)
  • .compozy/tasks/ext-ideas/research/analysis.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/analysis_broader_ecosystem.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/analysis_claude_code.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/analysis_hermes_agent.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/analysis_openclaw.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/analysis_pi_mono.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_cicd.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_cloud_ai.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_code_quality.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_communication.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_issue_trackers.md is excluded by !**/*.md
  • .compozy/tasks/ext-ideas/research/integrations_observability.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/_meta.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-002/issue_007.md is excluded by !**/*.md
📒 Files selected for processing (7)
  • internal/cli/makefile_publish_test.go
  • internal/core/agent/client.go
  • internal/core/agent/client_test.go
  • internal/core/agent/registry_overlay.go
  • internal/core/agent/registry_overlay_test.go
  • internal/core/extension/dispatcher_test.go
  • internal/core/extension/ts_template_manager_integration_test.go

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

Adds a full executable extensions subsystem: CLI management (ext commands), multi-source discovery and enablement persistence, declarative asset bootstrap, hook dispatching and host API routing with capability checks and auditing, extension manager/runtime overlays, agent/subprocess updates, and extensive tests.

Changes

Cohort / File(s) Summary
Repo config & CI
\.editorconfig, \.gitignore, \.husky/pre-commit, Makefile, compozy.go
Editorconfig rules updated; new ignores added; pre-commit hook runs lint-staged via bunx; Makefile gained build/publish targets for extension SDKs and help text; blank-import added for extension init side-effects.
CLI: extension commands
internal/cli/extension/root.go, .../display.go, .../install.go, .../enablement.go, .../doctor.go, .../enablement_test.go, .../display_test.go, .../install_test.go, .../doctor_test.go
Implements compozy ext group and subcommands (list, inspect, install, uninstall, enable, disable, doctor) with discovery, enablement, install/uninstall, diagnostics, and tests.
CLI core & exec wiring
internal/cli/commands.go, commands_test.go, state.go, root.go, testdata/exec_help.golden, root_command_execution_test.go, root_test.go
Adds --extensions flag to exec, wires extensionsEnabled into command config/state, registers ext command, updates help golden, and adds integration tests for exec/start behaviors with extensions.
Declarative bootstrap & skills preflight
internal/cli/extensions_bootstrap.go, extensions_bootstrap_test.go, run.go, skills_preflight.go, skills_preflight_test.go
Introduces declarative asset bootstrap for providers/skill packs; integrates bootstrap into prepare/run and fetchReviews; unifies bundled+extension skill verification/refresh; adds tests.
Discovery & assets extraction
internal/core/extension/discovery.go, discovery_bundled.go, assets.go, assets_test.go, discovery_test.go
Multi-source discovery (bundled embed, user, workspace), manifest loading and precedence, provider/skill-pack extraction with deterministic sorting, failure records, and tests.
Enablement persistence
internal/core/extension/enablement.go, enablement_test.go
Persistent per-source enablement store (Ref keyed) with Load/Save/Enable/Disable semantics and tests validating defaults, persistence, and error handling.
Runtime model & registry
internal/core/extension/chain.go, doc.go
RuntimeExtension type, ExtensionCaller interface, ExtensionState, Registry with deterministic ordering, hook-chain builder utilities.
Hook dispatcher
internal/core/extension/dispatcher.go, dispatcher_test.go
HookDispatcher implementing sequential mutable dispatch with patch merging, observer fan-out, audit recording, timeouts and error handling; comprehensive tests.
Capability checks
internal/core/extension/capability.go, capability_test.go
CapabilityChecker, host-method/hook capability mapping, structured denial errors serializable to request errors, and tests.
Audit logging
internal/core/extension/audit.go, audit_test.go
Append-only JSONL AuditLogger with Open/Record/Close, validation/normalization, concurrency and close semantics, and tests including crash scenarios.
Host API & errors
internal/core/extension/host_api.go, host_api_errors.go, host_api_test.go
HostAPIRouter with namespace service registration, lifecycle/state gating, capability enforcement, audit recording, standardized request-error helpers, and tests.
Host services & kernel ops
internal/core/extension/host_helpers.go, host_helpers_test.go
KernelOps interface and default implementation, HostServices, host service registration, prompt rendering, event subscribe/publish, path scoping, and tests.
Host reads (tasks/memory/artifacts)
internal/core/extension/host_reads.go, host_reads_test.go
Handlers and default read ops for tasks/memory/artifacts with scoping, parsing, error handling, and tests.
Host writes (tasks/runs/memory/artifacts)
internal/core/extension/host_writes.go, host_writes_test.go
Write handlers and default ops (CreateTask, StartRun, WriteMemory, WriteArtifact), artifact pre/post hooks, atomic writes, events emission, recursion guards, and tests.
Manager & lifecycle
internal/core/extension/manager.go, manager_constants.go
Extensions Manager startup/shutdown, lifecycle events, dispatch bridging to HookDispatcher, failure handling, and helpers.
Agent / subprocess & registry overlays
internal/core/agent/client.go, hooks.go, session.go, process_unix.go (removed functions), registry_overlay.go, registry_specs.go, registry_launch.go, tests
Session request JSON tags and marshal logic; pre-create/resume hook dispatch; subprocess refactor to subprocess.Process; overlay activation for runtime providers; registry snapshot usage and SetupAgentName; related tests updated/added.
Extension supporting types & tests
multiple internal/core/extension/* (assets, discovery, audit, enablement, tests, docs)
Exported types/functions added for DeclaredProviders/SkillPacks, Discovery types, Audit API, Enablement Ref/state/store, and extensive unit/integration tests across these modules.
Core API changes
internal/core/api.go
Adds Config.EnableExecutableExtensions, opens a run scope for prepare, updates execute calls to pass event bus/runtime manager, and ensures journal closure with timeout-aware cleanup.
Repo tests
internal/cli/makefile_publish_test.go
Adds Makefile-target test ensuring publish-extension-sdks depends on verification and publishes both SDK workspaces.

Sequence Diagram(s)

sequenceDiagram
    participant CLI
    participant Discovery
    participant Enablement
    participant Registry
    participant Manager
    participant Dispatcher
    participant Extension
    participant HostAPI
    CLI->>Discovery: Discover(roots)
    Discovery->>Enablement: Load enablement for refs
    Enablement-->>Discovery: enabled/disabled
    Discovery-->>CLI: DiscoveredExtensions + DeclaredAssets
    CLI->>Registry: Register RuntimeExtension(s)
    Registry->>Manager: Snapshot extensions
    Manager->>Extension: Start() (initialize subprocesses)
    Extension-->>Manager: started / ready
    CLI->>Dispatcher: DispatchMutable(hook, payload)
    Dispatcher->>Registry: buildHookChain(hook)
    loop per extension in chain
      Dispatcher->>Extension: Call(execute_hook, payload)
      Extension-->>Dispatcher: patch/result
      Dispatcher->>Dispatcher: applyHookPatch()
    end
    Dispatcher-->>CLI: final payload
    Extension->>HostAPI: host.* RPC (Handle)
    HostAPI->>Registry: capability check
    HostAPI-->>Extension: response/result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: add extensability #80 — Implements overlapping extensibility subsystem changes (extension discovery, host API, dispatcher/manager, CLI ext commands); strong code-level overlap.
  • feat: use acp instead of stream raw json #34 — Related agent/subprocess and session management changes touching internal/core/agent/* (session requests, subprocess lifecycle and hooks).
  • feat: agents spec #78 — Related edits for agent/session and client-side hook integration that intersect with registry/agent wiring introduced here.
✨ 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 ext

@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

🧹 Nitpick comments (10)
internal/core/agent/session_helpers_test.go (1)

575-577: Consider relocating this assertion to the subprocess package tests.

This test now validates subprocess.NormalizeWaitError behavior, which is defined in a different package. Testing external package behavior here creates coupling and may result in duplicate test coverage. Consider whether this assertion belongs in internal/core/subprocess tests instead.

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

In `@internal/core/agent/session_helpers_test.go` around lines 575 - 577, The test
in session_helpers_test.go is asserting behavior of
subprocess.NormalizeWaitError (an API from the subprocess package) which couples
core tests to external-package implementation; move the assertion into a new or
existing test in the internal/core/subprocess package tests and remove it from
session_helpers_test.go. Specifically, create/update a test that exercises
subprocess.NormalizeWaitError with the same inputs (e.g., call
NormalizeWaitError("wait for ACP agent process", nil) and assert nil) and keep
session_helpers_test.go focused on only core/session helpers rather than
subprocess internals.
internal/core/extension/enablement_test.go (1)

10-62: Add t.Parallel() to enable concurrent test execution.

Top-level test functions are missing t.Parallel() calls. Since these tests use t.TempDir() for isolation, they can safely run in parallel.

♻️ Proposed fix
 func TestEnablementStoreDefaults(t *testing.T) {
+	t.Parallel()
+
 	homeDir := t.TempDir()

Apply similarly to TestEnablementStorePersistsRoundTrip, TestEnablementStoreRejectsBundledMutations, TestNewEnablementStoreResolvesHomeDir, TestEnablementStoreRejectsInvalidReferences, and TestEnablementStoreRejectsCorruptState.

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

In `@internal/core/extension/enablement_test.go` around lines 10 - 62, Add
t.Parallel() as the first statement in each top-level test function
(TestEnablementStoreDefaults, TestEnablementStorePersistsRoundTrip,
TestEnablementStoreRejectsBundledMutations,
TestNewEnablementStoreResolvesHomeDir,
TestEnablementStoreRejectsInvalidReferences,
TestEnablementStoreRejectsCorruptState) to allow safe concurrent execution; for
table-driven tests like TestEnablementStoreDefaults also call t.Parallel() at
the start of each subtest closure inside t.Run so individual cases run in
parallel while still using t.TempDir() isolation.
internal/core/extension/assets_test.go (1)

8-8: Consider adding t.Parallel() for independent tests.

Both test functions appear to be independent and could benefit from parallel execution. As per coding guidelines, independent subtests should use t.Parallel().

♻️ Suggested fix
 func TestExtractDeclaredProvidersGroupsByCategory(t *testing.T) {
+	t.Parallel()
+
 	entry := DiscoveredExtension{
 func TestExtractDeclaredSkillPacksResolvesAbsolutePaths(t *testing.T) {
+	t.Parallel()
+
 	extensionDir := t.TempDir()

Also applies to: 51-51

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

In `@internal/core/extension/assets_test.go` at line 8, Add t.Parallel() at the
start of each independent test function (e.g.,
TestExtractDeclaredProvidersGroupsByCategory and the other test around the
51-range) so they run concurrently; locate the test functions by their names and
insert a single call to t.Parallel() immediately after the function opening
brace to mark them as parallelizable.
internal/cli/run.go (1)

120-124: Consider using a pointer instead of variadic for optional single argument.

The variadic signature assets ...declarativeAssets is unusual when only 0 or 1 asset is ever expected. A pointer *declarativeAssets would express intent more clearly.

♻️ Alternative signature
-func (s *commandState) runPrepared(
-	ctx context.Context,
-	cmd *cobra.Command,
-	cfg core.Config,
-	assets ...declarativeAssets,
-) error {
-	var discovery declarativeAssets
-	if len(assets) > 0 {
-		discovery = assets[0]
-	}
+func (s *commandState) runPrepared(
+	ctx context.Context,
+	cmd *cobra.Command,
+	cfg core.Config,
+	assets *declarativeAssets,
+) error {
+	var discovery declarativeAssets
+	if assets != nil {
+		discovery = *assets
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/run.go` around lines 120 - 124, The runPrepared function uses a
variadic parameter assets ...declarativeAssets although only 0 or 1 asset is
ever expected; change the signature to accept a single pointer
*declarativeAssets (e.g., func (s *commandState) runPrepared(ctx
context.Context, cmd *cobra.Command, cfg core.Config, assets
*declarativeAssets)), update the function body to treat assets as possibly nil
instead of indexing the slice, and update all callers of runPrepared to pass
either nil or &asset accordingly; ensure any code that relied on assets
length/iteration is adjusted to handle a single optional value.
internal/core/extension/host_reads_test.go (1)

163-166: Consider extracting the compaction threshold to a named constant.

The magic number 180 for triggering compaction should ideally reference the same constant used in production code to prevent tests from drifting out of sync with the actual threshold.

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

In `@internal/core/extension/host_reads_test.go` around lines 163 - 166, Replace
the magic number 180 in the test loop that builds lines (the builder loop in
host_reads_test.go) with the production compaction threshold constant instead of
a literal; reference the actual constant used by the compaction logic (e.g.,
CompactionThreshold or the appropriately named constant from the package that
implements compaction) by importing that package and using that symbol, or if
the constant is unexported, add an exported alias in the production package and
use it here so the test stays in sync with production behavior.
internal/core/extension/discovery_test.go (1)

13-31: Consider adding t.Parallel() to independent tests.

These discovery tests appear to be independent since they each create their own temporary directories. Adding t.Parallel() would improve test execution time.

♻️ Example for one test
 func TestDiscoveryReturnsEmptyWhenNoExtensionsInstalled(t *testing.T) {
+	t.Parallel()
+
 	withVersion(t, "1.5.0")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/extension/discovery_test.go` around lines 13 - 31, This test
(TestDiscoveryReturnsEmptyWhenNoExtensionsInstalled) is independent and should
run in parallel; add a call to t.Parallel() near the start of
TestDiscoveryReturnsEmptyWhenNoExtensionsInstalled (before shared setup like
withVersion or newTestDiscovery) so the test harness can execute it
concurrently; apply the same change to other independent discovery tests in this
file (tests that create their own temp dirs and don't share global state) such
as other TestDiscovery* functions.
internal/core/extension/host_writes.go (1)

397-427: Atomic write implementation is correct but could use fsync for durability.

The temp-file-then-rename pattern is correct for atomicity. However, on some filesystems, the content may not be durable without an explicit fsync before closing. Consider whether durability guarantees are needed for this use case.

♻️ Optional: Add Sync() for durability
 	if _, err := tmpFile.Write(content); err != nil {
 		cleanup()
 		return fmt.Errorf("write temp file for %s: %w", path, err)
 	}
+	if err := tmpFile.Sync(); err != nil {
+		cleanup()
+		return fmt.Errorf("sync temp file for %s: %w", path, err)
+	}
 	if err := tmpFile.Close(); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/extension/host_writes.go` around lines 397 - 427, The atomic
write in writeHostFileAtomically should call Sync to ensure durability: after
tmpFile.Write(content) succeed, call tmpFile.Sync() (handle and return any
error) before tmpFile.Close(), and after successful os.Rename(tmpPath, path)
optionally open the parent dir (using dir := filepath.Dir(path)), call
dirFile.Sync() and close it to ensure the rename is durable; update error
handling to cleanup tmpPath and propagate fsync errors from tmpFile.Sync() and
dirFile.Sync() as appropriate.
internal/core/extension/host_api.go (1)

86-88: Avoid defaulting to context.Background() in production code.

Per coding guidelines, context.Background() should be avoided outside main and focused tests. Consider returning an error when ctx is nil instead of silently substituting a background context.

♻️ Proposed fix
 func (r *HostAPIRouter) Handle(
 	ctx context.Context,
 	extensionName string,
 	method string,
 	params json.RawMessage,
 ) (any, error) {
-	if ctx == nil {
-		ctx = context.Background()
+	if ctx == nil {
+		return nil, fmt.Errorf("handle host api request: context is required")
 	}

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".

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

In `@internal/core/extension/host_api.go` around lines 86 - 88, The nil-context
guard currently substitutes context.Background() for a nil ctx; instead, change
this to return an error when ctx is nil to avoid silently using Background in
production. Locate the nil check around the ctx variable in host_api.go (the
block starting with "if ctx == nil") and replace it with an explicit error
return (e.g., return nil, fmt.Errorf(...) or appropriate typed error) from the
enclosing function, and update any callers to ensure they pass a non-nil
context; do not introduce context.Background() except in main or tests.
internal/core/extension/chain.go (1)

164-181: WantsEvent returns true for empty filter - verify this is intentional.

When e.eventKinds is empty (line 172-174), the method returns true, meaning the extension wants all events. This is likely intentional (opt-out filtering), but should be documented clearly.

📝 Suggested documentation improvement
 // WantsEvent reports whether the extension should receive one bus event.
+// When no event filter is configured (empty eventKinds), all events are wanted.
 func (e *RuntimeExtension) WantsEvent(kind runtimeevents.EventKind) bool {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/extension/chain.go` around lines 164 - 181, The WantsEvent
method currently treats an empty e.eventKinds as "match all" which can be
surprising; add a clear godoc comment on RuntimeExtension.WantsEvent that states
an empty eventKinds slice means the extension subscribes to all event kinds
(opt-out filtering), and update any related comments/tests (e.g., unit test for
WantsEvent) to reflect and assert this behavior so future readers know the
empty-slice semantics; reference the RuntimeExtension type and WantsEvent method
when adding the documentation.
internal/core/extension/assets.go (1)

134-166: Silent glob error handling is acceptable but consider logging for observability.

The resolveSkillPattern method silently returns nil when glob operations fail (lines 142-144, 154-156). While this is robust for production, consider adding debug-level logging for observability when glob patterns fail to match, which could help diagnose configuration issues.

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

In `@internal/core/extension/assets.go` around lines 134 - 166, The
resolveSkillPattern function currently swallows glob errors; update it to log
debug-level details whenever filepath.Glob or fs.Glob returns an error (include
the original pattern and the returned err) so failures are observable: inside
DiscoveredExtension.resolveSkillPattern, where you call
filepath.Glob(filepath.Join(e.diskRoot, filepath.FromSlash(trimmed))) and
fs.Glob(e.rootFS, path.Join(e.fsBase, trimmed)), add debug logging that records
the pattern, the source (disk vs embedded), and the error (e.g., using the
package or extension logger available in this package or on the
DiscoveredExtension struct) before returning nil.
🤖 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/extension/display.go`:
- Around line 113-116: The code currently returns a generic "extension not
found" after calling findEffectiveExtension; instead, before returning, check
whether discovery recorded a failure for that extension name (the same place
where appendDiscoveryFailureNotes would add diagnostics) and surface that
failure/error first. Concretely, in the block around
findEffectiveExtension(result, name) add logic to consult the discovery failure
data stored on result (or call appendDiscoveryFailureNotes(result, name)) and
return that detailed error/notes when present rather than the generic
fmt.Errorf("extension %q not found", name), so inspect returns the discovery
failure details for malformed/partial installs.

In `@internal/cli/extension/doctor.go`:
- Around line 88-94: The footer "No extension override or drift issues
detected." is added whenever report.Infos is empty even if report.Warnings
contains drift warnings from skillPackDriftWarnings(); update the conditional so
the message is only appended when both report.Infos and report.Warnings are
empty. Specifically, modify the block that currently checks len(report.Infos) ==
0 (after appending overrideInfos via overrideInfos(result.Overrides)) to check
len(report.Infos) == 0 && len(report.Warnings) == 0 so that warnings prevent the
misleading footer.

In `@internal/cli/extension/enablement.go`:
- Around line 57-63: The current code uses findEffectiveExtension(result, name)
which returns the winning entry, causing ext enable/disable to target the
effective winner instead of the specific match the user intended; change the
logic to search the discovered matches in result for the highest-precedence
entry whose Enabled state differs from the requested enable value and pass that
entry to toggleEntry(ctx, env.store, entry, enable). Concretely, replace the
single findEffectiveExtension call with iterating the matches for name (from
result or the function that returns all matches), select the top-precedence
match with entry.Enabled != enable (prefer workspace/user overrides over
bundled), and if found call toggleEntry with that entry; if none found return
the existing "not found/already in desired state" error.

In `@internal/cli/extension/install.go`:
- Around line 299-323: copySymlink currently recreates symlinks verbatim which
allows absolute links or "../" escapes to point outside the install dir; change
copySymlink to refuse or sanitize such links: after reading linkTarget in
copySymlink, detect unsafe targets (filepath.IsAbs(linkTarget) or path elements
with ".." that would escape the extension root when resolved) and either return
an error rejecting the symlink or rewrite it to a safe relative link (or fall
back to copying the resolved file contents) so the installed extension cannot
reference files outside its install directory; ensure any sanitization uses
filepath.Clean/IsAbs and resolves the final target relative to the source tree
before deciding.
- Around line 79-80: When deps.copyDir(sourcePath, installPath) fails you must
clean up the partially-created install tree to avoid corrupt installs: before
calling deps.copyDir, check whether installPath already existed (os.Stat) and
record that; if copy returns an error and installPath did not exist beforehand,
remove the installPath directory (os.RemoveAll) to rollback the partial install,
then return the original fmt.Errorf("copy extension into user scope: %w", err);
implement this logic around the existing deps.copyDir call so rollback only
removes what this operation created.

In `@internal/cli/extension/root.go`:
- Around line 85-104: resolveEnv currently hard-fails by always calling
resolveWorkspaceRoot, which prevents ext commands from running outside a
workspace; make workspace resolution best-effort by attempting
resolveWorkspaceRoot(ctx) and, on a non-fatal "no workspace" case, set
commandEnv.workspaceRoot to an empty string (or nil equivalent) and continue
returning the env, rather than returning an error. Keep resolveEnv's homeDir and
store creation unchanged, and move strict workspace validation into the specific
subcommands that require workspace writes (update those handlers to call
resolveWorkspaceRoot or check commandEnv.workspaceRoot and return a clear error
if missing). Use the function/struct names resolveEnv, resolveWorkspaceRoot, and
commandEnv to locate the changes.

In `@internal/cli/extensions_bootstrap.go`:
- Around line 26-29: The bootstrap call instantiates extensions.Discovery with
only WorkspaceRoot, which drops user scope and enablement state; update the
construction of extensions.Discovery used before calling Discover(ctx) to
include the same fields used by the main CLI discovery flow (e.g., HomeDir and
the EnablementStore/enablement backend from cfg) so that
extensions.Discovery{WorkspaceRoot: cfg.WorkspaceRoot, HomeDir: cfg.HomeDir,
EnablementStore: cfg.EnablementStore} (or similarly named cfg fields) is passed
into Discover; keep the existing error wrapping for Discover(ctx) intact so
bootstrap discovery honors user-enabled declarative providers/skill packs and
stays consistent with `compozy ext ...`.

In `@internal/core/agent/client.go`:
- Around line 509-514: The subprocess is launched with context.Background(),
dropping request-scoped values; change the call in the subprocess.Launch
invocation to use a derived context that preserves values but is detached from
cancellation by using context.WithoutCancel(ctx) (or equivalent) so the launched
process (stored in c.process) inherits tracing/logging/auth from the request
context without being cancelled when the request ends; update the Launch call to
pass the new ctx returned by context.WithoutCancel and remove
context.Background() usage.
- Around line 49-57: Change the Prompt field from []byte to string in the hook
JSON contract structs in internal/core/agent/client.go (replace Prompt []byte
`json:"prompt,omitempty"` with Prompt string `json:"prompt,omitempty"` for the
struct containing
WorkingDir/Model/MCPServers/ExtraEnv/Context/RunID/JobID/RuntimeMgr and any
other similar hook/request structs in the same file), and update all call sites
that set or read Prompt (convert between []byte and string using string(b) or
[]byte(s) as needed) so the JSON marshals to plain text; alternatively implement
MarshalJSON/UnmarshalJSON on those structs if you need custom byte handling, but
prefer the simple type change to preserve human-readable prompt payloads.

In `@internal/core/agent/hooks.go`:
- Around line 33-37: The withHookContextFrom function currently copies RunID,
JobID and RuntimeMgr but omits Context, which can detach the mutated
SessionRequest from the caller's cancellation/deadline; update
withHookContextFrom to copy src.Context into r.Context (i.e., r.Context =
src.Context) so the original context is preserved after pre-hook mutation, and
apply the same change to the other helper noted in the review (the partner
helper that also copies RunID/JobID/RuntimeMgr).

In `@internal/core/agent/registry_overlay.go`:
- Around line 158-171: splitOverlayCommand and parseOverlayArgs currently use
strings.Fields which loses quoting/escape semantics and breaks paths/args with
spaces; replace both with a shell-aware splitter (e.g., shlex/shellwords) that
preserves quoted strings and escapes so an executable like "/Applications/Visual
Studio Code.app/.../code" and its flags remain intact. Update
splitOverlayCommand to call the new shell-aware split function (instead of
strings.Fields) and return the first element as the command and the rest as
args, and change parseOverlayArgs to return the shell-aware split result (or nil
for empty input). Also ensure the same parser is used when reading fixed_args
and probe_args from metadata so overlays with spaced paths or quoted multi-word
args work correctly.

In `@internal/core/extension/audit.go`:
- Around line 181-236: The Close method can return early on ctx cancellation
while the background goroutine spawned to call syncAndCloseFile(file) continues,
leaving closeDone/closeErr set and the file not fully closed; modify Close so
that on ctx.Done() it still waits for the background goroutine to finish (read
from closeDone) before returning, then wrap/return the context error (e.g.,
capture <-done then return fmt.Errorf("close audit logger: %w", ctx.Err()) or
return the file-close error combined with ctx.Err. Update the select at the end
of Close to ensure the goroutine started in Close (which sets l.closeErr and
closes l.closeDone) is always awaited before returning, referencing symbols:
Close, syncAndCloseFile, closeDone, closeErr, file, path, mu.

In `@internal/core/extension/dispatcher_test.go`:
- Around line 295-302: Replace the wall-clock assertion with channel
synchronization: launch DispatchObserver(context.Background(),
HookAgentPostSessionCreate, ...) in a goroutine that closes a done channel when
it returns, then use select to assert done is received before you close/release
the test's releaseSlow channel (or before a reasonable global test timeout);
this verifies DispatchObserver returned without relying on a 50ms sleep.
Reference DispatchObserver and HookAgentPostSessionCreate to locate where to add
the done channel and select logic.

In `@internal/core/extension/dispatcher.go`:
- Around line 188-206: The code computes an effective timeout into the local
variable timeout (falling back to entry.extension.DefaultHookTimeout()) but
still serializes durationMilliseconds(entry.hook.Timeout) into the
executeHookRequestHook, causing extensions to receive the raw manifest value
(often 0); update the construction of executeHookRequestHook to use the computed
timeout variable (e.g. TimeoutMS: durationMilliseconds(timeout)) so the
extension sees the actual effective deadline used for callCtx; locate the code
around executeHookRequest/executeHookRequestHook, effectiveHookName,
durationMilliseconds, and d.nextInvocationID to make this change.

In `@internal/core/extension/enablement.go`:
- Around line 210-243: saveWorkspaceState currently does an unprotected
read-modify-write on the workspace state file which can lose concurrent updates;
update saveWorkspaceState to (1) acquire a file lock on the workspace state path
(or a dedicated lock file) before loading and modifying the record, (2) perform
the load/modify while the lock is held, and (3) write atomically by writing
payload to a temp file in the same directory and renaming it into place
(ensuring MkdirAll runs before creating the temp file), then release the lock;
reference the saveWorkspaceState, loadWorkspaceEnablementRecord,
workspaceStatePath symbols when implementing the lock+atomic-write flow and
ensure errors release the lock.

In `@internal/core/extension/host_helpers.go`:
- Around line 528-555: The boundary check in resolveScopedPath currently uses
filepath.Clean and pathWithinRoot (and pathWithinRoot itself) which are purely
lexical and can be bypassed via symlinks; update resolveScopedPath to resolve
symlinks before containment checks by calling filepath.EvalSymlinks (or migrate
to the os.Root API if you prefer platform-safe root confinement), apply
EvalSymlinks to both the candidate path and the roots (o.workspaceRoot and
model.CompozyDir(o.workspaceRoot)), then use pathWithinRoot against the
evaluated real paths; also update pathWithinRoot to operate on evaluated
(symlink-resolved) paths so NewPathOutOfScopeError is only raised when the real
filesystem location is outside the allowed roots.
- Around line 363-383: The decoder.More() check in decodeHostParams does not
detect arbitrary trailing top-level JSON; replace that check by attempting a
second decoder.Decode(&extra json.RawMessage) and treat io.EOF as the clean
end-of-input, otherwise return subprocess.NewInvalidParams with method and the
decode error (or a custom "unexpected trailing data" message); ensure you
declare the extra variable (e.g., var extra json.RawMessage) and import io so
you can compare the error to io.EOF while keeping the existing error payload
structure used in decodeHostParams.

In `@Makefile`:
- Around line 84-86: The publish-extension-sdks Makefile target currently runs
npm publish directly; change its rule to depend on the verify and
build-extension-sdks targets so publishing is gated by verification and build
steps — update the publish-extension-sdks target to list verify and
build-extension-sdks as prerequisites and keep the existing npm publish commands
under that target (target name: publish-extension-sdks; dependency names:
verify, build-extension-sdks).
- Around line 85-86: The Makefile publish commands for the scoped packages do
not set public access; update the two npm publish invocations in the Makefile
that reference `@compozy/extension-sdk` and `@compozy/create-extension` to include
the --access public flag so scoped packages are published as public (i.e.,
modify the npm publish --workspace `@compozy/extension-sdk` and npm publish
--workspace `@compozy/create-extension` lines to add --access public).

---

Nitpick comments:
In `@internal/cli/run.go`:
- Around line 120-124: The runPrepared function uses a variadic parameter assets
...declarativeAssets although only 0 or 1 asset is ever expected; change the
signature to accept a single pointer *declarativeAssets (e.g., func (s
*commandState) runPrepared(ctx context.Context, cmd *cobra.Command, cfg
core.Config, assets *declarativeAssets)), update the function body to treat
assets as possibly nil instead of indexing the slice, and update all callers of
runPrepared to pass either nil or &asset accordingly; ensure any code that
relied on assets length/iteration is adjusted to handle a single optional value.

In `@internal/core/agent/session_helpers_test.go`:
- Around line 575-577: The test in session_helpers_test.go is asserting behavior
of subprocess.NormalizeWaitError (an API from the subprocess package) which
couples core tests to external-package implementation; move the assertion into a
new or existing test in the internal/core/subprocess package tests and remove it
from session_helpers_test.go. Specifically, create/update a test that exercises
subprocess.NormalizeWaitError with the same inputs (e.g., call
NormalizeWaitError("wait for ACP agent process", nil) and assert nil) and keep
session_helpers_test.go focused on only core/session helpers rather than
subprocess internals.

In `@internal/core/extension/assets_test.go`:
- Line 8: Add t.Parallel() at the start of each independent test function (e.g.,
TestExtractDeclaredProvidersGroupsByCategory and the other test around the
51-range) so they run concurrently; locate the test functions by their names and
insert a single call to t.Parallel() immediately after the function opening
brace to mark them as parallelizable.

In `@internal/core/extension/assets.go`:
- Around line 134-166: The resolveSkillPattern function currently swallows glob
errors; update it to log debug-level details whenever filepath.Glob or fs.Glob
returns an error (include the original pattern and the returned err) so failures
are observable: inside DiscoveredExtension.resolveSkillPattern, where you call
filepath.Glob(filepath.Join(e.diskRoot, filepath.FromSlash(trimmed))) and
fs.Glob(e.rootFS, path.Join(e.fsBase, trimmed)), add debug logging that records
the pattern, the source (disk vs embedded), and the error (e.g., using the
package or extension logger available in this package or on the
DiscoveredExtension struct) before returning nil.

In `@internal/core/extension/chain.go`:
- Around line 164-181: The WantsEvent method currently treats an empty
e.eventKinds as "match all" which can be surprising; add a clear godoc comment
on RuntimeExtension.WantsEvent that states an empty eventKinds slice means the
extension subscribes to all event kinds (opt-out filtering), and update any
related comments/tests (e.g., unit test for WantsEvent) to reflect and assert
this behavior so future readers know the empty-slice semantics; reference the
RuntimeExtension type and WantsEvent method when adding the documentation.

In `@internal/core/extension/discovery_test.go`:
- Around line 13-31: This test
(TestDiscoveryReturnsEmptyWhenNoExtensionsInstalled) is independent and should
run in parallel; add a call to t.Parallel() near the start of
TestDiscoveryReturnsEmptyWhenNoExtensionsInstalled (before shared setup like
withVersion or newTestDiscovery) so the test harness can execute it
concurrently; apply the same change to other independent discovery tests in this
file (tests that create their own temp dirs and don't share global state) such
as other TestDiscovery* functions.

In `@internal/core/extension/enablement_test.go`:
- Around line 10-62: Add t.Parallel() as the first statement in each top-level
test function (TestEnablementStoreDefaults,
TestEnablementStorePersistsRoundTrip,
TestEnablementStoreRejectsBundledMutations,
TestNewEnablementStoreResolvesHomeDir,
TestEnablementStoreRejectsInvalidReferences,
TestEnablementStoreRejectsCorruptState) to allow safe concurrent execution; for
table-driven tests like TestEnablementStoreDefaults also call t.Parallel() at
the start of each subtest closure inside t.Run so individual cases run in
parallel while still using t.TempDir() isolation.

In `@internal/core/extension/host_api.go`:
- Around line 86-88: The nil-context guard currently substitutes
context.Background() for a nil ctx; instead, change this to return an error when
ctx is nil to avoid silently using Background in production. Locate the nil
check around the ctx variable in host_api.go (the block starting with "if ctx ==
nil") and replace it with an explicit error return (e.g., return nil,
fmt.Errorf(...) or appropriate typed error) from the enclosing function, and
update any callers to ensure they pass a non-nil context; do not introduce
context.Background() except in main or tests.

In `@internal/core/extension/host_reads_test.go`:
- Around line 163-166: Replace the magic number 180 in the test loop that builds
lines (the builder loop in host_reads_test.go) with the production compaction
threshold constant instead of a literal; reference the actual constant used by
the compaction logic (e.g., CompactionThreshold or the appropriately named
constant from the package that implements compaction) by importing that package
and using that symbol, or if the constant is unexported, add an exported alias
in the production package and use it here so the test stays in sync with
production behavior.

In `@internal/core/extension/host_writes.go`:
- Around line 397-427: The atomic write in writeHostFileAtomically should call
Sync to ensure durability: after tmpFile.Write(content) succeed, call
tmpFile.Sync() (handle and return any error) before tmpFile.Close(), and after
successful os.Rename(tmpPath, path) optionally open the parent dir (using dir :=
filepath.Dir(path)), call dirFile.Sync() and close it to ensure the rename is
durable; update error handling to cleanup tmpPath and propagate fsync errors
from tmpFile.Sync() and dirFile.Sync() as appropriate.
🪄 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: 979bc1d9-cf1c-4acb-b6c0-9f773872648c

📥 Commits

Reviewing files that changed from the base of the PR and between ab93fca and abcbcde.

⛔ Files ignored due to path filters (100)
  • .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
  • .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
  • .codex/ledger/2026-04-07-MEMORY-review-nitpicks.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-compozy-agents-techspec.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-declarative-assets.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-early-run-scope.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-ext-cli-state.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-extensibility-review.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-extension-bootstrap.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-extension-foundation.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-extension-lifecycle.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-hook-dispatches.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-job-run-review-hooks.md is excluded by !**/*.md
  • .codex/ledger/2026-04-10-MEMORY-three-level-discovery.md is excluded by !**/*.md
  • .compozy/docs/extensibility/architecture.md is excluded by !**/*.md
  • .compozy/docs/extensibility/capability-reference.md is excluded by !**/*.md
  • .compozy/docs/extensibility/getting-started.md is excluded by !**/*.md
  • .compozy/docs/extensibility/hello-world-go.md is excluded by !**/*.md
  • .compozy/docs/extensibility/hello-world-ts.md is excluded by !**/*.md
  • .compozy/docs/extensibility/hook-reference.md is excluded by !**/*.md
  • .compozy/docs/extensibility/host-api-reference.md is excluded by !**/*.md
  • .compozy/docs/extensibility/index.md is excluded by !**/*.md
  • .compozy/docs/extensibility/migration-guide.md is excluded by !**/*.md
  • .compozy/docs/extensibility/testing.md is excluded by !**/*.md
  • .compozy/docs/extensibility/trust-and-enablement.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/_meta.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_02.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_03.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_04.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_05.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_06.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_07.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_08.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_09.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_10.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_11.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_12.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_13.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_14.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/memory/task_15.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_01.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_02.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_03.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_04.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_05.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_06.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_07.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_08.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_09.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_10.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_11.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_12.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_13.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_14.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/task_15.md is excluded by !**/*.md
  • .github/actions/setup-bun/action.yml is excluded by !**/*.yml
  • .github/actions/setup-git-cliff/action.yml is excluded by !**/*.yml
  • .github/actions/setup-release/action.yml is excluded by !**/*.yml
  • .golangci.yml is excluded by !**/*.yml
  • .oxfmtrc.json is excluded by !**/*.json
  • .oxlintrc.json is excluded by !**/*.json
  • AGENTS.md is excluded by !**/*.md
  • CHANGELOG.md is excluded by !**/*.md
  • CLAUDE.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • aur-pkg/TODO.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
  • cliff.toml is excluded by !**/*.toml
  • docs/events.md is excluded by !**/*.md
  • package.json is excluded by !**/*.json
  • sdk/create-extension/README.md is excluded by !**/*.md
  • sdk/create-extension/package.json is excluded by !**/*.json
  • sdk/create-extension/tsconfig.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/package.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/lifecycle-observer/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/templates/lifecycle-observer/extension.toml is excluded by !**/*.toml
  • sdk/extension-sdk-ts/templates/lifecycle-observer/package.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/lifecycle-observer/tsconfig.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/prompt-decorator/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/templates/prompt-decorator/extension.toml is excluded by !**/*.toml
  • sdk/extension-sdk-ts/templates/prompt-decorator/package.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/prompt-decorator/tsconfig.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/review-provider/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/templates/review-provider/extension.toml is excluded by !**/*.toml
  • sdk/extension-sdk-ts/templates/review-provider/package.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/skill-pack/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/templates/skill-pack/extension.toml is excluded by !**/*.toml
  • sdk/extension-sdk-ts/templates/skill-pack/package.json is excluded by !**/*.json
  • sdk/extension-sdk-ts/templates/skill-pack/skills/announce-run/SKILL.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/tsconfig.json is excluded by !**/*.json
  • tsconfig.base.json is excluded by !**/*.json
  • tsconfig.json is excluded by !**/*.json
  • turbo.json is excluded by !**/*.json
📒 Files selected for processing (176)
  • .editorconfig
  • .gitignore
  • .husky/pre-commit
  • Makefile
  • compozy.go
  • internal/cli/commands.go
  • internal/cli/commands_test.go
  • internal/cli/extension/display.go
  • internal/cli/extension/display_test.go
  • internal/cli/extension/doctor.go
  • internal/cli/extension/doctor_test.go
  • internal/cli/extension/enablement.go
  • internal/cli/extension/install.go
  • internal/cli/extension/root.go
  • internal/cli/extensions_bootstrap.go
  • internal/cli/extensions_bootstrap_test.go
  • internal/cli/root.go
  • internal/cli/root_command_execution_test.go
  • internal/cli/root_test.go
  • internal/cli/run.go
  • internal/cli/skills_preflight.go
  • internal/cli/skills_preflight_test.go
  • internal/cli/state.go
  • internal/cli/testdata/exec_help.golden
  • internal/core/agent/client.go
  • internal/core/agent/client_test.go
  • internal/core/agent/hooks.go
  • internal/core/agent/process_unix.go
  • internal/core/agent/registry_launch.go
  • internal/core/agent/registry_overlay.go
  • internal/core/agent/registry_overlay_test.go
  • internal/core/agent/registry_specs.go
  • internal/core/agent/registry_validate.go
  • internal/core/agent/session.go
  • internal/core/agent/session_helpers_test.go
  • internal/core/api.go
  • internal/core/extension/assets.go
  • internal/core/extension/assets_test.go
  • internal/core/extension/audit.go
  • internal/core/extension/audit_test.go
  • internal/core/extension/builtin/doc.go
  • internal/core/extension/capability.go
  • internal/core/extension/capability_test.go
  • internal/core/extension/chain.go
  • internal/core/extension/discovery.go
  • internal/core/extension/discovery_bundled.go
  • internal/core/extension/discovery_test.go
  • internal/core/extension/dispatcher.go
  • internal/core/extension/dispatcher_test.go
  • internal/core/extension/doc.go
  • internal/core/extension/enablement.go
  • internal/core/extension/enablement_test.go
  • internal/core/extension/hooks_integration_test.go
  • internal/core/extension/host_api.go
  • internal/core/extension/host_api_errors.go
  • internal/core/extension/host_api_test.go
  • internal/core/extension/host_helpers.go
  • internal/core/extension/host_helpers_test.go
  • internal/core/extension/host_reads.go
  • internal/core/extension/host_reads_test.go
  • internal/core/extension/host_writes.go
  • internal/core/extension/host_writes_test.go
  • internal/core/extension/manager.go
  • internal/core/extension/manager_constants.go
  • internal/core/extension/manager_events.go
  • internal/core/extension/manager_health.go
  • internal/core/extension/manager_shutdown.go
  • internal/core/extension/manager_spawn.go
  • internal/core/extension/manager_test.go
  • internal/core/extension/manifest.go
  • internal/core/extension/manifest_load.go
  • internal/core/extension/manifest_test.go
  • internal/core/extension/manifest_validate.go
  • internal/core/extension/runtime.go
  • internal/core/extension/runtime_test.go
  • internal/core/extension/sdk_manager_integration_test.go
  • internal/core/extension/testdata/mock_extension/main.go
  • internal/core/extension/testdata/sdk_extension/main.go
  • internal/core/extension/ts_template_manager_integration_test.go
  • internal/core/fetch.go
  • internal/core/kernel/commands/commands_test.go
  • internal/core/kernel/deps.go
  • internal/core/kernel/deps_test.go
  • internal/core/kernel/handlers.go
  • internal/core/kernel/handlers_extensions_test.go
  • internal/core/kernel/run_scope_cancellation_integration_test.go
  • internal/core/kernel/run_scope_integration_test.go
  • internal/core/memory/store.go
  • internal/core/model/artifacts.go
  • internal/core/model/hook_types.go
  • internal/core/model/hooks.go
  • internal/core/model/preparation.go
  • internal/core/model/preparation_test.go
  • internal/core/model/run_scope.go
  • internal/core/model/runtime_config.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/provider/overlay.go
  • internal/core/provider/overlay_test.go
  • internal/core/run/exec/exec.go
  • internal/core/run/exec/exec_integration_test.go
  • internal/core/run/exec/prompt_exec.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/hooks.go
  • internal/core/run/executor/review_hooks.go
  • internal/core/run/executor/runner.go
  • internal/core/run/internal/acpshared/command_io.go
  • internal/core/run/internal/acpshared/session_handler.go
  • internal/core/run/internal/acpshared/session_handler_test.go
  • internal/core/run/internal/runshared/config.go
  • internal/core/run/journal/journal.go
  • internal/core/run/journal/journal_test.go
  • internal/core/run/run.go
  • internal/core/run/run_test.go
  • internal/core/subprocess/handshake.go
  • internal/core/subprocess/process.go
  • internal/core/subprocess/process_unix.go
  • internal/core/subprocess/process_unix_test.go
  • internal/core/subprocess/process_windows.go
  • internal/core/subprocess/transport.go
  • internal/core/subprocess/transport_test.go
  • internal/setup/extensions.go
  • internal/setup/extensions_test.go
  • internal/version/version.go
  • pkg/compozy/events/docs_test.go
  • pkg/compozy/events/event.go
  • pkg/compozy/events/kinds/extension.go
  • pkg/compozy/events/kinds/payload_compat_test.go
  • sdk/create-extension/src/index.ts
  • sdk/create-extension/test/create-extension.test.ts
  • sdk/extension-sdk-ts/src/extension.ts
  • sdk/extension-sdk-ts/src/handlers.ts
  • sdk/extension-sdk-ts/src/host_api.ts
  • sdk/extension-sdk-ts/src/index.ts
  • sdk/extension-sdk-ts/src/testing/index.ts
  • sdk/extension-sdk-ts/src/testing/mock_transport.ts
  • sdk/extension-sdk-ts/src/testing/test_harness.ts
  • sdk/extension-sdk-ts/src/transport.ts
  • sdk/extension-sdk-ts/src/types.ts
  • sdk/extension-sdk-ts/templates/lifecycle-observer/src/extension.ts
  • sdk/extension-sdk-ts/templates/lifecycle-observer/src/index.ts
  • sdk/extension-sdk-ts/templates/lifecycle-observer/test/lifecycle-observer.test.ts
  • sdk/extension-sdk-ts/templates/prompt-decorator/src/extension.ts
  • sdk/extension-sdk-ts/templates/prompt-decorator/src/index.ts
  • sdk/extension-sdk-ts/templates/prompt-decorator/test/prompt-decorator.test.ts
  • sdk/extension-sdk-ts/templates/review-provider/test/review-provider.test.js
  • sdk/extension-sdk-ts/templates/skill-pack/test/skill-pack.test.js
  • sdk/extension-sdk-ts/test/extension.test.ts
  • sdk/extension-sdk-ts/test/fluent_hooks.test.ts
  • sdk/extension-sdk-ts/test/handlers.test.ts
  • sdk/extension-sdk-ts/test/host_api.test.ts
  • sdk/extension-sdk-ts/test/templates.test.ts
  • sdk/extension-sdk-ts/test/transport.test.ts
  • sdk/extension/compat_test.go
  • sdk/extension/doc.go
  • sdk/extension/extension.go
  • sdk/extension/extension_test.go
  • sdk/extension/handlers.go
  • sdk/extension/hooks.go
  • sdk/extension/host_api.go
  • sdk/extension/host_api_test.go
  • sdk/extension/internal_test.go
  • sdk/extension/smoke_test.go
  • sdk/extension/testing/harness.go
  • sdk/extension/testing/harness_test.go
  • sdk/extension/testing/mock_transport.go
  • sdk/extension/testing/mock_transport_test.go
  • sdk/extension/transport.go
  • sdk/extension/types.go
  • vitest.config.ts
💤 Files with no reviewable changes (1)
  • internal/core/agent/process_unix.go

Comment thread internal/cli/extension/display.go
Comment thread internal/cli/extension/doctor.go
Comment thread internal/cli/extension/enablement.go Outdated
Comment thread internal/cli/extension/install.go
Comment thread internal/cli/extension/install.go
Comment thread internal/core/extension/enablement.go
Comment thread internal/core/extension/host_helpers.go Outdated
Comment thread internal/core/extension/host_helpers.go Outdated
Comment thread Makefile Outdated
Comment thread Makefile 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: 5

Caution

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

⚠️ Outside diff range comments (1)
internal/core/agent/client.go (1)

590-615: ⚠️ Potential issue | 🔴 Critical

Gate concurrent callers until ACP initialization finishes.

Line 611 publishes c.started = true before conn.Initialize(...) returns. A concurrent CreateSession / ResumeSession can then skip startup and issue RPCs on c.conn while the ACP handshake is still in flight, or after initialization fails and Close() starts tearing the client down. This needs a start-in-progress barrier so other callers wait for the first initialization attempt to finish.

🤖 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 590 - 615, The code marks
c.started = true before the ACP handshake finishes, letting concurrent
CreateSession/ResumeSession callers use c.conn while Initialize is still
in-flight; add a start-in-progress barrier (e.g. a boolean c.starting plus a
sync.Cond or a channel) around the startup sequence in the client: set
c.starting = true before Launch/conn.Initialize, and only set c.started = true
(and clear c.starting and broadcast/signal) after conn.Initialize returns
successfully; have CreateSession/ResumeSession wait on that condition while
c.starting is true and return the initialization error if Initialize fails
(ensure Close()/waitForProcess clears/signals the condition on error) so no RPCs
run against a partially-initialized or failed c.conn.
♻️ Duplicate comments (1)
internal/cli/extension/root.go (1)

85-105: ⚠️ Potential issue | 🟠 Major

Workspace resolution still hard-fails for all ext commands.

resolveEnv unconditionally calls resolveWorkspaceRoot, causing ext list, ext inspect, ext enable, and ext disable to fail when run outside a workspace. These commands should work for bundled and user-scoped extensions even without a workspace context.

Consider making workspace resolution best-effort: if resolveWorkspaceRoot fails with a "no workspace found" error, set workspaceRoot to an empty string and continue, then let only subcommands that truly require workspace writes validate and fail explicitly.

,

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

In `@internal/cli/extension/root.go` around lines 85 - 105, resolveEnv currently
always calls resolveWorkspaceRoot and bubbles up its error, which causes all ext
commands to fail outside a workspace; change resolveEnv to treat the specific
"no workspace found" error from resolveWorkspaceRoot as non-fatal by setting
workspaceRoot to "" (or nil equivalent) and continuing, returning a commandEnv
with cleaned homeDir and the enablement store; ensure only subcommands that need
workspace writes explicitly call/validate resolveWorkspaceRoot or check
commandEnv.workspaceRoot and return a clear error when absent. Use the
resolveEnv function and resolveWorkspaceRoot symbol names to locate and
implement this best-effort behavior.
🧹 Nitpick comments (2)
internal/cli/makefile_publish_test.go (1)

14-16: Avoid exact full-line dependency matching for Make target prerequisites.

strings.Contains(makefile, "publish-extension-sdks: verify build-extension-sdks") is brittle to harmless prerequisite reordering/spacing. Consider parsing the target line and asserting required deps by membership (verify, build-extension-sdks) instead of exact sequence.

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

In `@internal/cli/makefile_publish_test.go` around lines 14 - 16, The test
currently checks for an exact substring match on the Makefile line using
makefile and the literal "publish-extension-sdks: verify build-extension-sdks",
which is brittle; update the test to locate the "publish-extension-sdks:" target
line from the makefile string (e.g., via strings.Split or a regex), extract and
split the prerequisites on whitespace, trim them, and assert that the resulting
slice contains both "verify" and "build-extension-sdks" (order- and
spacing-independent) instead of relying on exact sequence matching.
internal/core/agent/client_test.go (1)

1486-1499: This hook stub bypasses the JSON round-trip the real path uses.

DispatchMutableHook returns the same Go value back to the test, so the pre-hook tests won't catch serialization regressions in SessionRequest / ResumeSessionRequest—especially custom MarshalJSON / UnmarshalJSON behavior and fields excluded with json:"-". Consider round-tripping through JSON here, or add one integration-style test against the real runtime manager.

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

In `@internal/core/agent/client_test.go` around lines 1486 - 1499,
DispatchMutableHook currently returns the Go value directly, skipping the JSON
marshal/unmarshal path used in production and allowing serialization regressions
(e.g., custom MarshalJSON/UnmarshalJSON or json:"-" fields) to go unnoticed;
change DispatchMutableHook to perform a JSON round-trip of the input (marshal to
bytes then unmarshal back into the same concrete type) before returning or
before calling the mutator so tests exercise real serialization for types like
SessionRequest/ResumeSessionRequest, and ensure you propagate marshal/unmarshal
errors and preserve the existing behavior when mutate == nil.
🤖 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/makefile_publish_test.go`:
- Around line 11-25: Refactor
TestPublishExtensionSDKsTargetRequiresVerificationAndPublicAccess into
table-driven subtests: keep the initial check for the publish target dependency
in its own t.Run("Should require verify and build-extension-sdks") (call
readRepoMakefile once) and then convert the loop over the expected npm publish
lines into a table of test cases where each case runs as
t.Run(fmt.Sprintf("Should contain %q", want), func(t *testing.T) { t.Parallel();
assert the strings.Contains(makefile, want) }), ensuring each subtest references
the same makefile variable and uses t.Parallel() for independent execution.

In `@internal/core/agent/client.go`:
- Around line 683-697: The current logic in the goroutine using processRef()
treats a nil return from process.Wait() as a clean exit before checking
process.Forced(), which misclassifies forced kills because subprocess/process.go
clears waitErr on forced termination; change the order so you call
process.Forced() first (after obtaining process := c.processRef() and ensuring
it's non-nil) and call c.failOpenSessions(context.Canceled) if Forced() is true,
then handle the err == nil case to call c.failOpenSessions(errors.New("ACP agent
process exited before all sessions completed")), otherwise call
c.failOpenSessions(err); keep the use of process.Wait(), process.Forced(), and
c.failOpenSessions exactly as named.

In `@internal/core/agent/registry_overlay.go`:
- Around line 259-277: The bug is that handleEscapeStart marks every '\' (unless
inSingleQuote) as starting an escape, which causes backslashes inside
double-quoted strings to be consumed; update handleEscapeStart so that if
p.inSingleQuote you WriteRune('\\') as before, but if p.inDoubleQuote you also
WriteRune('\\') and return true (do not set p.escaped); only set p.escaped =
true for unquoted input (or other contexts where escaping is intended). Keep
handleEscapedRune unchanged (it should still consume the next rune when
p.escaped is true). Reference: function handleEscapeStart, function
handleEscapedRune, fields p.inSingleQuote, p.inDoubleQuote, p.escaped, and
p.current.WriteRune.

In `@internal/core/extension/dispatcher_test.go`:
- Around line 388-390: The test currently only checks err != nil for
applyHookPatch and should assert the specific failure reason; update the two
occurrences that call applyHookPatch (the one around
applyHookPatch(map[string]any{"prompt_text": "base"}, json.RawMessage(`"bad"`)
and the other at lines ~430-433) to use a specific error assertion such as
require.ErrorContains/require.ErrorAs (or t.ErrorContains/t.ErrorAs) and match a
stable substring or concrete error type explaining why the patch is invalid
(e.g., "invalid JSON type" or the specific error returned by applyHookPatch) so
the negative path is pinned to the intended contract.
- Around line 87-92: The test blocks indefinitely if DispatchMutable() fails to
call handlers; change the two blind channel receives on firstSeen and secondSeen
to receive with a timeout (e.g., using select and time.After or a context
deadline) so the test fails fast; specifically modify the receives that
currently read from firstSeen and secondSeen (after DispatchMutable and handler
setup) to select between the channel and a timeout case that calls t.Fatalf with
a clear message indicating which handler did not run.

---

Outside diff comments:
In `@internal/core/agent/client.go`:
- Around line 590-615: The code marks c.started = true before the ACP handshake
finishes, letting concurrent CreateSession/ResumeSession callers use c.conn
while Initialize is still in-flight; add a start-in-progress barrier (e.g. a
boolean c.starting plus a sync.Cond or a channel) around the startup sequence in
the client: set c.starting = true before Launch/conn.Initialize, and only set
c.started = true (and clear c.starting and broadcast/signal) after
conn.Initialize returns successfully; have CreateSession/ResumeSession wait on
that condition while c.starting is true and return the initialization error if
Initialize fails (ensure Close()/waitForProcess clears/signals the condition on
error) so no RPCs run against a partially-initialized or failed c.conn.

---

Duplicate comments:
In `@internal/cli/extension/root.go`:
- Around line 85-105: resolveEnv currently always calls resolveWorkspaceRoot and
bubbles up its error, which causes all ext commands to fail outside a workspace;
change resolveEnv to treat the specific "no workspace found" error from
resolveWorkspaceRoot as non-fatal by setting workspaceRoot to "" (or nil
equivalent) and continuing, returning a commandEnv with cleaned homeDir and the
enablement store; ensure only subcommands that need workspace writes explicitly
call/validate resolveWorkspaceRoot or check commandEnv.workspaceRoot and return
a clear error when absent. Use the resolveEnv function and resolveWorkspaceRoot
symbol names to locate and implement this best-effort behavior.

---

Nitpick comments:
In `@internal/cli/makefile_publish_test.go`:
- Around line 14-16: The test currently checks for an exact substring match on
the Makefile line using makefile and the literal "publish-extension-sdks: verify
build-extension-sdks", which is brittle; update the test to locate the
"publish-extension-sdks:" target line from the makefile string (e.g., via
strings.Split or a regex), extract and split the prerequisites on whitespace,
trim them, and assert that the resulting slice contains both "verify" and
"build-extension-sdks" (order- and spacing-independent) instead of relying on
exact sequence matching.

In `@internal/core/agent/client_test.go`:
- Around line 1486-1499: DispatchMutableHook currently returns the Go value
directly, skipping the JSON marshal/unmarshal path used in production and
allowing serialization regressions (e.g., custom MarshalJSON/UnmarshalJSON or
json:"-" fields) to go unnoticed; change DispatchMutableHook to perform a JSON
round-trip of the input (marshal to bytes then unmarshal back into the same
concrete type) before returning or before calling the mutator so tests exercise
real serialization for types like SessionRequest/ResumeSessionRequest, and
ensure you propagate marshal/unmarshal errors and preserve the existing behavior
when mutate == nil.
🪄 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: f764a07c-f63e-4eaf-9f47-584a714f6b40

📥 Commits

Reviewing files that changed from the base of the PR and between abcbcde and 75bb2e8.

⛔ Files ignored due to path filters (34)
  • .compozy/tasks/extensibility/reviews-001/_meta.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_012.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_013.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_014.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_015.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_016.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_017.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_018.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_019.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_020.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_021.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_022.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_023.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_024.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_025.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_026.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_027.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_028.md is excluded by !**/*.md
  • .compozy/tasks/extensibility/reviews-001/issue_029.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • sdk/create-extension/README.md is excluded by !**/*.md
  • sdk/extension-sdk-ts/README.md is excluded by !**/*.md
  • sdk/extension/README.md is excluded by !**/*.md
📒 Files selected for processing (35)
  • Makefile
  • internal/cli/extension/display.go
  • internal/cli/extension/display_test.go
  • internal/cli/extension/doctor.go
  • internal/cli/extension/doctor_test.go
  • internal/cli/extension/enablement.go
  • internal/cli/extension/install.go
  • internal/cli/extension/root.go
  • internal/cli/makefile_publish_test.go
  • internal/core/agent/client.go
  • internal/core/agent/client_test.go
  • internal/core/agent/hooks.go
  • internal/core/agent/registry_overlay.go
  • internal/core/agent/registry_overlay_test.go
  • internal/core/extension/assets_test.go
  • internal/core/extension/chain.go
  • internal/core/extension/dispatcher.go
  • internal/core/extension/dispatcher_test.go
  • internal/core/extension/enablement_test.go
  • internal/core/extension/hooks_integration_test.go
  • internal/core/extension/host_helpers.go
  • internal/core/extension/host_reads.go
  • internal/core/extension/host_reads_test.go
  • internal/core/extension/host_writes.go
  • internal/core/extension/testdata/mock_extension/main.go
  • internal/update/check_test.go
  • sdk/extension-sdk-ts/src/extension.ts
  • sdk/extension-sdk-ts/src/handlers.ts
  • sdk/extension-sdk-ts/src/host_api.ts
  • sdk/extension-sdk-ts/src/testing/mock_transport.ts
  • sdk/extension-sdk-ts/src/testing/test_harness.ts
  • sdk/extension-sdk-ts/src/transport.ts
  • sdk/extension-sdk-ts/src/types.ts
  • sdk/extension/doc.go
  • sdk/extension/testing/doc.go
✅ Files skipped from review due to trivial changes (3)
  • internal/core/extension/assets_test.go
  • internal/cli/extension/doctor_test.go
  • internal/core/extension/dispatcher.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • Makefile
  • internal/core/agent/registry_overlay_test.go
  • internal/core/agent/hooks.go
  • internal/cli/extension/doctor.go
  • internal/cli/extension/display.go
  • internal/core/extension/chain.go
  • internal/cli/extension/install.go

Comment thread internal/cli/makefile_publish_test.go
Comment thread internal/core/agent/client.go
Comment thread internal/core/agent/registry_overlay.go
Comment thread internal/core/extension/dispatcher_test.go Outdated
Comment thread internal/core/extension/dispatcher_test.go
@coderabbitai coderabbitai Bot mentioned this pull request Apr 30, 2026
@coderabbitai coderabbitai Bot mentioned this pull request May 2, 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