Commit Graph

179 Commits

Author SHA1 Message Date
Ashish Huddar 1bb80ef67c
feat: stable session titles via pinnedSummary metadata (#946)
* feat(core): pin first quality summary for stable session titles

When a session's first non-fallback summary is obtained, persist it to
metadata as pinnedSummary. This prevents the dashboard title from drifting
as the agent generates new summaries each turn.

Changes:
- Add pinnedSummary to SessionMetadata type
- Serialize/deserialize in writeMetadata/readMetadata
- Pin in lifecycle manager's checkSession (correct write path)
- Propagate through restore path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(web): stable session titles via unified getSessionTitle

Reorder title fallback: PR > issue > branch > summary (was: summary > issue > branch).
Replace inline getSessionHeadline with shared getSessionTitle in both
SessionDetail and page.tsx for consistent behavior across all components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: coverage for pinnedSummary and title fallback chain

9 new tests:
- 3 serialize tests: pinnedSummary priority over agent/metadata summary
- 5 lifecycle-manager tests: pinning logic guards and error handling
- 1 metadata test: pinnedSummary serialization in writeMetadata

Updated existing tests to match new title fallback order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: fix mobile title expectations

* fix(web): use pinnedSummary only for title selection, not dashboard.summary

Previously, pinnedSummary was assigned to dashboard.summary in
sessionToDashboard(), causing two problems:
1. enrichSessionAgentSummary() never fetched newer summaries because
   dashboard.summary was already set
2. Every UI surface rendering session.summary showed the stale pinned
   value instead of live agent output

Fix: pinnedSummary is now read only by getSessionTitle() via
session.metadata["pinnedSummary"], keeping title stable without
freezing the live summary field. dashboard.summary always reflects the
current agentInfo or metadata summary.

Addresses review comment on PR #946.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:52:48 +05:30
Ashish Huddar 30ee327daf
fix(notifier): remove desktop notifications from default configs (#962)
* fix(notifier): remove desktop notifications from all default configs (#960)

Desktop notifications via osascript open blank Finder windows on macOS
without providing useful information. Remove 'desktop' from all default
notifier lists so new users are not affected. Existing configs that
explicitly list 'desktop' continue to work unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(cli): add coverage for autoCreateConfig default notifiers

Line 585 in start.ts (`notifiers: []`) was not covered by the diff
coverage check. Added a test that exercises `autoCreateConfig` via
`createConfigOnly()`, verifying that the generated config does not
include 'desktop' in `defaults.notifiers`.

Required infrastructure additions:
- Mock `node:process` so `import { cwd } from "node:process"` in
  start.ts can be overridden per-test via `mockProcessCwd`
- Set `detectProjectType` mock to return `{ languages: [], frameworks: [] }`
  instead of null to avoid crash in autoCreateConfig

Closes #960

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(config): disable all notifications by default — fully opt-in

Change DefaultPluginsSchema.notifiers default from ["composio"] to []
and notificationRouting defaults to all empty arrays, so no notifications
are sent unless the user explicitly configures them. Addresses reviewer
feedback that composio notifier should not be active out of the box.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(config): use empty object as notificationRouting default

Empty array entries like { urgent: [] } block the ?? fallback in
notifyHuman (lifecycle-manager.ts:1169) because [] is not null/undefined.
Using {} lets unlisted priorities fall back to defaults.notifiers, so
explicit per-user notifier config is respected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 08:59:51 +05:30
Harsh Batheja e25e9b4967
fix(lifecycle): reduce GitHub API rate limiting from batch enrichment bypass (#906)
* fix(lifecycle): reduce GitHub API rate limiting from batch enrichment bypass

Three optimizations to prevent API storms in the lifecycle manager poll cycle:

1. **CRITICAL - maybeDispatchMergeConflicts**: Gate the getMergeability()
   fallback to only run when batch enrichment didn't run at all. Previously
   it called getMergeability() (3 REST calls) whenever hasConflicts was
   undefined, even when the batch had already fetched PR data. Now uses
   cachedData.hasConflicts ?? false when the batch ran.

2. **HIGH - maybeDispatchCIFailureDetails**: Use batch enrichment ciChecks
   when available instead of calling getCIChecks() (separate REST call)
   on every poll. The GraphQL batch query now fetches statusCheckRollup
   contexts (individual check names, statuses, URLs) alongside the rollup
   state. Falls back to getCIChecks() only when batch didn't run.

3. **MEDIUM - maybeDispatchReviewBacklog**: Throttle getPendingComments +
   getAutomatedComments API calls to at most once per 2 minutes per session.
   These were called every 30s even when nothing had changed.

Impact: ~8-10 API calls/PR/poll reduced to ~2-4, enabling 3-4x more
concurrent sessions before hitting GitHub's 5,000/hr REST limit.

Also extends PREnrichmentData with ciChecks?: CICheck[] and adds
parseCheckContexts() helper to graphql-batch.ts for parsing CheckRun
and StatusContext nodes from the GraphQL statusCheckRollup.contexts field.

* fix(scm-github): fall back to getCIChecks() when contexts list is truncated

When a PR has >20 CI checks, contexts(first: 20) silently truncates the
list. Setting ciChecks to undefined when pageInfo.hasNextPage is true
ensures maybeDispatchCIFailureDetails falls back to the getCIChecks()
REST call, which returns all checks without truncation.

Also adds pageInfo { hasNextPage } to the contexts GraphQL query so
truncation can be detected.

* fix(lifecycle): prune lastReviewBacklogCheckAt in pollAll cleanup loop

Add the new throttle map to the existing pruning loop that removes stale
entries for sessions no longer in the session list. Previously the map
was only cleared on terminal status transitions, leaving orphaned entries
for sessions removed externally (killed + cleaned up without transition).

* fix(lifecycle): bypass throttle on review transition; fix StatusContext conclusion

Two fixes for automated review findings:

1. Bypass review backlog throttle when a transition reaction just fired for
   humanReactionKey or automatedReactionKey. The transitionReaction branch
   needs to read the current fingerprint via the API to record
   lastPendingReviewDispatchHash. Without bypassing, the throttle prevents
   this write and the next unthrottled poll sees a stale (empty) hash,
   clears the reaction tracker, and fires a duplicate dispatch.

2. Set conclusion on StatusContext nodes in parseCheckContexts() to match
   the REST getCIChecksFromStatusRollup() format (rawState.toUpperCase()).
   The CI failure fingerprint includes c.conclusion ?? '', so inconsistent
   conclusion values between GraphQL and REST paths caused phantom fingerprint
   changes when switching sources, triggering duplicate dispatches.

* fix(scm-github): normalize CheckRun conclusion and map NEUTRAL to skipped

Two consistency fixes in parseCheckContexts() vs the REST path:

1. NEUTRAL conclusion: was mapped to 'passed' (with SUCCESS), but
   mapRawCheckStateToStatus() in the REST path maps NEUTRAL to 'skipped'.
   Changed to treat NEUTRAL the same as SKIPPED.

2. CheckRun conclusion: was stored as the raw GraphQL string (may be
   lowercase). REST getCIChecks/getCIChecksFromStatusRollup always store
   conclusion as rawState.toUpperCase(). Now stores rawConclusion which
   is already uppercased during the status branching logic.

Both fixes prevent phantom fingerprint changes when maybeDispatchCIFailureDetails
switches between GraphQL batch and REST fallback across poll cycles.

* fix(scm-github): map STALE/NOT_REQUIRED/NONE conclusions to skipped

parseCheckContexts() was mapping these conclusions to 'failed' via the
else fallback, while mapRawCheckStateToStatus() in the REST path
explicitly maps all of them to 'skipped'. Added them to the skipped
branch alongside SKIPPED and NEUTRAL to fully mirror the REST mapping.

* fix(scm-github): map QUEUED/WAITING to pending not running

parseCheckContexts() mapped QUEUED and WAITING CheckRun statuses to
'running', but mapRawCheckStateToStatus() in the REST path maps both
to 'pending'. Only IN_PROGRESS maps to 'running' in the REST path.

Fixes fingerprint inconsistency when switching between GraphQL batch
and REST fallback across poll cycles.

* fix(scm-github): map STARTUP_FAILURE to skipped; guard null pageInfo

- STARTUP_FAILURE conclusion now falls through to the "skipped" branch
  (matching mapRawCheckStateToStatus() REST default) instead of the
  explicit failure enumeration catch-all
- Null pageInfo guard prevents TypeError from typeof null === "object"
  JavaScript quirk when accessing hasNextPage on a null pageInfo field
- Tests added for both cases

* fix(scm-github): map COMPLETED+null conclusion to skipped not passed

When a CheckRun has status COMPLETED and conclusion null, the REST path's
mapRawCheckStateToStatus() converts it to "" which maps to "skipped".
The GraphQL path was incorrectly mapping it to "passed" via !rawConclusion.
Fix: only map rawConclusion === "SUCCESS" to "passed"; null falls through
to the else branch → "skipped", matching the REST path exactly.
2026-04-06 14:59:00 +05:30
Harsh Batheja 9ac659066a
revert: remove model rate-limit pause functionality (PR #367) (#908)
* Revert "fix: pause workers on model limits and stabilize session visibility (#367)"

This reverts commit 003eb78adb.

* fix: resolve post-revert type errors and broken test references

- Fix dangling import { in session-manager.ts (leftover from removing globalPause imports)
- Fix duplicate registerStop body in start.ts (leftover parent-version code after catch block)
- Remove --keep-session flag from stop command (was added by #367)
- Fix PullRequestsPage.tsx useSessionEvents call removing dropped initialGlobalPause arg
- Remove globalPause test cases from spawn.test.ts, communication.test.ts, api-routes.test.ts
- Remove unused updateMetadata imports from test files

* fix: remove unused allSessions variable after globalPause removal
2026-04-06 14:58:41 +05:30
harshitsinghbhandari e2b43ec0b2 fix comments 2026-04-05 18:41:46 +05:30
harshitsinghbhandari 2a1b513e7d fix tests 2026-04-04 21:52:50 +05:30
Harsh Batheja 34bc5bb2d5
feat: support multiple concurrent orchestrators with isolated worktrees (#870)
* feat: support multiple concurrent orchestrators with isolated worktrees

- Each orchestrator session gets a numbered ID ({prefix}-orchestrator-N)
  and an isolated git worktree, replacing the single shared orchestrator
- reserveNextOrchestratorIdentity atomically reserves the next available
  number and detects conflicting project prefix configurations early
- isOrchestratorSession / isOrchestratorSessionName accept optional
  allSessionPrefixes for cross-project false-positive prevention
- getProjectPause and resolveGlobalPause track the longest active pause
  across all concurrent orchestrators instead of returning the first found
- cleanupWorktreeAndMetadata helper used consistently on all failure paths
  including post-launch; system prompt file included in cleanup
- pollBacklog, status.ts, ProjectSidebar, sessions page, global-pause,
  project-utils, serialize, and sessions API route all pass
  allSessionPrefixes to isOrchestratorSession
- Web sessions/[id]/page.tsx fetches project prefix map and passes it
  through prefixByProjectRef to avoid stale closure in fetchProjectSessions

* fix: seed sseAttentionLevels from fresh sessions on full refresh

When scheduleRefresh dispatches a reset action, derive sseAttentionLevels
from the freshly fetched sessions using getAttentionLevel. This clears
phantom entries for removed sessions and seeds correct levels for new
sessions, preventing stale favicon color and attention counts until the
next SSE snapshot.

* fix: merge duplicate @/lib/types imports into single import statement

Combining the separate import type and value import from @/lib/types
into one import to satisfy the no-duplicate-imports lint rule.
2026-04-04 12:30:51 +05:30
Dhruv Sharma ed187d976c
Merge pull request #873 from harshitsinghbhandari/feat/736
feat(core): support external tracker, scm, and notifier plugins from config
2026-04-04 02:50:19 +05:30
harshitsinghbhandari 32075b879d fix: address illegalcall review comments
- Fix bad temp name for non-standard packages: use full packageName
  instead of last hyphen-segment to avoid collisions between packages
  like "custom-tracker-plugin" and "my-custom-plugin"

- Fix disabled top-level plugin blocking inline loading: when an
  existing plugin entry has `enabled: false`, it's now set to `true`
  when there's an inline tracker/scm/notifier reference for the same
  package/path

- Create validatePluginConfigFields helper to deduplicate the
  TrackerConfigSchema, SCMConfigSchema, and NotifierConfigSchema
  validation logic (was duplicated thrice)

- Fix multi-project external plugin order-dependency: tracker and SCM
  plugins now receive no project-level config at create() time, making
  them consistent with built-in plugins. Project-specific config is
  passed per-call via ProjectConfig argument, avoiding first-project
  wins behavior

- Document post-validation invariant for plugin field: added
  documentation clarifying that `plugin` is always populated after
  validateConfig() to help downstream consumers understand they can
  safely assume non-null after validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-03 22:09:24 +05:30
Dhruv Sharma c488812245
Merge pull request #756 from ComposioHQ/fix/terminal-statuses-in-lifecycle-poll
fix: use TERMINAL_STATUSES in lifecycle poll accounting
2026-04-03 11:31:32 +05:30
harshitsinghbhandari ddba72a485 Merge branch 'main' of https://github.com/ComposioHQ/agent-orchestrator into feat/736 2026-04-03 02:40:53 +05:30
harshitsinghbhandari a41291fba9 comments fixed 2026-04-03 01:44:59 +05:30
harshitsinghbhandari 9a3a18b2c7 performed review and fixed 2026-04-03 01:23:37 +05:30
harshitsinghbhandari 6884425e3d fix load builtin error 2026-04-03 00:09:40 +05:30
harshitsinghbhandari 59546eaf93 feat(core): unify plugin config cleaning and enforce reserved fields for tracker and scm 2026-04-02 23:53:05 +05:30
harshitsinghbhandari c58bae743b feat(core): enforce reserved fields in notifier configs and improve loader error handling 2026-04-02 23:38:13 +05:30
harshitsinghbhandari 44528b3cee add error handling for path in config 2026-04-02 23:13:14 +05:30
harshitsinghbhandari 1268c7fa28 fix(core): ensure consistent temp plugin name generation 2026-04-02 22:48:57 +05:30
harshitsinghbhandari 7dd4dc9b4a fix: ensure notifier config is passed when manifest name differs
extractPluginConfig was being called with mod.manifest.name before the
config was updated from the temp name. When manifest name differs from
temp name (e.g., temp "teams" vs manifest "ms-teams"), extractPluginConfig
found no match and returned undefined, causing the plugin to be registered
without its configuration (webhook URLs, tokens, etc.).

Fix by reordering operations:
1. Validate and update configs with manifest.name FIRST
2. Extract plugin config AFTER (now finds the config by manifest.name)
3. Register plugin with its correct configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 21:40:53 +05:30
harshitsinghbhandari fcdbe24d6c fix: prevent one project's bad config from breaking shared plugin
When multiple projects share the same external plugin, a misconfiguration
in one project (wrong expectedPluginName) would prevent the plugin from
being registered at all, silently breaking it for all projects.

Fix by:
1. Register the plugin FIRST, before validating individual project configs
2. Validate each project's config in its own try-catch
3. Log validation errors but continue processing other projects

This ensures:
- Plugin is always registered if it loads successfully
- Projects with correct config get updated
- Projects with bad config get a warning but don't break others
- Misconfigured projects fail at point of use with clearer errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 21:28:53 +05:30
harshitsinghbhandari aae6377de4 fix: preserve multi-word plugin names from package specifiers
generateTempPluginName was splitting package names on both hyphens and
slashes, which truncated multi-word names like "jira-cloud" to just
"cloud".

Fix by first extracting the package name without scope, then using a
regex to match the common ao-plugin-{slot}-{name} pattern which
preserves the full plugin name including hyphens.

Examples:
- @acme/ao-plugin-tracker-jira-cloud -> jira-cloud (was: cloud)
- ao-plugin-scm-azure-devops -> azure-devops (was: devops)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 21:14:37 +05:30
harshitsinghbhandari f10f45168a fix: address cursor bugbot review comments
- Strip package and path loading metadata from notifier config passed
  to plugin create(). These fields are used for plugin resolution, not
  plugin-specific configuration. Prevents filesystem paths from leaking
  into plugin config where they could conflict with plugin-specific
  fields of the same name.

- Remove collectExternalPluginConfigs from public API exports since it
  is only used internally within config.ts during validateConfig.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 20:18:28 +05:30
harshitsinghbhandari 3e0ce4e3a9 fix: address remaining PR review comments
- Fix multiple projects sharing same external plugin: change
  findExternalPluginEntry to findAllExternalPluginEntries which returns
  all matching entries, allowing all projects to be updated with the
  actual manifest.name

- Fix path without slashes misidentified as npm package: use separate
  pkg and path parameters in generateTempPluginName instead of combining
  them and checking for slashes. Local paths like "my-tracker" (no
  slashes) are now correctly returned as-is

- Replace console.warn with process.stderr.write for structured logging
  in plugin-registry.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 20:06:28 +05:30
harshitsinghbhandari 812bfa5bef fix: remove unused import and add missing location field
- Remove unused collectExternalPluginConfigs import from tests
- Add missing location field to _externalPluginEntries in test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 19:51:10 +05:30
harshitsinghbhandari 99386a595e fix: address Copilot review feedback on external plugin support
1. Only set expectedPluginName when user explicitly specifies `plugin`
   - Auto-generated temp names no longer block manifest name inference
   - External plugins like @acme/ao-plugin-tracker-jira-cloud with
     manifest.name "jira-cloud" will now load correctly

2. Add structured location data to ExternalPluginEntryRef
   - New ExternalPluginLocation type with kind/projectId/configType
   - Avoids fragile string parsing of source (project/notifier keys
     can legally contain dots)

3. Expand home paths (~/) in inline plugin configs
   - Apply expandHome() to tracker/scm/notifier path fields
   - Consistent behavior with config.plugins path expansion

4. Improve error messages
   - Use "package" vs "path" in manifest mismatch errors instead
     of always saying "package"

5. Fix tests to use config._externalPluginEntries
   - Tests now check entries stored by validateConfig() instead
     of calling collectExternalPluginConfigs() again on modified config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 19:38:05 +05:30
harshitsinghbhandari 3dd8a0c316 feat(core): support external tracker, scm, and notifier plugins from config
Add support for specifying external plugins inline in tracker, scm, and
notifier configs using optional `package` or `path` fields.

**Config schema changes:**
- TrackerConfig, SCMConfig, NotifierConfig now accept optional `package`
  (npm) or `path` (local) fields alongside the existing `plugin` field
- When only `package`/`path` is specified, plugin name is auto-inferred
- When both `plugin` and `package`/`path` are specified, manifest.name
  is validated against the declared plugin name

**Example usage:**
```yaml
projects:
  my-app:
    tracker:
      plugin: jira  # optional - validates against manifest.name
      package: "@acme/ao-plugin-tracker-jira"
      teamId: "TEAM-123"
    scm:
      path: ./plugins/my-scm  # local plugin

notifiers:
  teams:
    package: "@acme/ao-plugin-notifier-teams"
```

**Implementation details:**
- collectExternalPluginConfigs() extracts external plugin entries from
  tracker/scm/notifier configs during config validation
- Auto-generates InstalledPluginConfig entries merged into config.plugins
- Plugin registry validates manifest.name matches expectedPluginName and
  updates config with actual manifest.name after loading
- Warns when plugin slot doesn't match configured slot

Closes #736

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 19:04:47 +05:30
Your Name 9e78fe3b23 fix: resolve prompt delivery persistence and false warnings 2026-04-02 17:41:46 +05:30
Your Name feecc6f177 fix: robust prompt delivery with retries and CLI warnings 2026-04-02 17:27:53 +05:30
Harsh Batheja 53ef778357
feat: add CI failure detail notifications in lifecycle manager (#850)
* feat: add CI failure detail notifications in lifecycle manager

When CI fails on a PR, the lifecycle manager now fetches individual check
details (names, statuses, URLs) and sends them to the worker session.
This complements the existing static reaction message with actionable
debugging information.

Flow:
- On first transition to ci_failed: static reaction message fires (existing)
- On next poll: detailed CI failure info with check names and URLs dispatched
- Fingerprinting prevents re-sending the same failure set
- New/changed failures trigger fresh detailed notifications
- Tracking metadata cleared when PR is merged/closed or CI passes

Follows the same deduplication pattern as maybeDispatchReviewBacklog().

* fix: send CI details directly to avoid consuming reaction retry budget

The detailed CI failure dispatch now uses sessionManager.send() directly
instead of executeReaction(), so it doesn't increment the ci-failed
reaction tracker. This prevents low retries/escalateAfter settings from
causing premature escalation before the agent receives failure details.

The transition reaction still owns escalation; the detailed dispatch is
purely informational follow-up delivery.

* feat: add merge conflict notifications in lifecycle manager

Adds maybeDispatchMergeConflicts() that detects merge conflicts from
the PR enrichment cache or getMergeability() and notifies the worker
session. Conflicts are dispatched independently of session status since
they can coexist with ci_failed, changes_requested, etc.

- Uses the existing merge-conflicts reaction config
- Dispatches once per conflict occurrence (tracks lastMergeConflictDispatched)
- Clears tracking when conflicts resolve, allowing re-dispatch if they recur
- Sends directly via sessionManager.send() (same pattern as CI details)

* fix: use CICheck type instead of inline type declaration

Replace inline Array<{ name, status, url, conclusion }> with the
existing CICheck type from ./types.js for formatCIFailureMessage and
the checks variable in maybeDispatchCIFailureDetails.

* fix: resolve no-useless-assignment lint error in merge conflict check

Declare hasConflicts without initial value since both branches of the
if/else assign to it before it's read.

* feat: show agent notification state in session page blockers

The blockers section on both SessionDetail (IssuesList) and SessionCard
(alert pills) now shows whether the agent has been notified about each
blocker. Reads lifecycle manager dispatch metadata:
- lastCIFailureDispatchHash for CI failures
- lastMergeConflictDispatched for merge conflicts
- lastPendingReviewDispatchHash for review comments

Displays "agent notified" indicator next to blockers where the lifecycle
manager has already forwarded the issue to the worker session.

* fix: use lifecycle status as fallback for blockers when PR data is stale

The blockers section now uses the lifecycle manager's session status
metadata as a source of truth when PR enrichment data hasn't caught up.
PR enrichment uses a 5-min cache and can timeout or be rate-limited,
causing blockers to show stale/incorrect state.

Changes:
- IssuesList and getAlerts now check metadata["status"] (lifecycle
  manager state) alongside PR enrichment data
- CI failing: shown when pr.ciStatus is "failing" OR lifecycle status
  is "ci_failed"
- Changes requested: shown when pr.reviewDecision matches OR lifecycle
  status is "changes_requested"
- Merge conflicts: shown when pr.mergeability.noConflicts is false OR
  lifecycle dispatch metadata indicates conflicts were detected

* fix: fall back to getMergeability when cached hasConflicts is undefined

When PREnrichmentData has hasConflicts as undefined (the field is typed
as boolean | undefined), the previous check treated it as no conflicts.
Now falls through to the getMergeability() call instead.

* test: add coverage for CI/conflict notify action and recovery paths

- Test CI tracking clears when CI recovers to passing
- Test notify action for CI failure details (human notification path)
- Test notify action for merge conflicts (human notification path)

These cover the previously uncovered notify action branches and the
CI recovery cleanup path in the lifecycle manager.

* fix: resolve typecheck error in CI recovery test

writeMetadata requires SessionMetadata type which doesn't include
custom keys like lastCIFailureFingerprint. Rewrote the test to use
setupCheck and let the lifecycle manager set tracking metadata
naturally through the CI failure flow, then verify cleanup on recovery.

* fix: don't use dispatch metadata for conflict detection in UI

lastMergeConflictDispatched lingers after conflicts resolve until the
lifecycle manager's next poll clears it. Using it as a conflict signal
caused stale "merge conflict" alerts. Now only pr.mergeability.noConflicts
drives conflict detection; the metadata is only used for the "agent
notified" badge.

* fix: use Promise.allSettled for dispatch functions to avoid orphaned rejections

Promise.all rejects immediately on first failure, leaving in-flight
promises unmonitored. Promise.allSettled waits for all to complete.
2026-04-02 11:23:08 +05:30
Harsh Batheja df8395c3ec
feat: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755)
* feat: standardize agent plugins with shared hooks, activity JSONL, and CLAUDE.md

- Add CLAUDE.md with full project context and agent plugin implementation standards
- Extract shared PATH-wrapper metadata hooks into @composio/ao-core (agent-workspace-hooks.ts)
- Backfill Aider + OpenCode: setupWorkspaceHooks, postLaunchSetup, getSessionInfo, getRestoreCommand
- Add recordActivity method to Agent interface for terminal-derived JSONL writing
- Create activity-log.ts in core: appendActivityEntry, readLastActivityEntry
- Lifecycle manager calls recordActivity before getActivityState for agents that implement it
- Upgrade detectActivity in Aider/OpenCode with real terminal prompt/permission patterns
- Upgrade Codex getActivityState to parse JSONL entry types (6 states, up from 2)
- Replace duplicated normalizePermissionMode with shared normalizeAgentPermissionMode from core
- Remove ~200 lines of duplicated shell wrapper code from Codex plugin
- Add git wrapper detection for existing branch switches (parity with Claude Code hooks)
- 484 tests passing across all 4 agent plugins

* chore: unignore CLAUDE.md and AGENTS.md, slim down AGENTS.md to pointer

- Remove CLAUDE.md and AGENTS.md from .gitignore (both should be tracked)
- Slim AGENTS.md from 96 to 30 lines — commands, TL;DR, key files
- Full context now lives in CLAUDE.md; AGENTS.md points there

* fix: eliminate redundant double file read in readLastActivityEntry

Remove the readLastJsonlEntry call that was only used as a null-check,
then immediately discarded before re-reading the same file from scratch.
Now performs a single open + stat + tail-read per poll cycle.

* fix: remove duplicate case-insensitive regex in OpenCode detectActivity

The \(y\)es.*\(n\)o pattern with /i flag was identical to the preceding
\(Y\)es.*\(N\)o/i check — remove the redundant line.

* fix: use zero-initialized buffer and slice to bytesRead in readLastActivityEntry

Replace Buffer.allocUnsafe with Buffer.alloc and slice the result to
actual bytesRead, preventing uninitialized heap data from being parsed
if the file shrinks between stat() and read().

* fix: add staleness cap for waiting_input/blocked and deduplicate recordActivity

- Add ACTIVITY_INPUT_STALENESS_MS (5 min) cap so stale waiting_input/blocked
  entries don't keep sessions stuck in needs_input on the dashboard forever.
- Extract checkActivityLogState() into core — shared by aider, opencode, codex.
- Extract classifyTerminalActivity() into core — deduplicates the identical
  recordActivity logic across all three plugins.

* fix: prioritize native Codex JSONL over AO activity log in getActivityState

Reorder detection so Codex's native 6-state JSONL (approval_request,
error, tool_call, etc.) is checked first. AO activity JSONL from
terminal parsing is now a fallback only for waiting_input/blocked states
that the native JSONL may not have captured. Previously the AO log was
always fresh (written every poll cycle by recordActivity) and shadowed
the richer native detection entirely.

* fix: restrict AO activity JSONL to waiting_input/blocked only

checkActivityLogState now only returns results for waiting_input/blocked
states. Non-critical states (active/ready/idle) return null, forcing
callers to fall through to their native signals (git commits, chat
history, OpenCode API, Codex native JSONL). This prevents the lifecycle
manager's recordActivity writes (which refresh mtime every poll cycle)
from shadowing richer detection methods and breaking stuck-detection.

* fix: prevent stale idle timestamp in aider and skip flags in git wrapper

- Remove Aider's fallback that returned idle with activityResult.modifiedAt
  (always fresh due to recordActivity writes). Now returns null when no
  git commits or chat history are found, letting the lifecycle manager
  handle stuck-detection correctly.
- Fix git wrapper catch-all case to skip flag arguments (e.g. -B) and
  look at $3 for the actual branch name.

* fix: add mtime fallback for empty Codex JSONL and remove unused exports

- When the native Codex session file exists but readLastJsonlEntry returns
  null (empty/unparseable), fall back to stat-based mtime detection instead
  of losing activity detection entirely.
- Remove unused exports getActivityLogPath and ACTIVITY_INPUT_STALENESS_MS
  from @composio/ao-core barrel — they are only used internally.

* fix: opencode activity state detection and CLAUDE.md agent plugin standards

- Fix session ID capture to handle both session_id (snake_case) and
  sessionID (camelCase) from OpenCode 1.3.x JSON responses
- Replace broken --command true with -- noop for session creation
  (true is not a valid OpenCode command since 1.3.x)
- Add JSONL mtime fallback in getActivityState so active/ready/idle
  states work even when findOpenCodeSession returns null
- Rewrite CLAUDE.md activity detection section with the full
  getActivityState contract, mandatory JSONL mtime fallback pattern,
  and 8 required tests every agent plugin must implement

* fix: opencode --command true flag and activity JSONL mtime staleness

- Fix opencode getLaunchCommand to use `--command true` instead of `-- noop`
  (aligns with test expectations and opencode CLI docs)
- Fix checkActivityLogState to use entry.ts instead of file mtime for
  staleness checking — recordActivity refreshes mtime every poll cycle,
  which prevented stale waiting_input/blocked entries from being detected
- Fix opencode getActivityState fallback to use entry state directly
  instead of re-deriving from mtime, which always returned "active"
  because recordActivity refreshes the file every cycle
- Update tests to reflect new entry-state-based fallback behavior

* fix: deduplicate recordActivity writes and restore mtime-based fallback

recordActivity was writing to the JSONL every poll cycle (~30s), which
kept refreshing the file mtime and prevented the JSONL mtime fallback
in getActivityState from ever reaching "ready" or "idle".

Fix: skip writes when the state hasn't changed and the last entry is
<20s old. This keeps mtime fresh during active work (writes every
20-30s, within the 30s activeWindow) but lets it age naturally when
the agent goes quiet.

Also restores the mtime-based age classification in the JSONL fallback
(active/ready/idle by mtime age) instead of returning the entry state
directly, which was always "active" since that's what recordActivity
writes.

Applied to both OpenCode and Aider plugins. Updated CLAUDE.md with
the dedup pattern and rationale.

* fix: align integration tests with opencode `-- noop` launch command

Update 9 test expectations from `--command true` to `-- noop` to match
the reverted getLaunchCommand implementation.

* fix: add JSONL mtime fallback to Aider getActivityState

When git commits and chat history are both unavailable (e.g. early
session startup), Aider's getActivityState now falls back to the AO
activity JSONL mtime for active/ready/idle classification — matching
OpenCode's existing step 3 fallback. Previously it returned null,
leaving the dashboard with no activity signal.

* fix: add write deduplication to Codex recordActivity

Add the same dedup logic that Aider and OpenCode already have — skip
writes when the state hasn't changed and the last entry is recent
(<20s). Prevents unbounded file growth and stale mtime refreshes.

* refactor: extract shared recordTerminalActivity into core

Move the duplicated recordActivity logic (classify + dedup + append)
from all three plugins into a shared `recordTerminalActivity` function
in `@composio/ao-core/activity-log`. Each plugin's `recordActivity`
is now a thin wrapper that delegates to the shared function.

Add core tests for classifyTerminalActivity, checkActivityLogState,
and recordTerminalActivity (10 tests).

* fix: validate JSONL entry fields, handle invalid dates, consistent operators

- Validate required fields (ts, state, source) before casting parsed
  JSON to ActivityLogEntry — prevents malformed entries from propagating
- Guard against invalid Date parsing in checkActivityLogState — returns
  null instead of comparing against NaN
- Use <= instead of < in Aider chat-history threshold comparisons to
  match OpenCode and Aider's own JSONL fallback path

* fix: use --command true for opencode run, validate sed key parameter

- Replace `-- noop` with `--command true` in opencode getLaunchCommand
  so the bootstrap uses a valid command
- Validate metadata key against [a-zA-Z0-9_-]+ in the git wrapper's
  update_ao_metadata to prevent sed metacharacter injection

* fix: extract DEFAULT_ACTIVE_WINDOW_MS constant, clarify Codex recordActivity

- Extract magic number 30_000 into DEFAULT_ACTIVE_WINDOW_MS constant in
  core types, used by Aider and OpenCode for active/ready thresholds
- Clarify in CLAUDE.md that Codex implements recordActivity as a safety
  net for when its native JSONL is missing/unparseable, not redundantly

* fix: add JSONL mtime fallback to Codex getActivityState

When native Codex session file is missing but AO JSONL has data,
derive active/ready/idle from JSONL mtime instead of returning null.
Matches the fallback pattern already in Aider (step 4) and OpenCode
(step 3).

* fix: validate ActivityState and source values when parsing JSONL entries

Validate that `state` is one of the known ActivityState values and
`source` is "terminal" or "native" before constructing the entry.
Construct the entry explicitly instead of using unsafe double cast.

* fix: wrap getOutput + recordActivity in try-catch to protect getActivityState

If runtime.getOutput() throws (e.g. tmux unresponsive), the error
previously propagated to the outer catch, skipping getActivityState
entirely. Now the entire recordActivity preamble is wrapped in its
own try-catch so getActivityState always runs.

* chore: add .ao/ to gitignore

* chore: clean up gitignore comment

* test: add coverage for activity-log and agent-workspace-hooks

- activity-log: test readLastActivityEntry (missing file), invalid
  entry.ts in checkActivityLogState, blocked state path
- agent-workspace-hooks: test buildAgentPath (dedup, defaults,
  ordering), setupPathWrapperWorkspace (create/skip wrappers,
  AGENTS.md create/skip)

Raises diff coverage from 39% toward 80% threshold.

* test: add real file I/O tests for readLastActivityEntry and recordTerminalActivity

Test readLastActivityEntry with actual temp files: valid entries,
empty file, invalid JSON, invalid state, missing fields, multi-line.
Test recordTerminalActivity dedup logic and actionable state bypass.

* fix: add activeWindow threshold to Codex native JSONL state detection

Action entries (tool_call, user_input, exec_command) now use the 30s
active window: <=30s is "active", 30s-5min is "ready", >5min is "idle".
Previously these skipped "ready" entirely, going straight from "active"
to "idle" at the 5min threshold.

* fix: add activeWindow threshold to Claude Code native JSONL state detection

Same fix as Codex — action entries (user, tool_use, progress) now use
the 30s active window for consistent 3-state classification across
all agent plugins: <=30s active, 30s-5min ready, >5min idle.

* fix: handle truncated JSONL, add exited state, fix session lookup and dedup window

- readLastActivityEntry: increase tail buffer to 4KB, skip truncated
  first line when reading from offset, try lines from end on parse error
- Add "exited" to valid ActivityState set in JSONL validation
- findOpenCodeSession: pick most recently updated session when multiple
  title matches exist, preventing stale session binding
- Increase dedup window from 20s to 60s so mtime can age past the 30s
  active window between writes, allowing "ready" state to be reached

* fix: use entry state for JSONL fallback, write AGENTS.md to .ao/, revert dedup to 20s

- Replace mtime-based active/ready/idle derivation in all 3 plugin
  fallbacks with direct entry.state + entry.ts usage. This eliminates
  the fundamental conflict between write deduplication and mtime
  freshness — the entry already has the correct detected state.
- Revert dedup window to 20s (purely I/O optimization, no longer
  affects state detection since mtime is not used)
- Write AO session context to .ao/AGENTS.md (gitignored) instead of
  modifying the repo-tracked AGENTS.md, preventing dirty worktree state

* fix: reorder Codex fallback chain so AO JSONL is checked before stat mtime

When native JSONL exists but can't be parsed, the stat() fallback
previously returned early, skipping AO JSONL waiting_input/blocked
detection and the ready state. Now it falls through to AO JSONL first,
then uses stat mtime as a last resort with proper 3-state classification.

* fix: re-validate canonicalized ao_dir against trusted roots

After resolving symlinks with pwd -P, re-check real_ao_dir against
the trusted root allowlist. Prevents paths like /tmp/../../home/user
from passing the pre-canonicalization check then escaping to arbitrary
directories after symlink resolution.

* fix: update tests for .ao/AGENTS.md location and remove unused vi import

- Update Codex setupWorkspaceHooks tests to expect .ao/AGENTS.md
  instead of workspace root AGENTS.md
- Remove unused vi import from activity-log test (lint error)

* fix: add age-based decay to JSONL entry fallback via getActivityFallbackState

Extract getActivityFallbackState in core — reclassifies entry state
based on entry.ts age (active→ready→idle) so old entries don't stay
as "active" forever when recordActivity stops being called. All three
plugins now use this shared helper for their JSONL fallback paths.

* fix: apply staleness cap to actionable states in getActivityFallbackState

Stale waiting_input/blocked entries (older than ACTIVITY_INPUT_STALENESS_MS)
are now treated as idle in the fallback, preventing them from bypassing
the staleness filtering in checkActivityLogState.

* fix: respect entry state as ceiling in getActivityFallbackState

Age-based decay can only demote (active→ready→idle), never promote.
A fresh "idle" entry stays "idle" instead of being reclassified as
"active" — the detected state from terminal output is authoritative.

* docs: update CLAUDE.md to match current activity detection implementation

- Replace inline recordActivity dedup example with recordTerminalActivity delegation
- Replace mtime-based fallback example with getActivityFallbackState
- Update step 4 description: entry state + age-based decay, not mtime
- Add new core exports to utilities section
- Document .ao/AGENTS.md location and setupPathWrapperWorkspace
- Update required test list for entry-based fallback

* fix: skip metadata helper rewrite when version marker matches

Move ao-metadata-helper.sh write inside the needsUpdate check so it's
only rewritten when wrapper scripts are outdated, not on every call.

* fix: update Codex test for metadata helper skip when version matches

Metadata helper is now inside the needsUpdate check, so when the
version marker matches, no wrapper writes occur (including helper).
2026-04-01 21:50:07 +05:30
Dhruv Sharma 1701e03aac
Merge pull request #833 from ComposioHQ/feat/plugin-marketplace
Feat/plugin marketplace
2026-03-31 20:01:39 +05:30
Dhruv Sharma f9b3a4af20
Merge pull request #831 from ComposioHQ/feat/core-plugin-config
Feat/core plugin config
2026-03-31 19:55:16 +05:30
Dhruv Sharma 6cfb1cc864 fix: guard statSync against permission errors, add fetch timeout
- Wrap statSync in try/catch in resolveLocalPluginEntrypoint so
  permission-denied errors return null instead of crashing
- Add 30s AbortSignal timeout to refreshMarketplaceCatalog fetch
  to prevent indefinite hangs on slow networks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:37:52 +05:30
Dhruv Sharma d36c0947cf refactor: deduplicate plugin resolution logic between core and CLI
Export isPluginModule, normalizeImportedPluginModule,
resolvePackageExportsEntry, and resolveLocalPluginEntrypoint from
@composio/ao-core and import them in the CLI instead of maintaining
independent copies that have already diverged.

Removes ~70 lines of duplicated code from plugin-marketplace.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:53:30 +05:30
harshitsinghbhandari 23e006b00c fix: add type casts for mock promisify and registry.get
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 13:25:17 +05:30
harshitsinghbhandari 23f2f0dbeb fix: remove unused variable declarations in session-manager test
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 13:25:17 +05:30
harshitsinghbhandari 994297ca7e test: clean up session directories in recovery-actions afterEach
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 13:25:17 +05:30
harshitsinghbhandari 03e3426625 test: clarify error propagation test scope in session-manager
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 13:25:17 +05:30
harshitsinghbhandari 8fdcaff11e test: fix execFile mock to match Node callback contract
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 13:25:17 +05:30
harshitsinghbhandari 4a68fd9f99 test(core): fix timeouts in session manager communication tests
- Mock runtime output changes in OpenCode unit tests to trigger early
  delivery confirmation, reducing poll time from 3s to 500ms.
- Increase timeout for confirmation fallback tests to 10s to accommodate
  process-spawn overhead of mock binaries on some systems.
- Ensures reliable CI execution for @composio/ao-core tests.
2026-03-31 10:51:54 +05:30
harshitsinghbhandari 4d91457a01 test: ensure fake timers are restored in afterEach cleanup
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 10:14:59 +05:30
Dhruv Sharma 4c39dbe3ad refactor(plugin-registry): improve plugin specifier resolution and error handling 2026-03-31 04:24:30 +05:30
Dhruv Sharma 147cd2ea37 support custom import function in plugin registry 2026-03-31 04:02:14 +05:30
Dhruv Sharma 582c296737 add InstalledPluginConfig type and plugin config schema 2026-03-31 04:02:14 +05:30
harshitsinghbhandari 37777bc55b test: add retry loop coverage for deleteSession
Add comprehensive tests for retry logic including retry count,
delays, error propagation, early exit, and not-found handling.

Fixes Issue #5

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 00:35:07 +05:30
harshitsinghbhandari 282e8ae9fd test: add error path coverage for recovery system
Add tests for runtime.isAlive, workspace.destroy, and runtime.destroy
error handling to ensure cleanup continues despite partial failures.

Fixes test coverage gaps in validator and cleanup actions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-30 23:58:23 +05:30
Gautam Tayal 56b36613f6 refactor(cli): streamline agent handling in spawn command 2026-03-30 15:32:15 +05:30
Gautam Tayal 8c23903fa2 feat: add agent selection prompt 2026-03-30 15:32:15 +05:30
Deepak Veluvolu 852f1f93c8
fix: added GraphQL batch PRfeat: add GraphQL batch PR enrichment for orchestrator polling enrichment for orchestrator polling (fixes #608) (#637)
## Approach

  The orchestrator polling loop previously made individual API calls for each PR's
  state, CI status, and review decision - 3 separate calls per PR per poll.
  With multiple PRs being monitored, this quickly exhausted GitHub's 5,000-point
  hourly rate limit.

  This PR implements GraphQL batching using aliases, which allows fetching data
  for up to 25 PRs in a single GraphQL query. Additionally, a 2-Guard ETag
  strategy is used to skip queries entirely when nothing has changed.

  ## Implementation

  ### GraphQL Batching
  - `generateBatchQuery()` creates a single GraphQL query with unique aliases (pr0, pr1, pr2...)
  - Each PR gets the same set of fields: state, CI status, review decision, mergeability
  - Uses inline fragments for union types (CheckRun/StatusContext)
  - Variable types: String! for owner/repo, Int! for PR numbers

  ### 2-Guard ETag Strategy
  Before running expensive GraphQL queries, two lightweight REST ETag checks detect if
  anything changed:

  **Guard 1 (PR List ETag):**
  - Checks `/repos/{owner}/{repo}/pulls` with If-None-Match header
  - Returns 304 if no changes → skips GraphQL (0 points)
  - Detects: New commits, title/body edits, labels, reviews, state changes

  **Guard 2 (Commit Status ETag):**
  - Checks `/repos/{owner}/{repo}/commits/{sha}/status` per cached PR
  - Returns 304 if no changes → skips GraphQL (0 points)
  - Detects: CI status transitions (failing → passing, passing → failing, etc.)

  ### Caching
  - LRU caches for PR metadata (max 200 entries), ETags (100/500 entries)
  - Cache misses trigger individual API fallback via lifecycle-manager
  - No placeholder caching on errors - allows proper fallback behavior

  ## Impact

  - **API reduction:** ~88% fewer REST calls (216 vs 1,800 calls/hour for 5 PRs)
  - **GraphQL efficiency:** Batch query fetches 25 PRs for ~40 points vs ~400 for individual calls
  - **Polling interval:** Still 30s, but most polls return cached data (0 cost)
  - **Fallback:** Individual SCM calls still work for edge cases (permissions, cache misses)

  ## Testing

  - Unit tests for query generation and parsing helpers
  - Integration tests for real GraphQL API calls (skipped by default)
  - Covers batch failures, partial success, empty arrays, edge cases
2026-03-30 00:31:04 +05:30
harshitsinghbhandari 8cafb578ba fix(core): sync enrichment timeout with discovery constant to ensure sufficient headroom 2026-03-29 18:47:00 +05:30