oh-my-opencode 2.7.0 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4382,7 +4382,7 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
4382
4382
  - Do not stop until all tasks are done`;
4383
4383
  var COUNTDOWN_SECONDS = 2;
4384
4384
  var TOAST_DURATION_MS = 900;
4385
- var MIN_INJECTION_INTERVAL_MS = 1e4;
4385
+ var ERROR_COOLDOWN_MS = 3000;
4386
4386
  function getMessageDir(sessionID) {
4387
4387
  if (!existsSync6(MESSAGE_STORAGE))
4388
4388
  return null;
@@ -4396,7 +4396,7 @@ function getMessageDir(sessionID) {
4396
4396
  }
4397
4397
  return null;
4398
4398
  }
4399
- function detectInterrupt(error) {
4399
+ function isAbortError(error) {
4400
4400
  if (!error)
4401
4401
  return false;
4402
4402
  if (typeof error === "object") {
@@ -4422,47 +4422,41 @@ function getIncompleteCount(todos) {
4422
4422
  function createTodoContinuationEnforcer(ctx, options = {}) {
4423
4423
  const { backgroundManager } = options;
4424
4424
  const sessions = new Map;
4425
- function getOrCreateState(sessionID) {
4425
+ function getState(sessionID) {
4426
4426
  let state2 = sessions.get(sessionID);
4427
4427
  if (!state2) {
4428
- state2 = { version: 0, mode: "idle" };
4428
+ state2 = {};
4429
4429
  sessions.set(sessionID, state2);
4430
4430
  }
4431
4431
  return state2;
4432
4432
  }
4433
- function clearTimer(state2) {
4434
- if (state2.timer) {
4435
- clearTimeout(state2.timer);
4436
- state2.timer = undefined;
4437
- }
4438
- }
4439
- function invalidate(sessionID, reason) {
4433
+ function cancelCountdown(sessionID) {
4440
4434
  const state2 = sessions.get(sessionID);
4441
4435
  if (!state2)
4442
4436
  return;
4443
- if (state2.mode === "recovering")
4444
- return;
4445
- state2.version++;
4446
- clearTimer(state2);
4447
- if (state2.mode !== "idle" && state2.mode !== "errorBypass") {
4448
- log(`[${HOOK_NAME}] Invalidated`, { sessionID, reason, prevMode: state2.mode, newVersion: state2.version });
4449
- state2.mode = "idle";
4437
+ if (state2.countdownTimer) {
4438
+ clearTimeout(state2.countdownTimer);
4439
+ state2.countdownTimer = undefined;
4440
+ }
4441
+ if (state2.countdownInterval) {
4442
+ clearInterval(state2.countdownInterval);
4443
+ state2.countdownInterval = undefined;
4450
4444
  }
4451
4445
  }
4452
- function isMainSession(sessionID) {
4453
- const mainSessionID2 = getMainSessionID();
4454
- return !mainSessionID2 || sessionID === mainSessionID2;
4446
+ function cleanup(sessionID) {
4447
+ cancelCountdown(sessionID);
4448
+ sessions.delete(sessionID);
4455
4449
  }
4456
4450
  const markRecovering = (sessionID) => {
4457
- const state2 = getOrCreateState(sessionID);
4458
- invalidate(sessionID, "entering recovery mode");
4459
- state2.mode = "recovering";
4451
+ const state2 = getState(sessionID);
4452
+ state2.isRecovering = true;
4453
+ cancelCountdown(sessionID);
4460
4454
  log(`[${HOOK_NAME}] Session marked as recovering`, { sessionID });
4461
4455
  };
4462
4456
  const markRecoveryComplete = (sessionID) => {
4463
4457
  const state2 = sessions.get(sessionID);
4464
- if (state2 && state2.mode === "recovering") {
4465
- state2.mode = "idle";
4458
+ if (state2) {
4459
+ state2.isRecovering = false;
4466
4460
  log(`[${HOOK_NAME}] Session recovery complete`, { sessionID });
4467
4461
  }
4468
4462
  };
@@ -4476,101 +4470,51 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4476
4470
  }
4477
4471
  }).catch(() => {});
4478
4472
  }
4479
- async function executeInjection(sessionID, capturedVersion) {
4473
+ async function injectContinuation(sessionID, incompleteCount, total) {
4480
4474
  const state2 = sessions.get(sessionID);
4481
- if (!state2)
4482
- return;
4483
- if (state2.version !== capturedVersion) {
4484
- log(`[${HOOK_NAME}] Injection aborted: version mismatch`, {
4485
- sessionID,
4486
- capturedVersion,
4487
- currentVersion: state2.version
4488
- });
4475
+ if (state2?.isRecovering) {
4476
+ log(`[${HOOK_NAME}] Skipped injection: in recovery`, { sessionID });
4489
4477
  return;
4490
4478
  }
4491
- if (state2.mode !== "countingDown") {
4492
- log(`[${HOOK_NAME}] Injection aborted: mode changed`, {
4493
- sessionID,
4494
- mode: state2.mode
4495
- });
4479
+ if (state2?.lastErrorAt && Date.now() - state2.lastErrorAt < ERROR_COOLDOWN_MS) {
4480
+ log(`[${HOOK_NAME}] Skipped injection: recent error`, { sessionID });
4496
4481
  return;
4497
4482
  }
4498
- if (state2.lastAttemptedAt) {
4499
- const elapsed = Date.now() - state2.lastAttemptedAt;
4500
- if (elapsed < MIN_INJECTION_INTERVAL_MS) {
4501
- log(`[${HOOK_NAME}] Injection throttled: too soon since last injection`, {
4502
- sessionID,
4503
- elapsedMs: elapsed,
4504
- minIntervalMs: MIN_INJECTION_INTERVAL_MS
4505
- });
4506
- state2.mode = "idle";
4507
- return;
4508
- }
4483
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4484
+ if (hasRunningBgTasks) {
4485
+ log(`[${HOOK_NAME}] Skipped injection: background tasks running`, { sessionID });
4486
+ return;
4509
4487
  }
4510
- state2.mode = "injecting";
4511
4488
  let todos = [];
4512
4489
  try {
4513
4490
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
4514
4491
  todos = response.data ?? response;
4515
4492
  } catch (err) {
4516
- log(`[${HOOK_NAME}] Failed to fetch todos for injection`, { sessionID, error: String(err) });
4517
- state2.mode = "idle";
4518
- return;
4519
- }
4520
- if (state2.version !== capturedVersion) {
4521
- log(`[${HOOK_NAME}] Injection aborted after todo fetch: version mismatch`, { sessionID });
4522
- state2.mode = "idle";
4493
+ log(`[${HOOK_NAME}] Failed to fetch todos`, { sessionID, error: String(err) });
4523
4494
  return;
4524
4495
  }
4525
- const incompleteCount = getIncompleteCount(todos);
4526
- if (incompleteCount === 0) {
4527
- log(`[${HOOK_NAME}] No incomplete todos at injection time`, { sessionID, total: todos.length });
4528
- state2.mode = "idle";
4529
- return;
4530
- }
4531
- const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4532
- if (hasRunningBgTasks) {
4533
- log(`[${HOOK_NAME}] Skipped: background tasks still running`, { sessionID });
4534
- state2.mode = "idle";
4496
+ const freshIncompleteCount = getIncompleteCount(todos);
4497
+ if (freshIncompleteCount === 0) {
4498
+ log(`[${HOOK_NAME}] Skipped injection: no incomplete todos`, { sessionID });
4535
4499
  return;
4536
4500
  }
4537
4501
  const messageDir = getMessageDir(sessionID);
4538
4502
  const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
4539
- const agentHasWritePermission = !prevMessage?.tools || prevMessage.tools.write !== false && prevMessage.tools.edit !== false;
4540
- if (!agentHasWritePermission) {
4541
- log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, {
4542
- sessionID,
4543
- agent: prevMessage?.agent,
4544
- tools: prevMessage?.tools
4545
- });
4546
- state2.mode = "idle";
4503
+ const hasWritePermission = !prevMessage?.tools || prevMessage.tools.write !== false && prevMessage.tools.edit !== false;
4504
+ if (!hasWritePermission) {
4505
+ log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: prevMessage?.agent });
4547
4506
  return;
4548
4507
  }
4549
4508
  const agentName = prevMessage?.agent?.toLowerCase() ?? "";
4550
- const isPlanModeAgent = agentName === "plan" || agentName === "planner-sisyphus";
4551
- if (isPlanModeAgent) {
4552
- log(`[${HOOK_NAME}] Skipped: plan mode agent detected`, {
4553
- sessionID,
4554
- agent: prevMessage?.agent
4555
- });
4556
- state2.mode = "idle";
4509
+ if (agentName === "plan" || agentName === "planner-sisyphus") {
4510
+ log(`[${HOOK_NAME}] Skipped: plan mode agent`, { sessionID, agent: prevMessage?.agent });
4557
4511
  return;
4558
4512
  }
4559
4513
  const prompt = `${CONTINUATION_PROMPT}
4560
4514
 
4561
- [Status: ${todos.length - incompleteCount}/${todos.length} completed, ${incompleteCount} remaining]`;
4562
- if (state2.version !== capturedVersion) {
4563
- log(`[${HOOK_NAME}] Injection aborted: version changed before API call`, { sessionID });
4564
- state2.mode = "idle";
4565
- return;
4566
- }
4567
- state2.lastAttemptedAt = Date.now();
4515
+ [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]`;
4568
4516
  try {
4569
- log(`[${HOOK_NAME}] Injecting continuation prompt`, {
4570
- sessionID,
4571
- agent: prevMessage?.agent,
4572
- incompleteCount
4573
- });
4517
+ log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: prevMessage?.agent, incompleteCount: freshIncompleteCount });
4574
4518
  await ctx.client.session.prompt({
4575
4519
  path: { id: sessionID },
4576
4520
  body: {
@@ -4579,41 +4523,27 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4579
4523
  },
4580
4524
  query: { directory: ctx.directory }
4581
4525
  });
4582
- log(`[${HOOK_NAME}] Continuation prompt injected successfully`, { sessionID });
4526
+ log(`[${HOOK_NAME}] Injection successful`, { sessionID });
4583
4527
  } catch (err) {
4584
- log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) });
4528
+ log(`[${HOOK_NAME}] Injection failed`, { sessionID, error: String(err) });
4585
4529
  }
4586
- state2.mode = "idle";
4587
4530
  }
4588
- function startCountdown(sessionID, incompleteCount) {
4589
- const state2 = getOrCreateState(sessionID);
4590
- invalidate(sessionID, "starting new countdown");
4591
- state2.version++;
4592
- state2.mode = "countingDown";
4593
- const capturedVersion = state2.version;
4594
- log(`[${HOOK_NAME}] Starting countdown`, {
4595
- sessionID,
4596
- seconds: COUNTDOWN_SECONDS,
4597
- version: capturedVersion,
4598
- incompleteCount
4599
- });
4600
- showCountdownToast(COUNTDOWN_SECONDS, incompleteCount);
4531
+ function startCountdown(sessionID, incompleteCount, total) {
4532
+ const state2 = getState(sessionID);
4533
+ cancelCountdown(sessionID);
4601
4534
  let secondsRemaining = COUNTDOWN_SECONDS;
4602
- const toastInterval = setInterval(() => {
4603
- if (state2.version !== capturedVersion) {
4604
- clearInterval(toastInterval);
4605
- return;
4606
- }
4535
+ showCountdownToast(secondsRemaining, incompleteCount);
4536
+ state2.countdownInterval = setInterval(() => {
4607
4537
  secondsRemaining--;
4608
4538
  if (secondsRemaining > 0) {
4609
4539
  showCountdownToast(secondsRemaining, incompleteCount);
4610
4540
  }
4611
4541
  }, 1000);
4612
- state2.timer = setTimeout(() => {
4613
- clearInterval(toastInterval);
4614
- clearTimer(state2);
4615
- executeInjection(sessionID, capturedVersion);
4542
+ state2.countdownTimer = setTimeout(() => {
4543
+ cancelCountdown(sessionID);
4544
+ injectContinuation(sessionID, incompleteCount, total);
4616
4545
  }, COUNTDOWN_SECONDS * 1000);
4546
+ log(`[${HOOK_NAME}] Countdown started`, { sessionID, seconds: COUNTDOWN_SECONDS, incompleteCount });
4617
4547
  }
4618
4548
  const handler = async ({ event }) => {
4619
4549
  const props = event.properties;
@@ -4621,33 +4551,36 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4621
4551
  const sessionID = props?.sessionID;
4622
4552
  if (!sessionID)
4623
4553
  return;
4624
- const isInterrupt = detectInterrupt(props?.error);
4625
- const state2 = getOrCreateState(sessionID);
4626
- invalidate(sessionID, isInterrupt ? "user interrupt" : "session error");
4627
- state2.mode = "errorBypass";
4628
- log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error });
4554
+ const state2 = getState(sessionID);
4555
+ state2.lastErrorAt = Date.now();
4556
+ cancelCountdown(sessionID);
4557
+ log(`[${HOOK_NAME}] session.error`, { sessionID, isAbort: isAbortError(props?.error) });
4629
4558
  return;
4630
4559
  }
4631
4560
  if (event.type === "session.idle") {
4632
4561
  const sessionID = props?.sessionID;
4633
4562
  if (!sessionID)
4634
4563
  return;
4635
- log(`[${HOOK_NAME}] session.idle received`, { sessionID });
4636
- if (!isMainSession(sessionID)) {
4637
- log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID });
4564
+ log(`[${HOOK_NAME}] session.idle`, { sessionID });
4565
+ const mainSessionID2 = getMainSessionID();
4566
+ const isMainSession = sessionID === mainSessionID2;
4567
+ const isBackgroundTaskSession = subagentSessions.has(sessionID);
4568
+ if (mainSessionID2 && !isMainSession && !isBackgroundTaskSession) {
4569
+ log(`[${HOOK_NAME}] Skipped: not main or background task session`, { sessionID });
4638
4570
  return;
4639
4571
  }
4640
- const state2 = getOrCreateState(sessionID);
4641
- if (state2.mode === "recovering") {
4642
- log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID });
4572
+ const state2 = getState(sessionID);
4573
+ if (state2.isRecovering) {
4574
+ log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
4643
4575
  return;
4644
4576
  }
4645
- if (state2.mode === "errorBypass") {
4646
- log(`[${HOOK_NAME}] Skipped: error bypass (awaiting user message to resume)`, { sessionID });
4577
+ if (state2.lastErrorAt && Date.now() - state2.lastErrorAt < ERROR_COOLDOWN_MS) {
4578
+ log(`[${HOOK_NAME}] Skipped: recent error (cooldown)`, { sessionID });
4647
4579
  return;
4648
4580
  }
4649
- if (state2.mode === "countingDown" || state2.mode === "injecting") {
4650
- log(`[${HOOK_NAME}] Skipped: already ${state2.mode}`, { sessionID });
4581
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4582
+ if (hasRunningBgTasks) {
4583
+ log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID });
4651
4584
  return;
4652
4585
  }
4653
4586
  let todos = [];
@@ -4655,54 +4588,37 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4655
4588
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
4656
4589
  todos = response.data ?? response;
4657
4590
  } catch (err) {
4658
- log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) });
4591
+ log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(err) });
4659
4592
  return;
4660
4593
  }
4661
4594
  if (!todos || todos.length === 0) {
4662
- log(`[${HOOK_NAME}] No todos found`, { sessionID });
4595
+ log(`[${HOOK_NAME}] No todos`, { sessionID });
4663
4596
  return;
4664
4597
  }
4665
4598
  const incompleteCount = getIncompleteCount(todos);
4666
4599
  if (incompleteCount === 0) {
4667
- log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length });
4668
- return;
4669
- }
4670
- const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((t) => t.status === "running") : false;
4671
- if (hasRunningBgTasks) {
4672
- log(`[${HOOK_NAME}] Skipped: background tasks still running`, { sessionID });
4600
+ log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
4673
4601
  return;
4674
4602
  }
4675
- log(`[${HOOK_NAME}] Found incomplete todos`, {
4676
- sessionID,
4677
- incomplete: incompleteCount,
4678
- total: todos.length
4679
- });
4680
- startCountdown(sessionID, incompleteCount);
4603
+ startCountdown(sessionID, incompleteCount, todos.length);
4681
4604
  return;
4682
4605
  }
4683
4606
  if (event.type === "message.updated") {
4684
4607
  const info = props?.info;
4685
4608
  const sessionID = info?.sessionID;
4686
4609
  const role = info?.role;
4687
- const finish = info?.finish;
4688
4610
  if (!sessionID)
4689
4611
  return;
4690
4612
  if (role === "user") {
4691
4613
  const state2 = sessions.get(sessionID);
4692
- if (state2?.mode === "errorBypass") {
4693
- state2.mode = "idle";
4694
- log(`[${HOOK_NAME}] User message cleared errorBypass mode`, { sessionID });
4614
+ if (state2) {
4615
+ state2.lastErrorAt = undefined;
4695
4616
  }
4696
- invalidate(sessionID, "user message received");
4697
- return;
4617
+ cancelCountdown(sessionID);
4618
+ log(`[${HOOK_NAME}] User message: cleared error state`, { sessionID });
4698
4619
  }
4699
- if (role === "assistant" && !finish) {
4700
- invalidate(sessionID, "assistant is working (streaming)");
4701
- return;
4702
- }
4703
- if (role === "assistant" && finish) {
4704
- log(`[${HOOK_NAME}] Assistant turn finished`, { sessionID, finish });
4705
- return;
4620
+ if (role === "assistant") {
4621
+ cancelCountdown(sessionID);
4706
4622
  }
4707
4623
  return;
4708
4624
  }
@@ -4711,26 +4627,22 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
4711
4627
  const sessionID = info?.sessionID;
4712
4628
  const role = info?.role;
4713
4629
  if (sessionID && role === "assistant") {
4714
- invalidate(sessionID, "assistant streaming");
4630
+ cancelCountdown(sessionID);
4715
4631
  }
4716
4632
  return;
4717
4633
  }
4718
4634
  if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
4719
4635
  const sessionID = props?.sessionID;
4720
4636
  if (sessionID) {
4721
- invalidate(sessionID, `tool execution (${event.type})`);
4637
+ cancelCountdown(sessionID);
4722
4638
  }
4723
4639
  return;
4724
4640
  }
4725
4641
  if (event.type === "session.deleted") {
4726
4642
  const sessionInfo = props?.info;
4727
4643
  if (sessionInfo?.id) {
4728
- const state2 = sessions.get(sessionInfo.id);
4729
- if (state2) {
4730
- clearTimer(state2);
4731
- }
4732
- sessions.delete(sessionInfo.id);
4733
- log(`[${HOOK_NAME}] Session deleted, state cleaned up`, { sessionID: sessionInfo.id });
4644
+ cleanup(sessionInfo.id);
4645
+ log(`[${HOOK_NAME}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
4734
4646
  }
4735
4647
  return;
4736
4648
  }
@@ -11080,8 +10992,11 @@ function createEmptyMessageSanitizerHook() {
11080
10992
  return {
11081
10993
  "experimental.chat.messages.transform": async (_input, output) => {
11082
10994
  const { messages } = output;
11083
- for (const message of messages) {
11084
- if (message.info.role === "user")
10995
+ for (let i = 0;i < messages.length; i++) {
10996
+ const message = messages[i];
10997
+ const isLastMessage = i === messages.length - 1;
10998
+ const isAssistant = message.info.role === "assistant";
10999
+ if (isLastMessage && isAssistant)
11085
11000
  continue;
11086
11001
  const parts = message.parts;
11087
11002
  if (!hasValidContent(parts)) {
@@ -13135,6 +13050,7 @@ Generate comprehensive AGENTS.md files across project hierarchy. Combines root-l
13135
13050
  - **Predict-then-Compare**: Predict standard \u2192 find actual \u2192 document ONLY deviations
13136
13051
  - **Hierarchy Aware**: Parent covers general, children cover specific
13137
13052
  - **No Redundancy**: Child AGENTS.md NEVER repeats parent content
13053
+ - **LSP-First**: Use LSP tools for accurate code intelligence when available (semantic > text search)
13138
13054
 
13139
13055
  ---
13140
13056
 
@@ -13197,6 +13113,53 @@ background_task(agent="explore", prompt="Build/CI: FIND .github/workflows, Makef
13197
13113
  background_task(agent="explore", prompt="Test patterns: FIND pytest.ini, jest.config, test structure \u2192 REPORT unique testing conventions")
13198
13114
  \`\`\`
13199
13115
 
13116
+ ### Code Intelligence Analysis (LSP tools - run in parallel)
13117
+
13118
+ LSP provides semantic understanding beyond text search. Use for accurate code mapping.
13119
+
13120
+ \`\`\`
13121
+ # Step 1: Check LSP availability
13122
+ lsp_servers() # Verify language server is available
13123
+
13124
+ # Step 2: Analyze entry point files (run in parallel)
13125
+ # Find entry points first, then analyze each with lsp_document_symbols
13126
+ lsp_document_symbols(filePath="src/index.ts") # Main entry
13127
+ lsp_document_symbols(filePath="src/main.py") # Python entry
13128
+ lsp_document_symbols(filePath="cmd/main.go") # Go entry
13129
+
13130
+ # Step 3: Discover key symbols across workspace (run in parallel)
13131
+ lsp_workspace_symbols(filePath=".", query="class") # All classes
13132
+ lsp_workspace_symbols(filePath=".", query="interface") # All interfaces
13133
+ lsp_workspace_symbols(filePath=".", query="function") # Top-level functions
13134
+ lsp_workspace_symbols(filePath=".", query="type") # Type definitions
13135
+
13136
+ # Step 4: Analyze symbol centrality (for top 5-10 key symbols)
13137
+ # High reference count = central/important concept
13138
+ lsp_find_references(filePath="src/index.ts", line=X, character=Y) # Main export
13139
+ \`\`\`
13140
+
13141
+ #### LSP Analysis Output Format
13142
+
13143
+ \`\`\`
13144
+ CODE_INTELLIGENCE = {
13145
+ entry_points: [
13146
+ { file: "src/index.ts", exports: ["Plugin", "createHook"], symbol_count: 12 }
13147
+ ],
13148
+ key_symbols: [
13149
+ { name: "Plugin", type: "class", file: "src/index.ts", refs: 45, role: "Central orchestrator" },
13150
+ { name: "createHook", type: "function", file: "src/utils.ts", refs: 23, role: "Hook factory" }
13151
+ ],
13152
+ module_boundaries: [
13153
+ { dir: "src/hooks", exports: 21, imports_from: ["shared/"] },
13154
+ { dir: "src/tools", exports: 15, imports_from: ["shared/", "hooks/"] }
13155
+ ]
13156
+ }
13157
+ \`\`\`
13158
+
13159
+ <critical>
13160
+ **LSP Fallback**: If LSP unavailable (no server installed), skip this section and rely on explore agents + AST-grep patterns.
13161
+ </critical>
13162
+
13200
13163
  </parallel-tasks>
13201
13164
 
13202
13165
  **Collect all results. Mark "p1-analysis" as completed.**
@@ -13209,13 +13172,35 @@ background_task(agent="explore", prompt="Test patterns: FIND pytest.ini, jest.co
13209
13172
 
13210
13173
  ### Scoring Matrix
13211
13174
 
13212
- | Factor | Weight | Threshold |
13213
- |--------|--------|-----------|
13214
- | File count | 3x | >20 files = high |
13215
- | Subdirectory count | 2x | >5 subdirs = high |
13216
- | Code file ratio | 2x | >70% code = high |
13217
- | Unique patterns | 1x | Has own config |
13218
- | Module boundary | 2x | Has __init__.py/index.ts |
13175
+ | Factor | Weight | Threshold | Source |
13176
+ |--------|--------|-----------|--------|
13177
+ | File count | 3x | >20 files = high | bash |
13178
+ | Subdirectory count | 2x | >5 subdirs = high | bash |
13179
+ | Code file ratio | 2x | >70% code = high | bash |
13180
+ | Unique patterns | 1x | Has own config | explore |
13181
+ | Module boundary | 2x | Has __init__.py/index.ts | bash |
13182
+ | **Symbol density** | 2x | >30 symbols = high | LSP |
13183
+ | **Export count** | 2x | >10 exports = high | LSP |
13184
+ | **Reference centrality** | 3x | Symbols with >20 refs | LSP |
13185
+
13186
+ <lsp-scoring>
13187
+ **LSP-Enhanced Scoring** (if available):
13188
+
13189
+ \`\`\`
13190
+ For each directory in candidates:
13191
+ symbols = lsp_document_symbols(dir/index.ts or dir/__init__.py)
13192
+
13193
+ symbol_score = len(symbols) > 30 ? 6 : len(symbols) > 15 ? 3 : 0
13194
+ export_score = count(exported symbols) > 10 ? 4 : 0
13195
+
13196
+ # Check if this module is central (many things depend on it)
13197
+ for each exported symbol:
13198
+ refs = lsp_find_references(symbol)
13199
+ if refs > 20: centrality_score += 3
13200
+
13201
+ total_score += symbol_score + export_score + centrality_score
13202
+ \`\`\`
13203
+ </lsp-scoring>
13219
13204
 
13220
13205
  ### Decision Rules
13221
13206
 
@@ -13273,6 +13258,28 @@ Root AGENTS.md gets **full treatment** with Predict-then-Compare synthesis.
13273
13258
  |------|----------|-------|
13274
13259
  | Add feature X | \\\`src/x/\\\` | {pattern hint} |
13275
13260
 
13261
+ ## CODE MAP
13262
+
13263
+ {Generated from LSP analysis - shows key symbols and their relationships}
13264
+
13265
+ | Symbol | Type | Location | Refs | Role |
13266
+ |--------|------|----------|------|------|
13267
+ | {MainClass} | Class | \\\`src/index.ts\\\` | {N} | {Central orchestrator} |
13268
+ | {createX} | Function | \\\`src/utils.ts\\\` | {N} | {Factory pattern} |
13269
+ | {Config} | Interface | \\\`src/types.ts\\\` | {N} | {Configuration contract} |
13270
+
13271
+ ### Module Dependencies
13272
+
13273
+ \\\`\\\`\\\`
13274
+ {entry} \u2500\u2500imports\u2500\u2500> {core/}
13275
+ \u2502 \u2502
13276
+ \u2514\u2500\u2500imports\u2500\u2500> {utils/} <\u2500\u2500imports\u2500\u2500 {features/}
13277
+ \\\`\\\`\\\`
13278
+
13279
+ <code-map-note>
13280
+ **Skip CODE MAP if**: LSP unavailable OR project too small (<10 files) OR no clear module boundaries.
13281
+ </code-map-note>
13282
+
13276
13283
  ## CONVENTIONS
13277
13284
 
13278
13285
  {ONLY deviations from standard - skip generic advice}
@@ -13413,7 +13420,10 @@ Hierarchy:
13413
13420
  - **Generic content**: Remove anything that applies to ALL projects
13414
13421
  - **Sequential execution**: MUST use parallel agents
13415
13422
  - **Deep nesting**: Rarely need AGENTS.md at depth 4+
13416
- - **Verbose style**: "This directory contains..." \u2192 just list it`;
13423
+ - **Verbose style**: "This directory contains..." \u2192 just list it
13424
+ - **Ignoring LSP**: If LSP available, USE IT - semantic analysis > text grep
13425
+ - **LSP without fallback**: Always have explore agent backup if LSP unavailable
13426
+ - **Over-referencing**: Don't trace refs for EVERY symbol - focus on exports only`;
13417
13427
 
13418
13428
  // src/features/builtin-commands/commands.ts
13419
13429
  var BUILTIN_COMMAND_DEFINITIONS = {
@@ -14446,6 +14456,10 @@ function isServerInstalled(command) {
14446
14456
  if (command.length === 0)
14447
14457
  return false;
14448
14458
  const cmd = command[0];
14459
+ if (cmd.includes("/") || cmd.includes("\\")) {
14460
+ if (existsSync34(cmd))
14461
+ return true;
14462
+ }
14449
14463
  const isWindows2 = process.platform === "win32";
14450
14464
  const ext = isWindows2 ? ".exe" : "";
14451
14465
  const pathEnv = process.env.PATH || "";
@@ -14470,6 +14484,9 @@ function isServerInstalled(command) {
14470
14484
  return true;
14471
14485
  }
14472
14486
  }
14487
+ if (cmd === "bun" || cmd === "node") {
14488
+ return true;
14489
+ }
14473
14490
  return false;
14474
14491
  }
14475
14492
  function getAllServers() {
@@ -28558,7 +28575,7 @@ var ast_grep_replace = tool({
28558
28575
  }
28559
28576
  });
28560
28577
  // src/tools/grep/cli.ts
28561
- var {spawn: spawn8 } = globalThis.Bun;
28578
+ var {spawn: spawn9 } = globalThis.Bun;
28562
28579
 
28563
28580
  // src/tools/grep/constants.ts
28564
28581
  import { existsSync as existsSync40 } from "fs";
@@ -28568,6 +28585,31 @@ import { spawnSync } from "child_process";
28568
28585
  // src/tools/grep/downloader.ts
28569
28586
  import { existsSync as existsSync39, mkdirSync as mkdirSync11, chmodSync as chmodSync3, unlinkSync as unlinkSync10, readdirSync as readdirSync14 } from "fs";
28570
28587
  import { join as join47 } from "path";
28588
+ var {spawn: spawn8 } = globalThis.Bun;
28589
+ function findFileRecursive(dir, filename) {
28590
+ try {
28591
+ const entries = readdirSync14(dir, { withFileTypes: true, recursive: true });
28592
+ for (const entry of entries) {
28593
+ if (entry.isFile() && entry.name === filename) {
28594
+ return join47(entry.parentPath ?? dir, entry.name);
28595
+ }
28596
+ }
28597
+ } catch {
28598
+ return null;
28599
+ }
28600
+ return null;
28601
+ }
28602
+ var RG_VERSION = "14.1.1";
28603
+ var PLATFORM_CONFIG = {
28604
+ "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
28605
+ "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
28606
+ "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
28607
+ "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
28608
+ "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }
28609
+ };
28610
+ function getPlatformKey() {
28611
+ return `${process.arch}-${process.platform}`;
28612
+ }
28571
28613
  function getInstallDir() {
28572
28614
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
28573
28615
  return join47(homeDir, ".cache", "oh-my-opencode", "bin");
@@ -28576,6 +28618,110 @@ function getRgPath() {
28576
28618
  const isWindows2 = process.platform === "win32";
28577
28619
  return join47(getInstallDir(), isWindows2 ? "rg.exe" : "rg");
28578
28620
  }
28621
+ async function downloadFile(url2, destPath) {
28622
+ const response2 = await fetch(url2);
28623
+ if (!response2.ok) {
28624
+ throw new Error(`Failed to download: ${response2.status} ${response2.statusText}`);
28625
+ }
28626
+ const buffer = await response2.arrayBuffer();
28627
+ await Bun.write(destPath, buffer);
28628
+ }
28629
+ async function extractTarGz2(archivePath, destDir) {
28630
+ const platformKey = getPlatformKey();
28631
+ const args = ["tar", "-xzf", archivePath, "--strip-components=1"];
28632
+ if (platformKey.endsWith("-darwin")) {
28633
+ args.push("--include=*/rg");
28634
+ } else if (platformKey.endsWith("-linux")) {
28635
+ args.push("--wildcards", "*/rg");
28636
+ }
28637
+ const proc = spawn8(args, {
28638
+ cwd: destDir,
28639
+ stdout: "pipe",
28640
+ stderr: "pipe"
28641
+ });
28642
+ const exitCode = await proc.exited;
28643
+ if (exitCode !== 0) {
28644
+ const stderr = await new Response(proc.stderr).text();
28645
+ throw new Error(`Failed to extract tar.gz: ${stderr}`);
28646
+ }
28647
+ }
28648
+ async function extractZipWindows(archivePath, destDir) {
28649
+ const proc = spawn8(["powershell", "-Command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`], { stdout: "pipe", stderr: "pipe" });
28650
+ const exitCode = await proc.exited;
28651
+ if (exitCode !== 0) {
28652
+ throw new Error("Failed to extract zip with PowerShell");
28653
+ }
28654
+ const foundPath = findFileRecursive(destDir, "rg.exe");
28655
+ if (foundPath) {
28656
+ const destPath = join47(destDir, "rg.exe");
28657
+ if (foundPath !== destPath) {
28658
+ const { renameSync } = await import("fs");
28659
+ renameSync(foundPath, destPath);
28660
+ }
28661
+ }
28662
+ }
28663
+ async function extractZipUnix(archivePath, destDir) {
28664
+ const proc = spawn8(["unzip", "-o", archivePath, "-d", destDir], {
28665
+ stdout: "pipe",
28666
+ stderr: "pipe"
28667
+ });
28668
+ const exitCode = await proc.exited;
28669
+ if (exitCode !== 0) {
28670
+ throw new Error("Failed to extract zip");
28671
+ }
28672
+ const foundPath = findFileRecursive(destDir, "rg");
28673
+ if (foundPath) {
28674
+ const destPath = join47(destDir, "rg");
28675
+ if (foundPath !== destPath) {
28676
+ const { renameSync } = await import("fs");
28677
+ renameSync(foundPath, destPath);
28678
+ }
28679
+ }
28680
+ }
28681
+ async function extractZip3(archivePath, destDir) {
28682
+ if (process.platform === "win32") {
28683
+ await extractZipWindows(archivePath, destDir);
28684
+ } else {
28685
+ await extractZipUnix(archivePath, destDir);
28686
+ }
28687
+ }
28688
+ async function downloadAndInstallRipgrep() {
28689
+ const platformKey = getPlatformKey();
28690
+ const config3 = PLATFORM_CONFIG[platformKey];
28691
+ if (!config3) {
28692
+ throw new Error(`Unsupported platform: ${platformKey}`);
28693
+ }
28694
+ const installDir = getInstallDir();
28695
+ const rgPath = getRgPath();
28696
+ if (existsSync39(rgPath)) {
28697
+ return rgPath;
28698
+ }
28699
+ mkdirSync11(installDir, { recursive: true });
28700
+ const filename = `ripgrep-${RG_VERSION}-${config3.platform}.${config3.extension}`;
28701
+ const url2 = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
28702
+ const archivePath = join47(installDir, filename);
28703
+ try {
28704
+ await downloadFile(url2, archivePath);
28705
+ if (config3.extension === "tar.gz") {
28706
+ await extractTarGz2(archivePath, installDir);
28707
+ } else {
28708
+ await extractZip3(archivePath, installDir);
28709
+ }
28710
+ if (process.platform !== "win32") {
28711
+ chmodSync3(rgPath, 493);
28712
+ }
28713
+ if (!existsSync39(rgPath)) {
28714
+ throw new Error("ripgrep binary not found after extraction");
28715
+ }
28716
+ return rgPath;
28717
+ } finally {
28718
+ if (existsSync39(archivePath)) {
28719
+ try {
28720
+ unlinkSync10(archivePath);
28721
+ } catch {}
28722
+ }
28723
+ }
28724
+ }
28579
28725
  function getInstalledRipgrepPath() {
28580
28726
  const rgPath = getRgPath();
28581
28727
  return existsSync39(rgPath) ? rgPath : null;
@@ -28583,6 +28729,7 @@ function getInstalledRipgrepPath() {
28583
28729
 
28584
28730
  // src/tools/grep/constants.ts
28585
28731
  var cachedCli = null;
28732
+ var autoInstallAttempted = false;
28586
28733
  function findExecutable(name) {
28587
28734
  const isWindows2 = process.platform === "win32";
28588
28735
  const cmd = isWindows2 ? "where" : "which";
@@ -28601,6 +28748,7 @@ function getOpenCodeBundledRg() {
28601
28748
  const isWindows2 = process.platform === "win32";
28602
28749
  const rgName = isWindows2 ? "rg.exe" : "rg";
28603
28750
  const candidates = [
28751
+ join48(getDataDir(), "opencode", "bin", rgName),
28604
28752
  join48(execDir, rgName),
28605
28753
  join48(execDir, "bin", rgName),
28606
28754
  join48(execDir, "..", "bin", rgName),
@@ -28639,6 +28787,23 @@ function resolveGrepCli() {
28639
28787
  cachedCli = { path: "rg", backend: "rg" };
28640
28788
  return cachedCli;
28641
28789
  }
28790
+ async function resolveGrepCliWithAutoInstall() {
28791
+ const current = resolveGrepCli();
28792
+ if (current.backend === "rg") {
28793
+ return current;
28794
+ }
28795
+ if (autoInstallAttempted) {
28796
+ return current;
28797
+ }
28798
+ autoInstallAttempted = true;
28799
+ try {
28800
+ const rgPath = await downloadAndInstallRipgrep();
28801
+ cachedCli = { path: rgPath, backend: "rg" };
28802
+ return cachedCli;
28803
+ } catch {
28804
+ return current;
28805
+ }
28806
+ }
28642
28807
  var DEFAULT_MAX_DEPTH = 20;
28643
28808
  var DEFAULT_MAX_FILESIZE = "10M";
28644
28809
  var DEFAULT_MAX_COUNT = 500;
@@ -28753,7 +28918,7 @@ async function runRg(options) {
28753
28918
  }
28754
28919
  const paths = options.paths?.length ? options.paths : ["."];
28755
28920
  args.push(...paths);
28756
- const proc = spawn8([cli.path, ...args], {
28921
+ const proc = spawn9([cli.path, ...args], {
28757
28922
  stdout: "pipe",
28758
28923
  stderr: "pipe"
28759
28924
  });
@@ -28855,7 +29020,7 @@ var grep = tool({
28855
29020
  });
28856
29021
 
28857
29022
  // src/tools/glob/cli.ts
28858
- var {spawn: spawn9 } = globalThis.Bun;
29023
+ var {spawn: spawn10 } = globalThis.Bun;
28859
29024
 
28860
29025
  // src/tools/glob/constants.ts
28861
29026
  var DEFAULT_TIMEOUT_MS3 = 60000;
@@ -28893,6 +29058,19 @@ function buildFindArgs(options) {
28893
29058
  }
28894
29059
  return args;
28895
29060
  }
29061
+ function buildPowerShellCommand(options) {
29062
+ const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH2, DEFAULT_MAX_DEPTH2);
29063
+ const paths = options.paths?.length ? options.paths : ["."];
29064
+ const searchPath = paths[0] || ".";
29065
+ const escapedPath = searchPath.replace(/'/g, "''");
29066
+ const escapedPattern = options.pattern.replace(/'/g, "''");
29067
+ let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`;
29068
+ if (options.hidden) {
29069
+ psCommand += " -Force";
29070
+ }
29071
+ psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName";
29072
+ return ["powershell", "-NoProfile", "-Command", psCommand];
29073
+ }
28896
29074
  async function getFileMtime(filePath) {
28897
29075
  try {
28898
29076
  const stats = await stat(filePath);
@@ -28901,21 +29079,33 @@ async function getFileMtime(filePath) {
28901
29079
  return 0;
28902
29080
  }
28903
29081
  }
28904
- async function runRgFiles(options) {
28905
- const cli = resolveGrepCli();
29082
+ async function runRgFiles(options, resolvedCli) {
29083
+ const cli = resolvedCli ?? resolveGrepCli();
28906
29084
  const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
28907
29085
  const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
28908
29086
  const isRg = cli.backend === "rg";
28909
- const args = isRg ? buildRgArgs2(options) : buildFindArgs(options);
28910
- const paths = options.paths?.length ? options.paths : ["."];
29087
+ const isWindows2 = process.platform === "win32";
29088
+ let command;
29089
+ let cwd;
28911
29090
  if (isRg) {
29091
+ const args = buildRgArgs2(options);
29092
+ const paths = options.paths?.length ? options.paths : ["."];
28912
29093
  args.push(...paths);
29094
+ command = [cli.path, ...args];
29095
+ cwd = undefined;
29096
+ } else if (isWindows2) {
29097
+ command = buildPowerShellCommand(options);
29098
+ cwd = undefined;
29099
+ } else {
29100
+ const args = buildFindArgs(options);
29101
+ const paths = options.paths?.length ? options.paths : ["."];
29102
+ cwd = paths[0] || ".";
29103
+ command = [cli.path, ...args];
28913
29104
  }
28914
- const cwd = paths[0] || ".";
28915
- const proc = spawn9([cli.path, ...args], {
29105
+ const proc = spawn10(command, {
28916
29106
  stdout: "pipe",
28917
29107
  stderr: "pipe",
28918
- cwd: isRg ? undefined : cwd
29108
+ cwd
28919
29109
  });
28920
29110
  const timeoutPromise = new Promise((_, reject) => {
28921
29111
  const id = setTimeout(() => {
@@ -28947,7 +29137,14 @@ async function runRgFiles(options) {
28947
29137
  truncated = true;
28948
29138
  break;
28949
29139
  }
28950
- const filePath = isRg ? line : `${cwd}/${line}`;
29140
+ let filePath;
29141
+ if (isRg) {
29142
+ filePath = line;
29143
+ } else if (isWindows2) {
29144
+ filePath = line.trim();
29145
+ } else {
29146
+ filePath = `${cwd}/${line}`;
29147
+ }
28951
29148
  const mtime = await getFileMtime(filePath);
28952
29149
  files.push({ path: filePath, mtime });
28953
29150
  }
@@ -28998,11 +29195,12 @@ var glob = tool({
28998
29195
  },
28999
29196
  execute: async (args) => {
29000
29197
  try {
29198
+ const cli = await resolveGrepCliWithAutoInstall();
29001
29199
  const paths = args.path ? [args.path] : undefined;
29002
29200
  const result = await runRgFiles({
29003
29201
  pattern: args.pattern,
29004
29202
  paths
29005
- });
29203
+ }, cli);
29006
29204
  return formatGlobResult(result);
29007
29205
  } catch (e) {
29008
29206
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
@@ -29718,14 +29916,14 @@ var INTERACTIVE_BASH_DESCRIPTION = `Execute tmux commands. Use "omo-{name}" sess
29718
29916
  Blocked (use bash instead): capture-pane, save-buffer, show-buffer, pipe-pane.`;
29719
29917
 
29720
29918
  // src/tools/interactive-bash/utils.ts
29721
- var {spawn: spawn10 } = globalThis.Bun;
29919
+ var {spawn: spawn11 } = globalThis.Bun;
29722
29920
  var tmuxPath = null;
29723
29921
  var initPromise3 = null;
29724
29922
  async function findTmuxPath() {
29725
29923
  const isWindows2 = process.platform === "win32";
29726
29924
  const cmd = isWindows2 ? "where" : "which";
29727
29925
  try {
29728
- const proc = spawn10([cmd, "tmux"], {
29926
+ const proc = spawn11([cmd, "tmux"], {
29729
29927
  stdout: "pipe",
29730
29928
  stderr: "pipe"
29731
29929
  });
@@ -29739,7 +29937,7 @@ async function findTmuxPath() {
29739
29937
  if (!path7) {
29740
29938
  return null;
29741
29939
  }
29742
- const verifyProc = spawn10([path7, "-V"], {
29940
+ const verifyProc = spawn11([path7, "-V"], {
29743
29941
  stdout: "pipe",
29744
29942
  stderr: "pipe"
29745
29943
  });